@dereekb/firebase-server 14.0.1 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/calcom/index.esm.js +3 -2
  2. package/calcom/package.json +11 -10
  3. package/calcom/src/lib/calcom.oauth.connection.service.d.ts +2 -2
  4. package/discord/index.esm.js +174 -19
  5. package/discord/package.json +11 -10
  6. package/discord/src/lib/discord.oauth.connection.config.d.ts +43 -2
  7. package/discord/src/lib/discord.oauth.connection.module.d.ts +18 -1
  8. package/discord/src/lib/discord.oauth.connection.service.d.ts +40 -3
  9. package/index.esm.js +9 -1
  10. package/mailgun/package.json +10 -9
  11. package/mcp/package.json +12 -11
  12. package/model/index.esm.js +5182 -2369
  13. package/model/package.json +10 -9
  14. package/model/src/lib/userexternalconnection/index.d.ts +2 -0
  15. package/model/src/lib/userexternalconnection/oauth/index.d.ts +1 -0
  16. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.config.d.ts +51 -0
  17. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.controller.d.ts +49 -0
  18. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.refresh.d.ts +35 -0
  19. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.service.d.ts +300 -7
  20. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.state.d.ts +213 -12
  21. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.throttle.d.ts +81 -0
  22. package/model/src/lib/userexternalconnection/userexternalconnection.action.server.d.ts +214 -1
  23. package/model/src/lib/userexternalconnection/userexternalconnection.error.d.ts +118 -5
  24. package/model/src/lib/userexternalconnection/userexternalconnection.module.d.ts +52 -2
  25. package/model/src/lib/userexternalconnection/userexternalconnection.policy.d.ts +117 -0
  26. package/model/src/lib/userexternalconnection/userexternalconnection.signin.d.ts +286 -0
  27. package/oidc/package.json +11 -10
  28. package/package.json +12 -12
  29. package/src/lib/auth/auth.service.d.ts +26 -0
  30. package/test/package.json +12 -11
  31. package/twilio/package.json +9 -8
  32. package/zoho/index.esm.js +3 -2
  33. package/zoho/package.json +11 -10
  34. package/zoho/src/lib/zoho.oauth.connection.service.d.ts +2 -2
@@ -462,9 +462,10 @@ function _type_of$1(obj) {
462
462
  _create_class(CalcomUserExternalConnectionOAuthService, [
463
463
  {
464
464
  key: "authorizeUrlForState",
465
- value: function authorizeUrlForState(state) {
465
+ value: function authorizeUrlForState(input) {
466
+ // Cal.com's authorize URL takes no PKCE challenge, so `input.codeChallenge` is deliberately unused
466
467
  return this.authorizeUrlFactory({
467
- state: state
468
+ state: input.state
468
469
  });
469
470
  }
470
471
  },
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/calcom",
3
- "version": "14.0.1",
3
+ "version": "14.2.0",
4
+ "sideEffects": false,
4
5
  "type": "module",
5
6
  "peerDependencies": {
6
- "@dereekb/analytics": "14.0.1",
7
- "@dereekb/calcom": "14.0.1",
8
- "@dereekb/date": "14.0.1",
9
- "@dereekb/firebase": "14.0.1",
10
- "@dereekb/firebase-server": "14.0.1",
11
- "@dereekb/model": "14.0.1",
12
- "@dereekb/nestjs": "14.0.1",
13
- "@dereekb/rxjs": "14.0.1",
14
- "@dereekb/util": "14.0.1",
7
+ "@dereekb/analytics": "14.2.0",
8
+ "@dereekb/calcom": "14.2.0",
9
+ "@dereekb/date": "14.2.0",
10
+ "@dereekb/firebase": "14.2.0",
11
+ "@dereekb/firebase-server": "14.2.0",
12
+ "@dereekb/model": "14.2.0",
13
+ "@dereekb/nestjs": "14.2.0",
14
+ "@dereekb/rxjs": "14.2.0",
15
+ "@dereekb/util": "14.2.0",
15
16
  "@nestjs/common": "^12.0.1",
16
17
  "express": "^5.2.1"
17
18
  },
@@ -1,6 +1,6 @@
1
1
  import { type CalcomAccessToken, type CalcomOAuthAuthorizeUrlFactory } from '@dereekb/calcom';
2
2
  import { CalcomOAuthApi } from '@dereekb/calcom/nestjs';
3
- import { AbstractUserExternalConnectionOAuthService, UserExternalConnectionAccessor, UserExternalConnectionServerActions, UserExternalConnectionStateCoder, type UserExternalConnectionCredentials, type UserExternalConnectionOAuthExchangeInput, type UserExternalConnectionOAuthRefreshCredentialsInput, type UserExternalConnectionOAuthState } from '@dereekb/firebase-server/model';
3
+ import { AbstractUserExternalConnectionOAuthService, UserExternalConnectionAccessor, UserExternalConnectionServerActions, UserExternalConnectionStateCoder, type UserExternalConnectionCredentials, type UserExternalConnectionOAuthExchangeInput, type UserExternalConnectionOAuthRefreshCredentialsInput, type UserExternalConnectionOAuthAuthorizeUrlInput } from '@dereekb/firebase-server/model';
4
4
  import { type WebsiteUrl } from '@dereekb/util';
5
5
  import { CalcomUserExternalConnectionOAuthServiceConfig } from './calcom.oauth.connection.config';
6
6
  /**
@@ -27,7 +27,7 @@ export declare class CalcomUserExternalConnectionOAuthService extends AbstractUs
27
27
  readonly oauthApi: CalcomOAuthApi;
28
28
  readonly authorizeUrlFactory: CalcomOAuthAuthorizeUrlFactory;
29
29
  constructor(config: CalcomUserExternalConnectionOAuthServiceConfig, stateCoder: UserExternalConnectionStateCoder, userExternalConnectionActions: UserExternalConnectionServerActions, userExternalConnectionAccessor: UserExternalConnectionAccessor, oauthApi: CalcomOAuthApi);
30
- protected authorizeUrlForState(state: UserExternalConnectionOAuthState): WebsiteUrl;
30
+ protected authorizeUrlForState(input: UserExternalConnectionOAuthAuthorizeUrlInput): WebsiteUrl;
31
31
  protected credentialsForAuthorizationCode(input: UserExternalConnectionOAuthExchangeInput): Promise<UserExternalConnectionCredentials>;
32
32
  refreshCredentials(input: UserExternalConnectionOAuthRefreshCredentialsInput): Promise<UserExternalConnectionCredentials>;
33
33
  }
@@ -1,6 +1,6 @@
1
1
  import { DISCORD_USER_EXTERNAL_CONNECTION_PROVIDER_TYPE } from '@dereekb/firebase';
2
- import { userExternalConnectionOAuthControllerPath, userExternalConnectionOAuthRoutesForGlobalRouteExclude, UserExternalConnectionOAuthServiceConfig, userExternalConnectionOAuthServiceConfigFactory, UserExternalConnectionStateCoder, UserExternalConnectionServerActions, UserExternalConnectionAccessor, AbstractUserExternalConnectionOAuthService, AbstractUserExternalConnectionOAuthController } from '@dereekb/firebase-server/model';
3
- import { Injectable, Inject, Controller } from '@nestjs/common';
2
+ import { userExternalConnectionOAuthControllerPath, userExternalConnectionOAuthRoutesForGlobalRouteExclude, UserExternalConnectionOAuthServiceConfig, userExternalConnectionOAuthServiceConfigFactory, UserExternalConnectionStateCoder, UserExternalConnectionServerActions, UserExternalConnectionAccessor, UserExternalConnectionSignInService, UserExternalConnectionProviderPolicyRegistry, UserExternalConnectionSignInThrottle, AbstractUserExternalConnectionOAuthService, AbstractUserExternalConnectionOAuthController } from '@dereekb/firebase-server/model';
3
+ import { Injectable, Inject, Optional, Controller } from '@nestjs/common';
4
4
  import { DISCORD_OAUTH_SCOPE_DELIMITER } from '@dereekb/discord';
5
5
  import { DiscordOAuthApi } from '@dereekb/discord/nestjs';
6
6
  import { ConfigModule } from '@nestjs/config';
@@ -126,9 +126,28 @@ function _type_of$3(obj) {
126
126
  * — a token granted no scopes can do nothing — and it is what lets the connection be labeled with the
127
127
  * account it belongs to. Excludes `email`, which is a real privilege escalation the label does not
128
128
  * need, and `guilds` / `connections` / `bot`, which nothing here reads.
129
+ *
130
+ * An app using Discord to SIGN IN normally wants `['identify', 'email']`: without `email` the
131
+ * identity carries none, which both fails the auto-create delegate's default email requirement and
132
+ * silently skips the sign-in service's existing-account collision check.
129
133
  */ var DEFAULT_DISCORD_OAUTH_SCOPES = [
130
134
  'identify'
131
135
  ];
136
+ /**
137
+ * The scopes requested for an IDENTITY round trip — a sign-in, or linking Discord as a login method.
138
+ *
139
+ * A separate default from {@link DEFAULT_DISCORD_OAUTH_SCOPES} because the two answer different
140
+ * questions. The data default is least privilege for whatever the integration reads; this one is what
141
+ * it takes to know WHO signed in, which needs `email` — without it the identity carries none, which
142
+ * both fails the shipped auto-create delegate's email requirement and silently skips the sign-in
143
+ * service's existing-account collision check.
144
+ *
145
+ * Keeping them separate is what lets an app request a broad data grant without every login also
146
+ * demanding it, and vice versa.
147
+ */ var DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES = [
148
+ 'identify',
149
+ 'email'
150
+ ];
132
151
  /**
133
152
  * Configuration for the {@link DiscordUserExternalConnectionOAuthService}.
134
153
  *
@@ -141,7 +160,11 @@ function _type_of$3(obj) {
141
160
  function DiscordUserExternalConnectionOAuthServiceConfig() {
142
161
  _class_call_check$2(this, DiscordUserExternalConnectionOAuthServiceConfig);
143
162
  var _this;
144
- _this = _call_super$2(this, DiscordUserExternalConnectionOAuthServiceConfig, arguments), _define_property$2(_this, "scopes", void 0);
163
+ _this = _call_super$2(this, DiscordUserExternalConnectionOAuthServiceConfig, arguments), /**
164
+ * The scopes requested for a DATA connect.
165
+ */ _define_property$2(_this, "scopes", void 0), /**
166
+ * The scopes requested for a sign-in or a login-method link.
167
+ */ _define_property$2(_this, "signInScopes", void 0);
145
168
  return _this;
146
169
  }
147
170
  return DiscordUserExternalConnectionOAuthServiceConfig;
@@ -154,18 +177,22 @@ function _type_of$3(obj) {
154
177
  * code-declared paths. The client credentials are read by `discordOAuthServiceConfigFactory` in
155
178
  * `@dereekb/discord/nestjs`, so registering Discord adds no deployment configuration here.
156
179
  *
157
- * @param config - The env service, the return paths, and the optional scope override.
180
+ * @param config - The env service, the return paths, and the optional scope overrides.
158
181
  * @returns The validated service configuration.
159
182
  */ function discordUserExternalConnectionOAuthServiceConfigFactory(config) {
160
- var envService = config.envService, successPath = config.successPath, failurePath = config.failurePath, scopes = config.scopes;
183
+ var envService = config.envService, successPath = config.successPath, failurePath = config.failurePath, signInSuccessPath = config.signInSuccessPath, signInFailurePath = config.signInFailurePath, allowedReturnPaths = config.allowedReturnPaths, scopes = config.scopes, signInScopes = config.signInScopes;
161
184
  var baseConfig = userExternalConnectionOAuthServiceConfigFactory({
162
185
  envService: envService,
163
186
  providerType: DISCORD_USER_EXTERNAL_CONNECTION_PROVIDER_TYPE,
164
187
  successPath: successPath,
165
- failurePath: failurePath
188
+ failurePath: failurePath,
189
+ signInSuccessPath: signInSuccessPath,
190
+ signInFailurePath: signInFailurePath,
191
+ allowedReturnPaths: allowedReturnPaths
166
192
  });
167
193
  return _object_spread_props(_object_spread({}, baseConfig), {
168
- scopes: scopes !== null && scopes !== void 0 ? scopes : DEFAULT_DISCORD_OAUTH_SCOPES
194
+ scopes: scopes !== null && scopes !== void 0 ? scopes : DEFAULT_DISCORD_OAUTH_SCOPES,
195
+ signInScopes: signInScopes !== null && signInScopes !== void 0 ? signInScopes : DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES
169
196
  });
170
197
  }
171
198
 
@@ -428,30 +455,50 @@ function _type_of$1(obj) {
428
455
  * choosing the redirect — is the framework's.
429
456
  */ var DiscordUserExternalConnectionOAuthService = /*#__PURE__*/ function(AbstractUserExternalConnectionOAuthService) {
430
457
  _inherits$1(DiscordUserExternalConnectionOAuthService, AbstractUserExternalConnectionOAuthService);
431
- function DiscordUserExternalConnectionOAuthService(config, stateCoder, userExternalConnectionActions, userExternalConnectionAccessor, oauthApi) {
458
+ function DiscordUserExternalConnectionOAuthService(config, stateCoder, userExternalConnectionActions, userExternalConnectionAccessor, oauthApi, userExternalConnectionSignInService, userExternalConnectionProviderPolicyRegistry, userExternalConnectionSignInThrottle) {
432
459
  _class_call_check$1(this, DiscordUserExternalConnectionOAuthService);
433
460
  var _this;
434
- _this = _call_super$1(this, DiscordUserExternalConnectionOAuthService), _define_property$1(_this, "config", void 0), _define_property$1(_this, "stateCoder", void 0), _define_property$1(_this, "userExternalConnectionActions", void 0), _define_property$1(_this, "userExternalConnectionAccessor", void 0), _define_property$1(_this, "oauthApi", void 0), _define_property$1(_this, "authorizeUrlFactory", void 0);
461
+ _this = _call_super$1(this, DiscordUserExternalConnectionOAuthService), _define_property$1(_this, "config", void 0), _define_property$1(_this, "stateCoder", void 0), _define_property$1(_this, "userExternalConnectionActions", void 0), _define_property$1(_this, "userExternalConnectionAccessor", void 0), _define_property$1(_this, "oauthApi", void 0), _define_property$1(_this, "userExternalConnectionSignInService", void 0), _define_property$1(_this, "userExternalConnectionProviderPolicyRegistry", void 0), _define_property$1(_this, "userExternalConnectionSignInThrottle", void 0), _define_property$1(_this, "authorizeUrlFactory", void 0), _define_property$1(_this, "signInAuthorizeUrlFactory", void 0);
435
462
  _this.config = config;
436
463
  _this.stateCoder = stateCoder;
437
464
  _this.userExternalConnectionActions = userExternalConnectionActions;
438
465
  _this.userExternalConnectionAccessor = userExternalConnectionAccessor;
439
466
  _this.oauthApi = oauthApi;
440
- var scopes = config.scopes, userExternalConnectionOAuth = config.userExternalConnectionOAuth;
467
+ _this.userExternalConnectionSignInService = userExternalConnectionSignInService;
468
+ _this.userExternalConnectionProviderPolicyRegistry = userExternalConnectionProviderPolicyRegistry;
469
+ _this.userExternalConnectionSignInThrottle = userExternalConnectionSignInThrottle;
470
+ var scopes = config.scopes, signInScopes = config.signInScopes, userExternalConnectionOAuth = config.userExternalConnectionOAuth;
441
471
  // no clientId guard here, unlike the Cal.com equivalent: DiscordOAuthConfig requires both
442
472
  // credentials, so DiscordOAuthApi cannot construct without a client id to authorize as
443
473
  _this.authorizeUrlFactory = oauthApi.authorizeUrlFactory({
444
474
  redirectUri: userExternalConnectionOAuth.redirectUri,
445
475
  scopes: scopes
446
476
  });
477
+ // a second factory rather than a scope argument on the first: the url factory closes over its
478
+ // scopes, and the two sets are genuinely different grants rather than one set with a variation
479
+ _this.signInAuthorizeUrlFactory = oauthApi.authorizeUrlFactory({
480
+ redirectUri: userExternalConnectionOAuth.redirectUri,
481
+ scopes: signInScopes
482
+ });
447
483
  return _this;
448
484
  }
449
485
  _create_class(DiscordUserExternalConnectionOAuthService, [
450
486
  {
451
- key: "authorizeUrlForState",
452
- value: function authorizeUrlForState(state) {
453
- return this.authorizeUrlFactory({
454
- state: state
487
+ /**
488
+ * Picks the scope set from the handoff's direction.
489
+ *
490
+ * A `connect` is a DATA grant and gets `scopes`; a `signin` or a `link` is an IDENTITY grant and gets
491
+ * `signInScopes`. The service cannot read the mode off the state — it is an encrypted envelope only
492
+ * the state coder can open — so the framework decodes it and passes it in.
493
+ *
494
+ * @param input - The state, the optional PKCE challenge, and the mode.
495
+ * @returns Discord's authorize URL.
496
+ */ key: "authorizeUrlForState",
497
+ value: function authorizeUrlForState(input) {
498
+ var factory = input.mode === 'signin' || input.mode === 'link' ? this.signInAuthorizeUrlFactory : this.authorizeUrlFactory;
499
+ return factory({
500
+ state: input.state,
501
+ codeChallenge: input.codeChallenge
455
502
  });
456
503
  }
457
504
  },
@@ -467,7 +514,8 @@ function _type_of$1(obj) {
467
514
  4,
468
515
  this.oauthApi.exchangeAuthorizationCodeToAccessToken({
469
516
  code: input.code,
470
- redirectUri: input.redirectUri
517
+ redirectUri: input.redirectUri,
518
+ codeVerifier: input.codeVerifier
471
519
  })
472
520
  ];
473
521
  case 1:
@@ -512,6 +560,47 @@ function _type_of$1(obj) {
512
560
  }).call(this);
513
561
  }
514
562
  },
563
+ {
564
+ key: "signInIdentityForCredentials",
565
+ value: /**
566
+ * Reads the Discord account a sign-in is attributed to.
567
+ *
568
+ * MANDATORY, unlike the best-effort read in the exchange above: a sign-in with no snowflake has
569
+ * nothing to key the account on. `email`/`verified` come from the same call and are only populated
570
+ * when the `email` scope was granted — an app whose sign-in delegate provisions users by email must
571
+ * request it.
572
+ *
573
+ * @param input - The credentials the exchange produced.
574
+ * @returns The Discord identity to sign in as.
575
+ */ function signInIdentityForCredentials(input) {
576
+ return _async_to_generator(function() {
577
+ var _currentUser_verified, _currentUser_global_name, currentUser;
578
+ return _ts_generator(this, function(_state) {
579
+ switch(_state.label){
580
+ case 0:
581
+ return [
582
+ 4,
583
+ this.oauthApi.readCurrentUser({
584
+ accessToken: input.credentials.accessToken
585
+ })
586
+ ];
587
+ case 1:
588
+ currentUser = _state.sent();
589
+ return [
590
+ 2,
591
+ {
592
+ // the snowflake, never the username — Discord usernames became mutable in 2023
593
+ externalAccountId: currentUser.id,
594
+ email: currentUser.email,
595
+ emailVerified: (_currentUser_verified = currentUser.verified) !== null && _currentUser_verified !== void 0 ? _currentUser_verified : false,
596
+ label: (_currentUser_global_name = currentUser.global_name) !== null && _currentUser_global_name !== void 0 ? _currentUser_global_name : currentUser.username
597
+ }
598
+ ];
599
+ }
600
+ });
601
+ }).call(this);
602
+ }
603
+ },
515
604
  {
516
605
  key: "refreshCredentials",
517
606
  value: function refreshCredentials(input) {
@@ -545,6 +634,62 @@ function _type_of$1(obj) {
545
634
  });
546
635
  }).call(this);
547
636
  }
637
+ },
638
+ {
639
+ key: "revokeCredentials",
640
+ value: /**
641
+ * Revokes the Discord grant so a captured token cannot outlive the disconnect.
642
+ *
643
+ * The refresh token is revoked when there is one: Discord invalidates the whole grant, so revoking
644
+ * the longer-lived credential covers the access token issued from it. A failure is logged rather
645
+ * than thrown — the disconnect has already decided to forget these credentials, and failing it
646
+ * would leave the user connected to an account they asked to leave.
647
+ *
648
+ * @param input - The acting user and the credentials being discarded.
649
+ */ function revokeCredentials(input) {
650
+ return _async_to_generator(function() {
651
+ var _input_credentials, refreshToken, accessToken, token, e;
652
+ return _ts_generator(this, function(_state) {
653
+ switch(_state.label){
654
+ case 0:
655
+ _input_credentials = input.credentials, refreshToken = _input_credentials.refreshToken, accessToken = _input_credentials.accessToken;
656
+ token = refreshToken !== null && refreshToken !== void 0 ? refreshToken : accessToken;
657
+ _state.label = 1;
658
+ case 1:
659
+ _state.trys.push([
660
+ 1,
661
+ 3,
662
+ ,
663
+ 4
664
+ ]);
665
+ return [
666
+ 4,
667
+ this.oauthApi.revokeToken({
668
+ token: token,
669
+ tokenTypeHint: refreshToken ? 'refresh_token' : 'access_token'
670
+ })
671
+ ];
672
+ case 2:
673
+ _state.sent();
674
+ return [
675
+ 3,
676
+ 4
677
+ ];
678
+ case 3:
679
+ e = _state.sent();
680
+ this.logger.warn('Disconnected Discord but could not revoke the token at Discord: ', e);
681
+ return [
682
+ 3,
683
+ 4
684
+ ];
685
+ case 4:
686
+ return [
687
+ 2
688
+ ];
689
+ }
690
+ });
691
+ }).call(this);
692
+ }
548
693
  }
549
694
  ]);
550
695
  return DiscordUserExternalConnectionOAuthService;
@@ -555,7 +700,13 @@ DiscordUserExternalConnectionOAuthService = __decorate([
555
700
  __param(1, Inject(UserExternalConnectionStateCoder)),
556
701
  __param(2, Inject(UserExternalConnectionServerActions)),
557
702
  __param(3, Inject(UserExternalConnectionAccessor)),
558
- __param(4, Inject(DiscordOAuthApi))
703
+ __param(4, Inject(DiscordOAuthApi)),
704
+ __param(5, Optional()),
705
+ __param(5, Inject(UserExternalConnectionSignInService)),
706
+ __param(6, Optional()),
707
+ __param(6, Inject(UserExternalConnectionProviderPolicyRegistry)),
708
+ __param(7, Optional()),
709
+ __param(7, Inject(UserExternalConnectionSignInThrottle))
559
710
  ], DiscordUserExternalConnectionOAuthService);
560
711
 
561
712
  function _assert_this_initialized(self) {
@@ -690,7 +841,7 @@ function _unsupported_iterable_to_array(o, minLen) {
690
841
  * @param config - The module metadata configuration.
691
842
  * @returns NestJS ModuleMetadata mounting the Discord connect endpoints.
692
843
  */ function appDiscordUserExternalConnectionOAuthModuleMetadata(config) {
693
- var dependencyModule = config.dependencyModule, successPath = config.successPath, failurePath = config.failurePath, scopes = config.scopes, imports = config.imports, exports = config.exports, providers = config.providers;
844
+ var dependencyModule = config.dependencyModule, successPath = config.successPath, failurePath = config.failurePath, signInSuccessPath = config.signInSuccessPath, signInFailurePath = config.signInFailurePath, allowedReturnPaths = config.allowedReturnPaths, scopes = config.scopes, signInScopes = config.signInScopes, imports = config.imports, exports = config.exports, providers = config.providers;
694
845
  var dependencyModuleImport = dependencyModule ? [
695
846
  dependencyModule
696
847
  ] : [];
@@ -715,7 +866,11 @@ function _unsupported_iterable_to_array(o, minLen) {
715
866
  envService: envService,
716
867
  successPath: successPath,
717
868
  failurePath: failurePath,
718
- scopes: scopes
869
+ signInSuccessPath: signInSuccessPath,
870
+ signInFailurePath: signInFailurePath,
871
+ allowedReturnPaths: allowedReturnPaths,
872
+ scopes: scopes,
873
+ signInScopes: signInScopes
719
874
  });
720
875
  }
721
876
  },
@@ -724,4 +879,4 @@ function _unsupported_iterable_to_array(o, minLen) {
724
879
  };
725
880
  }
726
881
 
727
- export { DEFAULT_DISCORD_OAUTH_SCOPES, DISCORD_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH, DISCORD_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, DiscordUserExternalConnectionOAuthController, DiscordUserExternalConnectionOAuthService, DiscordUserExternalConnectionOAuthServiceConfig, appDiscordUserExternalConnectionOAuthModuleMetadata, discordUserExternalConnectionCredentials, discordUserExternalConnectionOAuthServiceConfigFactory };
882
+ export { DEFAULT_DISCORD_OAUTH_SCOPES, DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES, DISCORD_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH, DISCORD_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, DiscordUserExternalConnectionOAuthController, DiscordUserExternalConnectionOAuthService, DiscordUserExternalConnectionOAuthServiceConfig, appDiscordUserExternalConnectionOAuthModuleMetadata, discordUserExternalConnectionCredentials, discordUserExternalConnectionOAuthServiceConfigFactory };
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/discord",
3
- "version": "14.0.1",
3
+ "version": "14.2.0",
4
+ "sideEffects": false,
4
5
  "type": "module",
5
6
  "peerDependencies": {
6
- "@dereekb/analytics": "14.0.1",
7
- "@dereekb/date": "14.0.1",
8
- "@dereekb/discord": "14.0.1",
9
- "@dereekb/firebase": "14.0.1",
10
- "@dereekb/firebase-server": "14.0.1",
11
- "@dereekb/model": "14.0.1",
12
- "@dereekb/nestjs": "14.0.1",
13
- "@dereekb/rxjs": "14.0.1",
14
- "@dereekb/util": "14.0.1",
7
+ "@dereekb/analytics": "14.2.0",
8
+ "@dereekb/date": "14.2.0",
9
+ "@dereekb/discord": "14.2.0",
10
+ "@dereekb/firebase": "14.2.0",
11
+ "@dereekb/firebase-server": "14.2.0",
12
+ "@dereekb/model": "14.2.0",
13
+ "@dereekb/nestjs": "14.2.0",
14
+ "@dereekb/rxjs": "14.2.0",
15
+ "@dereekb/util": "14.2.0",
15
16
  "@nestjs/common": "^12.0.1",
16
17
  "express": "^5.2.1"
17
18
  },
@@ -27,8 +27,25 @@ export declare const DISCORD_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_RO
27
27
  * — a token granted no scopes can do nothing — and it is what lets the connection be labeled with the
28
28
  * account it belongs to. Excludes `email`, which is a real privilege escalation the label does not
29
29
  * need, and `guilds` / `connections` / `bot`, which nothing here reads.
30
+ *
31
+ * An app using Discord to SIGN IN normally wants `['identify', 'email']`: without `email` the
32
+ * identity carries none, which both fails the auto-create delegate's default email requirement and
33
+ * silently skips the sign-in service's existing-account collision check.
30
34
  */
31
35
  export declare const DEFAULT_DISCORD_OAUTH_SCOPES: readonly DiscordOAuthScope[];
36
+ /**
37
+ * The scopes requested for an IDENTITY round trip — a sign-in, or linking Discord as a login method.
38
+ *
39
+ * A separate default from {@link DEFAULT_DISCORD_OAUTH_SCOPES} because the two answer different
40
+ * questions. The data default is least privilege for whatever the integration reads; this one is what
41
+ * it takes to know WHO signed in, which needs `email` — without it the identity carries none, which
42
+ * both fails the shipped auto-create delegate's email requirement and silently skips the sign-in
43
+ * service's existing-account collision check.
44
+ *
45
+ * Keeping them separate is what lets an app request a broad data grant without every login also
46
+ * demanding it, and vice versa.
47
+ */
48
+ export declare const DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES: readonly DiscordOAuthScope[];
32
49
  /**
33
50
  * Configuration for the {@link DiscordUserExternalConnectionOAuthService}.
34
51
  *
@@ -38,7 +55,14 @@ export declare const DEFAULT_DISCORD_OAUTH_SCOPES: readonly DiscordOAuthScope[];
38
55
  * `@dereekb/discord/nestjs`, and the service reads them through the injected `DiscordOAuthApi`.
39
56
  */
40
57
  export declare abstract class DiscordUserExternalConnectionOAuthServiceConfig extends UserExternalConnectionOAuthServiceConfig {
58
+ /**
59
+ * The scopes requested for a DATA connect.
60
+ */
41
61
  readonly scopes: readonly DiscordOAuthScope[];
62
+ /**
63
+ * The scopes requested for a sign-in or a login-method link.
64
+ */
65
+ readonly signInScopes: readonly DiscordOAuthScope[];
42
66
  }
43
67
  export interface DiscordUserExternalConnectionOAuthServiceConfigFactoryConfig {
44
68
  readonly envService: FirebaseServerEnvService;
@@ -51,9 +75,26 @@ export interface DiscordUserExternalConnectionOAuthServiceConfigFactoryConfig {
51
75
  */
52
76
  readonly failurePath?: Maybe<string>;
53
77
  /**
54
- * The scopes to request. Defaults to {@link DEFAULT_DISCORD_OAUTH_SCOPES}.
78
+ * Path on the app URL a successful SIGN-IN returns to. Defaults to `successPath`.
79
+ */
80
+ readonly signInSuccessPath?: Maybe<string>;
81
+ /**
82
+ * Path on the app URL a FAILED sign-in returns to, e.g. `/auth/login`. Defaults to `failurePath`.
83
+ */
84
+ readonly signInFailurePath?: Maybe<string>;
85
+ /**
86
+ * App paths a sign-in request may ask to return to instead of `signInSuccessPath`.
87
+ */
88
+ readonly allowedReturnPaths?: Maybe<readonly string[]>;
89
+ /**
90
+ * The scopes to request for a DATA connect. Defaults to {@link DEFAULT_DISCORD_OAUTH_SCOPES}.
55
91
  */
56
92
  readonly scopes?: Maybe<readonly DiscordOAuthScope[]>;
93
+ /**
94
+ * The scopes to request for a SIGN-IN or a login-method link. Defaults to
95
+ * {@link DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES}.
96
+ */
97
+ readonly signInScopes?: Maybe<readonly DiscordOAuthScope[]>;
57
98
  }
58
99
  /**
59
100
  * Builds the Discord connect flow's configuration from the app's configured origins.
@@ -63,7 +104,7 @@ export interface DiscordUserExternalConnectionOAuthServiceConfigFactoryConfig {
63
104
  * code-declared paths. The client credentials are read by `discordOAuthServiceConfigFactory` in
64
105
  * `@dereekb/discord/nestjs`, so registering Discord adds no deployment configuration here.
65
106
  *
66
- * @param config - The env service, the return paths, and the optional scope override.
107
+ * @param config - The env service, the return paths, and the optional scope overrides.
67
108
  * @returns The validated service configuration.
68
109
  */
69
110
  export declare function discordUserExternalConnectionOAuthServiceConfigFactory(config: DiscordUserExternalConnectionOAuthServiceConfigFactoryConfig): DiscordUserExternalConnectionOAuthServiceConfig;
@@ -18,9 +18,26 @@ export interface ProvideAppDiscordUserExternalConnectionOAuthMetadataConfig exte
18
18
  */
19
19
  readonly failurePath?: Maybe<string>;
20
20
  /**
21
- * The scopes to request. Defaults to `DEFAULT_DISCORD_OAUTH_SCOPES`.
21
+ * Path on the app URL a successful SIGN-IN returns to. Defaults to `successPath`.
22
+ */
23
+ readonly signInSuccessPath?: Maybe<string>;
24
+ /**
25
+ * Path on the app URL a FAILED sign-in returns to, e.g. `/auth/login`. Defaults to `failurePath`.
26
+ */
27
+ readonly signInFailurePath?: Maybe<string>;
28
+ /**
29
+ * App paths a sign-in request may ask to return to instead of `signInSuccessPath`.
30
+ */
31
+ readonly allowedReturnPaths?: Maybe<readonly string[]>;
32
+ /**
33
+ * The scopes to request for a DATA connect. Defaults to `DEFAULT_DISCORD_OAUTH_SCOPES`.
22
34
  */
23
35
  readonly scopes?: Maybe<readonly DiscordOAuthScope[]>;
36
+ /**
37
+ * The scopes to request for a SIGN-IN or a login-method link. Defaults to
38
+ * `DEFAULT_DISCORD_SIGN_IN_OAUTH_SCOPES`.
39
+ */
40
+ readonly signInScopes?: Maybe<readonly DiscordOAuthScope[]>;
24
41
  }
25
42
  /**
26
43
  * Convenience function used to generate ModuleMetadata for an app's Discord external-connection
@@ -1,6 +1,6 @@
1
1
  import { type DiscordAccessToken, type DiscordOAuthAuthorizeUrlFactory, type DiscordOAuthCurrentUser } from '@dereekb/discord';
2
2
  import { DiscordOAuthApi } from '@dereekb/discord/nestjs';
3
- import { AbstractUserExternalConnectionOAuthService, UserExternalConnectionAccessor, UserExternalConnectionServerActions, UserExternalConnectionStateCoder, type UserExternalConnectionCredentials, type UserExternalConnectionOAuthExchangeInput, type UserExternalConnectionOAuthRefreshCredentialsInput, type UserExternalConnectionOAuthState } from '@dereekb/firebase-server/model';
3
+ import { AbstractUserExternalConnectionOAuthService, UserExternalConnectionAccessor, UserExternalConnectionProviderPolicyRegistry, UserExternalConnectionServerActions, UserExternalConnectionSignInService, UserExternalConnectionSignInThrottle, UserExternalConnectionStateCoder, type UserExternalConnectionCredentials, type UserExternalConnectionOAuthAuthorizeUrlInput, type UserExternalConnectionOAuthExchangeInput, type UserExternalConnectionOAuthRefreshCredentialsInput, type UserExternalConnectionOAuthRevokeCredentialsInput, type UserExternalConnectionOAuthSignInIdentityInput, type UserExternalConnectionSignInIdentity } from '@dereekb/firebase-server/model';
4
4
  import { type Maybe, type WebsiteUrl } from '@dereekb/util';
5
5
  import { DiscordUserExternalConnectionOAuthServiceConfig } from './discord.oauth.connection.config';
6
6
  export interface DiscordUserExternalConnectionCredentialsInput {
@@ -35,9 +35,46 @@ export declare class DiscordUserExternalConnectionOAuthService extends AbstractU
35
35
  readonly userExternalConnectionActions: UserExternalConnectionServerActions;
36
36
  readonly userExternalConnectionAccessor: UserExternalConnectionAccessor;
37
37
  readonly oauthApi: DiscordOAuthApi;
38
+ readonly userExternalConnectionSignInService?: Maybe<UserExternalConnectionSignInService>;
39
+ readonly userExternalConnectionProviderPolicyRegistry?: Maybe<UserExternalConnectionProviderPolicyRegistry>;
40
+ readonly userExternalConnectionSignInThrottle?: Maybe<UserExternalConnectionSignInThrottle>;
38
41
  readonly authorizeUrlFactory: DiscordOAuthAuthorizeUrlFactory;
39
- constructor(config: DiscordUserExternalConnectionOAuthServiceConfig, stateCoder: UserExternalConnectionStateCoder, userExternalConnectionActions: UserExternalConnectionServerActions, userExternalConnectionAccessor: UserExternalConnectionAccessor, oauthApi: DiscordOAuthApi);
40
- protected authorizeUrlForState(state: UserExternalConnectionOAuthState): WebsiteUrl;
42
+ readonly signInAuthorizeUrlFactory: DiscordOAuthAuthorizeUrlFactory;
43
+ constructor(config: DiscordUserExternalConnectionOAuthServiceConfig, stateCoder: UserExternalConnectionStateCoder, userExternalConnectionActions: UserExternalConnectionServerActions, userExternalConnectionAccessor: UserExternalConnectionAccessor, oauthApi: DiscordOAuthApi, userExternalConnectionSignInService?: Maybe<UserExternalConnectionSignInService>, userExternalConnectionProviderPolicyRegistry?: Maybe<UserExternalConnectionProviderPolicyRegistry>, userExternalConnectionSignInThrottle?: Maybe<UserExternalConnectionSignInThrottle>);
44
+ /**
45
+ * Picks the scope set from the handoff's direction.
46
+ *
47
+ * A `connect` is a DATA grant and gets `scopes`; a `signin` or a `link` is an IDENTITY grant and gets
48
+ * `signInScopes`. The service cannot read the mode off the state — it is an encrypted envelope only
49
+ * the state coder can open — so the framework decodes it and passes it in.
50
+ *
51
+ * @param input - The state, the optional PKCE challenge, and the mode.
52
+ * @returns Discord's authorize URL.
53
+ */
54
+ protected authorizeUrlForState(input: UserExternalConnectionOAuthAuthorizeUrlInput): WebsiteUrl;
41
55
  protected credentialsForAuthorizationCode(input: UserExternalConnectionOAuthExchangeInput): Promise<UserExternalConnectionCredentials>;
56
+ /**
57
+ * Reads the Discord account a sign-in is attributed to.
58
+ *
59
+ * MANDATORY, unlike the best-effort read in the exchange above: a sign-in with no snowflake has
60
+ * nothing to key the account on. `email`/`verified` come from the same call and are only populated
61
+ * when the `email` scope was granted — an app whose sign-in delegate provisions users by email must
62
+ * request it.
63
+ *
64
+ * @param input - The credentials the exchange produced.
65
+ * @returns The Discord identity to sign in as.
66
+ */
67
+ protected signInIdentityForCredentials(input: UserExternalConnectionOAuthSignInIdentityInput): Promise<UserExternalConnectionSignInIdentity>;
42
68
  refreshCredentials(input: UserExternalConnectionOAuthRefreshCredentialsInput): Promise<UserExternalConnectionCredentials>;
69
+ /**
70
+ * Revokes the Discord grant so a captured token cannot outlive the disconnect.
71
+ *
72
+ * The refresh token is revoked when there is one: Discord invalidates the whole grant, so revoking
73
+ * the longer-lived credential covers the access token issued from it. A failure is logged rather
74
+ * than thrown — the disconnect has already decided to forget these credentials, and failing it
75
+ * would leave the user connected to an account they asked to leave.
76
+ *
77
+ * @param input - The acting user and the credentials being discarded.
78
+ */
79
+ revokeCredentials(input: UserExternalConnectionOAuthRevokeCredentialsInput): Promise<void>;
43
80
  }
package/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { timingSafeEqual } from 'node:crypto';
2
2
  import { DBX_FIREBASE_SERVER_NO_AUTH_ERROR_CODE, UNAUTHENTICATED_ERROR_CODE, FORBIDDEN_ERROR_CODE, PERMISSION_DENIED_ERROR_CODE, NOT_FOUND_ERROR_CODE, MODEL_NOT_AVAILABLE_ERROR_CODE, BAD_REQUEST_ERROR_CODE, CONFLICT_ERROR_CODE, ALREADY_EXISTS_ERROR_CODE, UNAVAILABLE_ERROR_CODE, UNAVAILABLE_OR_DEACTIVATED_FUNCTION_ERROR_CODE, INTERNAL_SERVER_ERROR_CODE, FIREBASE_AUTH_USER_NOT_FOUND_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_RESET_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_LAST_COM_DATE_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_EXPIRES_AT_KEY, FIREBASE_AUTH_PHONE_NUMBER_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_EMAIL_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_INVALID_PHONE_NUMBER_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_LAST_COM_DATE_KEY, setIdAndKeyFromKeyIdRefOnDocumentData, DBX_FIREBASE_SERVER_PASSWORD_RESET_INVALID_CODE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_NO_CONFIG_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_SEND_ONCE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_THROTTLE_ERROR_CODE, FirestoreDocumentContextType, streamFromOnSnapshot, FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FIRESTORE_LIMIT_TO_LAST_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_OFFSET_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AFTER_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_BEFORE_QUERY_CONSTRAINT_TYPE, makeFirestoreQueryConstraintFunctionsDriver, firestoreContextFactory, firestoreField, optionalFirestoreField, DEFAULT_MODEL_FUNCTION_FIREBASE_CRUD_FUNCTION_SPECIFIER, UNKNOWN_MODEL_TYPE_ERROR_CODE, BAD_DOCUMENT_QUERY_CURSOR_ERROR_CODE, limit, startAfter, DEFAULT_ON_CALL_QUERY_MODEL_LIMIT, MAX_ON_CALL_QUERY_MODEL_LIMIT, oidcScopeTermSatisfied, oidcScopesFromScopeClaim, resolveEffectiveOidcScopeTerms, callModelOidcScopeForCallType, oidcScopeTermsSatisfied, CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE, FIRESTORE_SESSION_OIDC_SCOPE, ScheduledFunctionDevelopmentFunctionTypeEnum, SCHEDULED_FUNCTION_DEV_FUNCTION_SPECIFIER, storageListFilesResultFactory, assertStorageUploadOptionsStringFormat, firebaseStorageContextFactory, inContextFirebaseModelsServiceFactory, useFirebaseModelsService } from '@dereekb/firebase';
3
- import { partialServerError, isServerError, randomNumberFactory, cachedGetter, filterNullAndUndefinedValues, asSet, AUTH_ADMIN_ROLE, AUTH_TOS_SIGNED_ROLE, forEachKeyValue, KeyValueTypleValueFilter, filterUndefinedValues, isThrottled, mapObjectMap, batch, objectToMap, serverError, asArray, containsAllValues, websiteUrlDetails, mergeObjects, cronExpressionRepeatingEveryNMinutes, mapIdentityFunction, slashPathName, toRelativeSlashPathStartType, fixMultiSlashesInSlashPath, SLASH_PATH_SEPARATOR, objectHasNoKeys, websiteUrlFromPaths, pushItemOrArrayItemsIntoArray, makeGetter, asGetter, build, performAsyncTasks } from '@dereekb/util';
3
+ import { partialServerError, isServerError, randomNumberFactory, cachedGetter, filterNullAndUndefinedValues, asSet, filterUndefinedValues, AUTH_ADMIN_ROLE, AUTH_TOS_SIGNED_ROLE, forEachKeyValue, KeyValueTypleValueFilter, isThrottled, mapObjectMap, batch, objectToMap, serverError, asArray, containsAllValues, websiteUrlDetails, mergeObjects, cronExpressionRepeatingEveryNMinutes, mapIdentityFunction, slashPathName, toRelativeSlashPathStartType, fixMultiSlashesInSlashPath, SLASH_PATH_SEPARATOR, objectHasNoKeys, websiteUrlFromPaths, pushItemOrArrayItemsIntoArray, makeGetter, asGetter, build, performAsyncTasks } from '@dereekb/util';
4
4
  import { HttpsError } from 'firebase-functions/https';
5
5
  import { hoursToMs, minutesToMs, toISODateString } from '@dereekb/date';
6
6
  import { BaseError } from 'make-error';
@@ -1534,6 +1534,14 @@ function _unsupported_iterable_to_array$h(o, minLen) {
1534
1534
  _this._loadRecord.reset(); // reset the cache
1535
1535
  });
1536
1536
  }
1537
+ },
1538
+ {
1539
+ key: "mintCustomToken",
1540
+ value: function mintCustomToken(claims) {
1541
+ // `createCustomToken` rejects an explicit undefined for the claims argument on some Admin SDK
1542
+ // versions, so the no-claims call is made without the second argument at all
1543
+ return claims == null ? this.service.auth.createCustomToken(this.uid) : this.service.auth.createCustomToken(this.uid, filterUndefinedValues(claims));
1544
+ }
1537
1545
  }
1538
1546
  ]);
1539
1547
  return AbstractFirebaseServerAuthUserContext;
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/mailgun",
3
- "version": "14.0.1",
3
+ "version": "14.2.0",
4
+ "sideEffects": false,
4
5
  "type": "module",
5
6
  "peerDependencies": {
6
- "@dereekb/analytics": "14.0.1",
7
- "@dereekb/firebase": "14.0.1",
8
- "@dereekb/firebase-server": "14.0.1",
9
- "@dereekb/date": "14.0.1",
10
- "@dereekb/nestjs": "14.0.1",
11
- "@dereekb/model": "14.0.1",
12
- "@dereekb/rxjs": "14.0.1",
13
- "@dereekb/util": "14.0.1"
7
+ "@dereekb/analytics": "14.2.0",
8
+ "@dereekb/firebase": "14.2.0",
9
+ "@dereekb/firebase-server": "14.2.0",
10
+ "@dereekb/date": "14.2.0",
11
+ "@dereekb/nestjs": "14.2.0",
12
+ "@dereekb/model": "14.2.0",
13
+ "@dereekb/rxjs": "14.2.0",
14
+ "@dereekb/util": "14.2.0"
14
15
  },
15
16
  "exports": {
16
17
  "./package.json": "./package.json",