@dereekb/firebase-server 13.31.0 → 13.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/calcom/index.cjs.default.js +1 -0
- package/calcom/index.cjs.js +1451 -0
- package/calcom/index.cjs.mjs +2 -0
- package/calcom/index.d.ts +1 -0
- package/calcom/index.esm.js +1437 -0
- package/calcom/package.json +29 -0
- package/calcom/src/index.d.ts +1 -0
- package/calcom/src/lib/calcom.oauth.connection.cache.d.ts +49 -0
- package/calcom/src/lib/calcom.oauth.connection.config.d.ts +75 -0
- package/calcom/src/lib/calcom.oauth.connection.context.d.ts +84 -0
- package/calcom/src/lib/calcom.oauth.connection.controller.d.ts +18 -0
- package/calcom/src/lib/calcom.oauth.connection.module.d.ts +55 -0
- package/calcom/src/lib/calcom.oauth.connection.service.d.ts +33 -0
- package/calcom/src/lib/index.d.ts +6 -0
- package/discord/index.cjs.default.js +1 -0
- package/discord/index.cjs.js +761 -0
- package/discord/index.cjs.mjs +2 -0
- package/discord/index.d.ts +1 -0
- package/discord/index.esm.js +753 -0
- package/discord/package.json +29 -0
- package/discord/src/index.d.ts +1 -0
- package/discord/src/lib/discord.oauth.connection.config.d.ts +69 -0
- package/discord/src/lib/discord.oauth.connection.controller.d.ts +18 -0
- package/discord/src/lib/discord.oauth.connection.module.d.ts +39 -0
- package/discord/src/lib/discord.oauth.connection.service.d.ts +43 -0
- package/discord/src/lib/index.d.ts +4 -0
- package/index.cjs.js +14 -3
- package/index.esm.js +14 -3
- package/mailgun/package.json +9 -9
- package/mcp/package.json +11 -11
- package/model/index.cjs.js +11077 -6072
- package/model/index.esm.js +11020 -6081
- package/model/package.json +10 -9
- package/model/src/lib/index.d.ts +1 -0
- package/model/src/lib/mailgun/index.d.ts +1 -0
- package/model/src/lib/mailgun/notification.healthcheck.mailgun.d.ts +115 -0
- package/model/src/lib/notification/index.d.ts +2 -0
- package/model/src/lib/notification/notification.action.server.d.ts +42 -3
- package/model/src/lib/notification/notification.error.d.ts +31 -0
- package/model/src/lib/notification/notification.healthcheck.d.ts +39 -0
- package/model/src/lib/notification/notification.healthcheck.service.d.ts +118 -0
- package/model/src/lib/notification/notification.send.service.d.ts +22 -0
- package/model/src/lib/userexternalconnection/index.d.ts +8 -0
- package/model/src/lib/userexternalconnection/oauth/index.d.ts +7 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.config.d.ts +110 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.controller.d.ts +68 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.error.d.ts +29 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.refresh.d.ts +24 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.registry.d.ts +54 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.service.d.ts +216 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.state.d.ts +114 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.accessor.service.d.ts +111 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.action.server.d.ts +172 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.error.d.ts +44 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.module.d.ts +129 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.private.d.ts +192 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.reader.service.d.ts +157 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.refresh.service.d.ts +46 -0
- package/oidc/index.cjs.js +104 -78
- package/oidc/index.esm.js +104 -78
- package/oidc/package.json +10 -10
- package/package.json +24 -10
- package/src/lib/env/env.config.d.ts +15 -0
- package/src/lib/env/env.service.d.ts +7 -0
- package/src/lib/nest/env/env.service.d.ts +1 -0
- package/test/index.cjs.js +14 -2
- package/test/index.esm.js +15 -3
- package/test/package.json +11 -11
- package/test/src/lib/firebase/firebase.admin.auth.d.ts +1 -1
- package/twilio/package.json +8 -8
- package/zoho/README.md +8 -0
- package/zoho/index.cjs.js +1223 -33
- package/zoho/index.esm.js +1212 -36
- package/zoho/package.json +12 -9
- package/zoho/src/lib/index.d.ts +5 -0
- package/zoho/src/lib/zoho.oauth.connection.cache.d.ts +45 -0
- package/zoho/src/lib/zoho.oauth.connection.config.d.ts +80 -0
- package/zoho/src/lib/zoho.oauth.connection.controller.d.ts +18 -0
- package/zoho/src/lib/zoho.oauth.connection.module.d.ts +43 -0
- package/zoho/src/lib/zoho.oauth.connection.service.d.ts +107 -0
package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.controller.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Request, type Response } from 'express';
|
|
2
|
+
import { type Maybe } from '@dereekb/util';
|
|
3
|
+
import { type AbstractUserExternalConnectionOAuthService, type UserExternalConnectionOAuthCallbackQueryValues, type UserExternalConnectionOAuthState } from './userexternalconnection.oauth.service';
|
|
4
|
+
/**
|
|
5
|
+
* HTTP status used for the handoff redirects.
|
|
6
|
+
*
|
|
7
|
+
* A 302 keeps the redirect non-cacheable, which matters because each authorize URL carries a
|
|
8
|
+
* single-use `state`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const USER_EXTERNAL_CONNECTION_OAUTH_REDIRECT_STATUS = 302;
|
|
11
|
+
/**
|
|
12
|
+
* Query parameters a provider sends to the redirect URI.
|
|
13
|
+
*
|
|
14
|
+
* Snake-cased because these are the wire names — `error_description` is what RFC 6749 4.1.2.1
|
|
15
|
+
* specifies, so it is read as-is rather than renamed at the boundary.
|
|
16
|
+
*/
|
|
17
|
+
export interface UserExternalConnectionOAuthCallbackQuery extends UserExternalConnectionOAuthCallbackQueryValues {
|
|
18
|
+
readonly code?: Maybe<string>;
|
|
19
|
+
readonly state?: Maybe<UserExternalConnectionOAuthState>;
|
|
20
|
+
readonly error?: Maybe<string>;
|
|
21
|
+
readonly error_description?: Maybe<string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The two endpoints of an external-connection authorization-code handoff.
|
|
25
|
+
*
|
|
26
|
+
* A provider ships its own controller so it keeps full control of its route surface; extending this
|
|
27
|
+
* means it declares only the mount point and its constructor:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* @Controller(CALCOM_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH)
|
|
31
|
+
* export class CalcomUserExternalConnectionOAuthController extends AbstractUserExternalConnectionOAuthController {
|
|
32
|
+
* constructor(@Inject(CalcomUserExternalConnectionOAuthService) readonly oauthService: CalcomUserExternalConnectionOAuthService) {
|
|
33
|
+
* super();
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Mount at {@link userExternalConnectionOAuthControllerPath}, and exclude those routes from any
|
|
39
|
+
* global API route prefix with {@link userExternalConnectionOAuthRoutesForGlobalRouteExclude} — the
|
|
40
|
+
* redirect URI registered with a provider must match byte-for-byte, so a prefix silently breaks it.
|
|
41
|
+
*/
|
|
42
|
+
export declare abstract class AbstractUserExternalConnectionOAuthController {
|
|
43
|
+
abstract readonly oauthService: AbstractUserExternalConnectionOAuthService;
|
|
44
|
+
/**
|
|
45
|
+
* Begins the handoff by redirecting the user's browser to the provider's consent screen.
|
|
46
|
+
*
|
|
47
|
+
* Carries the `state` resolved for the request; a request without one is bounced to the failure
|
|
48
|
+
* URL rather than sent to the provider.
|
|
49
|
+
*
|
|
50
|
+
* @param request - The incoming authorize request, which the state is read from.
|
|
51
|
+
* @param response - The response to issue the redirect on.
|
|
52
|
+
*/
|
|
53
|
+
authorize(request: Request, response: Response): void;
|
|
54
|
+
/**
|
|
55
|
+
* Completes the handoff: verifies the returned `state`, exchanges the authorization code, and
|
|
56
|
+
* redirects to the configured success or failure URL.
|
|
57
|
+
*
|
|
58
|
+
* On refusal a provider sends `error` / `error_description` in place of a `code` (RFC 6749
|
|
59
|
+
* 4.1.2.1), so both are read and passed through — otherwise a rejected scope or a denied consent
|
|
60
|
+
* is indistinguishable from a missing code.
|
|
61
|
+
*
|
|
62
|
+
* @param query - The callback query parameters: `code` + `state` on approval, or `error` +
|
|
63
|
+
* `error_description` on refusal. Passed through whole, so a provider adapter can read the
|
|
64
|
+
* extras its exchange needs.
|
|
65
|
+
* @param response - The response to issue the redirect on.
|
|
66
|
+
*/
|
|
67
|
+
callback(query: UserExternalConnectionOAuthCallbackQuery, response: Response): Promise<void>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type UserExternalConnectionErrorCode } from '@dereekb/firebase';
|
|
2
|
+
import { type Maybe } from '@dereekb/util';
|
|
3
|
+
/**
|
|
4
|
+
* An error a provider reported on the redirect back, rather than one thrown on our side.
|
|
5
|
+
*
|
|
6
|
+
* Per RFC 6749 4.1.2.1 the authorization server redirects to the `redirect_uri` with these instead
|
|
7
|
+
* of a `code` when it refuses the request. Reading them is what keeps a declined consent or a
|
|
8
|
+
* rejected scope from being misreported as a missing authorization code.
|
|
9
|
+
*/
|
|
10
|
+
export interface UserExternalConnectionOAuthProviderError {
|
|
11
|
+
/**
|
|
12
|
+
* The OAuth error code, e.g. `invalid_request`, `invalid_scope`, `access_denied`.
|
|
13
|
+
*/
|
|
14
|
+
readonly error: string;
|
|
15
|
+
/**
|
|
16
|
+
* The provider's human-readable explanation, when it sent one.
|
|
17
|
+
*/
|
|
18
|
+
readonly errorDescription?: Maybe<string>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Maps an OAuth refusal reported by a provider to the connection entry's error code.
|
|
22
|
+
*
|
|
23
|
+
* The codes are the standard OAuth 2.0 ones, so this mapping is the same for every provider. A
|
|
24
|
+
* failure thrown on our own side has no provider error and stays `provider_error`.
|
|
25
|
+
*
|
|
26
|
+
* @param providerError - The error the provider reported on the redirect, when it reported one.
|
|
27
|
+
* @returns The error code to record on the connection entry.
|
|
28
|
+
*/
|
|
29
|
+
export declare function userExternalConnectionErrorCodeForOAuthProviderError(providerError: Maybe<UserExternalConnectionOAuthProviderError>): UserExternalConnectionErrorCode;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type UserExternalConnectionCredentialsRefresher } from '../userexternalconnection.refresh.service';
|
|
2
|
+
import { type UserExternalConnectionOAuthProviderRegistry } from './userexternalconnection.oauth.registry';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for {@link userExternalConnectionOAuthRegistryCredentialsRefresher}.
|
|
5
|
+
*/
|
|
6
|
+
export interface UserExternalConnectionOAuthRegistryCredentialsRefresherConfig {
|
|
7
|
+
readonly registry: UserExternalConnectionOAuthProviderRegistry;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a {@link UserExternalConnectionCredentialsRefresher} that renews credentials through the
|
|
11
|
+
* app's registered OAuth provider services.
|
|
12
|
+
*
|
|
13
|
+
* The bridge between the two layers: the reader knows it needs a refresh but nothing about providers,
|
|
14
|
+
* and each provider service knows how to refresh but nothing about who is asking. Dispatch is by
|
|
15
|
+
* provider type, so a provider the app never registered — or registered without a `refreshCredentials`
|
|
16
|
+
* implementation — resolves to null, which the reader reports as "cannot renew" rather than as a
|
|
17
|
+
* failure of the provider.
|
|
18
|
+
*
|
|
19
|
+
* @param config - The provider registry to dispatch through.
|
|
20
|
+
* @returns A refresher backed by the registry.
|
|
21
|
+
*
|
|
22
|
+
* @__NO_SIDE_EFFECTS__
|
|
23
|
+
*/
|
|
24
|
+
export declare function userExternalConnectionOAuthRegistryCredentialsRefresher(config: UserExternalConnectionOAuthRegistryCredentialsRefresherConfig): UserExternalConnectionCredentialsRefresher;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type InjectionToken, type Provider } from '@nestjs/common';
|
|
2
|
+
import { type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
3
|
+
import { type Maybe } from '@dereekb/util';
|
|
4
|
+
import { type AbstractUserExternalConnectionOAuthService } from './userexternalconnection.oauth.service';
|
|
5
|
+
/**
|
|
6
|
+
* The providers an app has an OAuth authorize/callback flow mounted for.
|
|
7
|
+
*
|
|
8
|
+
* Built FROM the registered services rather than from a hand-maintained list, so it cannot disagree
|
|
9
|
+
* with which provider modules the app actually imported. A provider absent from it has no endpoint
|
|
10
|
+
* to send the user to, so minting a state for it would hand back something unusable.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class UserExternalConnectionOAuthProviderRegistry {
|
|
13
|
+
abstract readonly providerTypes: ReadonlySet<UserExternalConnectionProviderType>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the app can begin an OAuth handoff for this provider.
|
|
16
|
+
*/
|
|
17
|
+
abstract hasAuthorizeFlowForProviderType(providerType: UserExternalConnectionProviderType): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Throws when the app has no OAuth handoff for this provider.
|
|
20
|
+
*
|
|
21
|
+
* @throws A precondition-conflict HttpsError.
|
|
22
|
+
*/
|
|
23
|
+
abstract assertHasAuthorizeFlowForProviderType(providerType: UserExternalConnectionProviderType): void;
|
|
24
|
+
/**
|
|
25
|
+
* The registered service for a provider, when there is one.
|
|
26
|
+
*/
|
|
27
|
+
abstract serviceForProviderType(providerType: UserExternalConnectionProviderType): Maybe<AbstractUserExternalConnectionOAuthService>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates the registry from the app's registered OAuth services.
|
|
31
|
+
*
|
|
32
|
+
* @param services - Every {@link AbstractUserExternalConnectionOAuthService} the app has mounted.
|
|
33
|
+
* @returns The registry.
|
|
34
|
+
* @throws {Error} When two services claim the same provider type.
|
|
35
|
+
*
|
|
36
|
+
* @__NO_SIDE_EFFECTS__
|
|
37
|
+
*/
|
|
38
|
+
export declare function userExternalConnectionOAuthProviderRegistry(services: AbstractUserExternalConnectionOAuthService[]): UserExternalConnectionOAuthProviderRegistry;
|
|
39
|
+
/**
|
|
40
|
+
* Creates the NestJS provider for the {@link UserExternalConnectionOAuthProviderRegistry}.
|
|
41
|
+
*
|
|
42
|
+
* Declared by the app rather than by the UserExternalConnection module, because each provider module
|
|
43
|
+
* imports that module for its actions and state coder — so the registry must live somewhere that can
|
|
44
|
+
* import the provider modules without a cycle.
|
|
45
|
+
*
|
|
46
|
+
* Registering a provider is then one module import plus one token here.
|
|
47
|
+
*
|
|
48
|
+
* @param oauthServiceTokens - Tokens of the registered `AbstractUserExternalConnectionOAuthService`
|
|
49
|
+
* providers, whose modules must be imported by the declaring module.
|
|
50
|
+
* @returns The NestJS provider.
|
|
51
|
+
*
|
|
52
|
+
* @__NO_SIDE_EFFECTS__
|
|
53
|
+
*/
|
|
54
|
+
export declare function userExternalConnectionOAuthProviderRegistryProvider(oauthServiceTokens: InjectionToken[]): Provider;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { type Request } from 'express';
|
|
3
|
+
import { type FirebaseAuthUserId, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
4
|
+
import { type Maybe, type WebsiteUrl } from '@dereekb/util';
|
|
5
|
+
import { type UserExternalConnectionCredentials } from '../userexternalconnection.private';
|
|
6
|
+
import { type UserExternalConnectionAccessor } from '../userexternalconnection.accessor.service';
|
|
7
|
+
import { type UserExternalConnectionServerActions } from '../userexternalconnection.action.server';
|
|
8
|
+
import { type UserExternalConnectionStateCoder } from './userexternalconnection.oauth.state';
|
|
9
|
+
import { type UserExternalConnectionOAuthServiceConfig } from './userexternalconnection.oauth.config';
|
|
10
|
+
/**
|
|
11
|
+
* The `state` value carried through the authorization-code handoff.
|
|
12
|
+
*
|
|
13
|
+
* Opaque to the provider: only {@link UserExternalConnectionStateCoder} can interpret it.
|
|
14
|
+
*/
|
|
15
|
+
export type UserExternalConnectionOAuthState = string;
|
|
16
|
+
/**
|
|
17
|
+
* Identifies who a handoff belongs to, as resolved from a verified `state`.
|
|
18
|
+
*/
|
|
19
|
+
export interface UserExternalConnectionOAuthActor {
|
|
20
|
+
readonly uid: FirebaseAuthUserId;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The raw callback query, as the provider sent it.
|
|
24
|
+
*
|
|
25
|
+
* Kept open rather than narrowed to the four RFC 6749 parameters, because providers send more than
|
|
26
|
+
* those and some of the extras are load-bearing: Zoho's `accounts-server` names the datacenter that
|
|
27
|
+
* issued the code, and an exchange sent to the wrong one fails. Nest's keyless `@Query()` already
|
|
28
|
+
* receives every parameter — only the typed view was dropping them.
|
|
29
|
+
*
|
|
30
|
+
* Declared here rather than beside the controller's typed view so the service does not have to
|
|
31
|
+
* import from the controller that imports it.
|
|
32
|
+
*/
|
|
33
|
+
export type UserExternalConnectionOAuthCallbackQueryValues = Record<string, Maybe<string>>;
|
|
34
|
+
export interface UserExternalConnectionOAuthExchangeInput {
|
|
35
|
+
/**
|
|
36
|
+
* The authorization code the provider issued.
|
|
37
|
+
*/
|
|
38
|
+
readonly code: string;
|
|
39
|
+
/**
|
|
40
|
+
* The redirect URI the code was issued against. Sent on the exchange because providers require it
|
|
41
|
+
* to match the one used on the authorize request.
|
|
42
|
+
*/
|
|
43
|
+
readonly redirectUri: WebsiteUrl;
|
|
44
|
+
/**
|
|
45
|
+
* The raw callback query, for a provider whose exchange needs a parameter beyond `code`.
|
|
46
|
+
*
|
|
47
|
+
* Optional, and most providers ignore it. Anything read from here arrived on a redirect the user's
|
|
48
|
+
* browser followed, so treat it as untrusted input — in particular, never use a value from here as
|
|
49
|
+
* a request target without checking it against an allowlist first.
|
|
50
|
+
*/
|
|
51
|
+
readonly query?: Maybe<UserExternalConnectionOAuthCallbackQueryValues>;
|
|
52
|
+
}
|
|
53
|
+
export interface UserExternalConnectionOAuthHandleCallbackInput {
|
|
54
|
+
/**
|
|
55
|
+
* The authorization code, present when the provider approved the request.
|
|
56
|
+
*/
|
|
57
|
+
readonly code?: Maybe<string>;
|
|
58
|
+
/**
|
|
59
|
+
* The state echoed back, identifying who is connecting.
|
|
60
|
+
*/
|
|
61
|
+
readonly state?: Maybe<UserExternalConnectionOAuthState>;
|
|
62
|
+
/**
|
|
63
|
+
* The OAuth error code, present when the provider refused instead of issuing a code.
|
|
64
|
+
*/
|
|
65
|
+
readonly error?: Maybe<string>;
|
|
66
|
+
/**
|
|
67
|
+
* The provider's explanation of the refusal.
|
|
68
|
+
*/
|
|
69
|
+
readonly errorDescription?: Maybe<string>;
|
|
70
|
+
/**
|
|
71
|
+
* The raw callback query, passed through to the exchange unchanged.
|
|
72
|
+
*/
|
|
73
|
+
readonly query?: Maybe<UserExternalConnectionOAuthCallbackQueryValues>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Input for {@link AbstractUserExternalConnectionOAuthService.credentialsRetainingStoredRefreshToken}.
|
|
77
|
+
*/
|
|
78
|
+
export interface UserExternalConnectionOAuthRetainRefreshTokenInput {
|
|
79
|
+
readonly uid: FirebaseAuthUserId;
|
|
80
|
+
/**
|
|
81
|
+
* The credentials the exchange produced.
|
|
82
|
+
*/
|
|
83
|
+
readonly credentials: UserExternalConnectionCredentials;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Input for {@link AbstractUserExternalConnectionOAuthService.refreshCredentials}.
|
|
87
|
+
*
|
|
88
|
+
* Carries no `providerType` — the service already knows its own, and taking one would create a
|
|
89
|
+
* parameter that could disagree with it.
|
|
90
|
+
*/
|
|
91
|
+
export interface UserExternalConnectionOAuthRefreshCredentialsInput {
|
|
92
|
+
readonly uid: FirebaseAuthUserId;
|
|
93
|
+
/**
|
|
94
|
+
* The credentials currently stored for this provider, carrying the refresh token and any
|
|
95
|
+
* provider-specific `extra` the exchange needs.
|
|
96
|
+
*/
|
|
97
|
+
readonly credentials: UserExternalConnectionCredentials;
|
|
98
|
+
}
|
|
99
|
+
export interface UserExternalConnectionOAuthCallbackResult {
|
|
100
|
+
readonly success: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The URL the user should be redirected to.
|
|
103
|
+
*/
|
|
104
|
+
readonly redirectUrl: WebsiteUrl;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reads the `state` an authorize request should carry to the provider.
|
|
108
|
+
*
|
|
109
|
+
* A top-level browser navigation carries no credentials, so the state must have been minted by a
|
|
110
|
+
* prior authenticated `read:authorizeState` call and arrive here as the `state` query parameter.
|
|
111
|
+
*
|
|
112
|
+
* @param request - The incoming authorize request.
|
|
113
|
+
* @returns The state, when present.
|
|
114
|
+
*/
|
|
115
|
+
export declare function userExternalConnectionOAuthStateForRequest(request: Request): Maybe<UserExternalConnectionOAuthState>;
|
|
116
|
+
/**
|
|
117
|
+
* Drives a provider's authorization-code handoff into a user's UserExternalConnection.
|
|
118
|
+
*
|
|
119
|
+
* Everything except the two abstract members is identical for every OAuth 2.0 provider: resolving
|
|
120
|
+
* who is connecting from the signed `state`, surfacing a provider-side refusal, persisting the
|
|
121
|
+
* credentials, recording the failure code, and choosing the redirect. A provider adapter extends
|
|
122
|
+
* this and supplies only the OAuth mechanics its service actually differs on.
|
|
123
|
+
*
|
|
124
|
+
* Subclasses expose `config`, `stateCoder`, `userExternalConnectionActions`, and
|
|
125
|
+
* `userExternalConnectionAccessor` as injected constructor properties.
|
|
126
|
+
*/
|
|
127
|
+
export declare abstract class AbstractUserExternalConnectionOAuthService {
|
|
128
|
+
abstract readonly config: UserExternalConnectionOAuthServiceConfig;
|
|
129
|
+
abstract readonly stateCoder: UserExternalConnectionStateCoder;
|
|
130
|
+
abstract readonly userExternalConnectionActions: UserExternalConnectionServerActions;
|
|
131
|
+
/**
|
|
132
|
+
* The read half of the pair.
|
|
133
|
+
*
|
|
134
|
+
* Deliberately the accessor rather than `UserExternalConnectionReader`: the reader can refresh, and
|
|
135
|
+
* it finds its refresh path through the registry these services are registered in — so depending on
|
|
136
|
+
* it here would be a cycle. This service needs only the raw read.
|
|
137
|
+
*/
|
|
138
|
+
abstract readonly userExternalConnectionAccessor: UserExternalConnectionAccessor;
|
|
139
|
+
private readonly _logger;
|
|
140
|
+
protected get logger(): Logger;
|
|
141
|
+
get providerType(): UserExternalConnectionProviderType;
|
|
142
|
+
get redirectUri(): WebsiteUrl;
|
|
143
|
+
get successUrl(): WebsiteUrl;
|
|
144
|
+
get failureUrl(): WebsiteUrl;
|
|
145
|
+
/**
|
|
146
|
+
* PROVIDER: builds the provider's consent-screen URL carrying the minted state.
|
|
147
|
+
*
|
|
148
|
+
* @param state - The signed state to echo back on the callback.
|
|
149
|
+
* @returns The authorize URL to redirect the user's browser to.
|
|
150
|
+
*/
|
|
151
|
+
protected abstract authorizeUrlForState(state: UserExternalConnectionOAuthState): WebsiteUrl;
|
|
152
|
+
/**
|
|
153
|
+
* PROVIDER: exchanges the authorization code and maps the token response to credentials.
|
|
154
|
+
*
|
|
155
|
+
* Both halves are the provider's own: token endpoints differ in body encoding and client
|
|
156
|
+
* authentication, and only the provider knows how its response maps onto
|
|
157
|
+
* {@link UserExternalConnectionCredentials}. When a provider rotates its refresh token, the
|
|
158
|
+
* rotated one must be the one returned here — the token the exchange started with is spent.
|
|
159
|
+
*
|
|
160
|
+
* @param input - The authorization code, the redirect URI it was issued against, and the raw
|
|
161
|
+
* callback query.
|
|
162
|
+
* @returns The credentials to persist.
|
|
163
|
+
*/
|
|
164
|
+
protected abstract credentialsForAuthorizationCode(input: UserExternalConnectionOAuthExchangeInput): Promise<UserExternalConnectionCredentials>;
|
|
165
|
+
/**
|
|
166
|
+
* PROVIDER (optional): exchanges the stored refresh token for new credentials.
|
|
167
|
+
*
|
|
168
|
+
* Optional because not every provider has a refresh path worth wiring — and because a provider that
|
|
169
|
+
* does not implement this stays correct rather than silently broken: `UserExternalConnectionReader`
|
|
170
|
+
* treats its absence as "cannot renew" and makes the user reconnect.
|
|
171
|
+
*
|
|
172
|
+
* PUBLIC, unlike the two abstract members above, because the reader reaches it through the provider
|
|
173
|
+
* registry rather than through a subclass.
|
|
174
|
+
*
|
|
175
|
+
* Implementations return what the provider issued and do NOT need to carry forward values the
|
|
176
|
+
* response omitted — the reader merges every result over the stored credentials.
|
|
177
|
+
*
|
|
178
|
+
* @param input - The acting user and the credentials currently stored.
|
|
179
|
+
* @returns The refreshed credentials.
|
|
180
|
+
*/
|
|
181
|
+
refreshCredentials?(input: UserExternalConnectionOAuthRefreshCredentialsInput): Promise<UserExternalConnectionCredentials>;
|
|
182
|
+
/**
|
|
183
|
+
* Carries the stored refresh token forward when a provider's exchange returned none.
|
|
184
|
+
*
|
|
185
|
+
* The paired write replaces a provider's credentials wholesale, so persisting an exchange that
|
|
186
|
+
* omitted `refresh_token` would DESTROY a working one while leaving the entry `connected` — a
|
|
187
|
+
* connection that can never be refreshed again and does not look broken. Providers that do not
|
|
188
|
+
* rotate their refresh token (Zoho, and others that issue one only on first consent) hit this on
|
|
189
|
+
* every reconnect; a rotating provider never reaches the read.
|
|
190
|
+
*
|
|
191
|
+
* A read failure is deliberately allowed to propagate: failing the handoff loudly is strictly
|
|
192
|
+
* better than clobbering the stored token.
|
|
193
|
+
*
|
|
194
|
+
* @param input - The acting user and the credentials the exchange produced.
|
|
195
|
+
* @returns The credentials to persist, with a stored refresh token retained when the exchange
|
|
196
|
+
* returned none.
|
|
197
|
+
*/
|
|
198
|
+
protected credentialsRetainingStoredRefreshToken(input: UserExternalConnectionOAuthRetainRefreshTokenInput): Promise<UserExternalConnectionCredentials>;
|
|
199
|
+
/**
|
|
200
|
+
* Builds the authorize URL to redirect an incoming `/authorize` request to.
|
|
201
|
+
*
|
|
202
|
+
* @param request - The incoming authorize request, which the state is read from.
|
|
203
|
+
* @returns The authorize URL, or null when the request carried no state.
|
|
204
|
+
*/
|
|
205
|
+
authorizeUrlForRequest(request: Request): Maybe<WebsiteUrl>;
|
|
206
|
+
/**
|
|
207
|
+
* Verifies the returned state, exchanges the authorization code, and persists the credentials.
|
|
208
|
+
*
|
|
209
|
+
* A refusal reported by the provider (`error` / `error_description`) is surfaced as the failure
|
|
210
|
+
* reason, so a rejected scope or a denied consent is not misreported as a missing code.
|
|
211
|
+
*
|
|
212
|
+
* @param input - The query parameters the provider redirected back with.
|
|
213
|
+
* @returns Where to redirect the user, and whether the handoff succeeded.
|
|
214
|
+
*/
|
|
215
|
+
handleCallback(input: UserExternalConnectionOAuthHandleCallbackInput): Promise<UserExternalConnectionOAuthCallbackResult>;
|
|
216
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type ConfigService } from '@nestjs/config';
|
|
2
|
+
import { type AES256GCMEncryptionSecret } from '@dereekb/nestjs';
|
|
3
|
+
import { type FirebaseServerEnvService } from '@dereekb/firebase-server';
|
|
4
|
+
import { type FirebaseAuthUserId, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
5
|
+
import { type Maybe, type Milliseconds } from '@dereekb/util';
|
|
6
|
+
/**
|
|
7
|
+
* Secret the external-connection OAuth `state` is encrypted with.
|
|
8
|
+
*
|
|
9
|
+
* Provider-agnostic on purpose: `state` is part of the OAuth 2.0 authorization-code flow itself
|
|
10
|
+
* (RFC 6749 4.1.1) and every provider echoes it back opaquely, so one secret serves the whole
|
|
11
|
+
* registry no matter how many providers are registered.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT the credentials secret. `USER_EXTERNAL_CONNECTION_ENCRYPTION_SECRET` is
|
|
14
|
+
* write-once — rotating it makes every stored `uecp` credential permanently undecryptable — whereas
|
|
15
|
+
* this one is freely rotatable, since a state lives for minutes and rotating it only invalidates
|
|
16
|
+
* handoffs that are mid-flight.
|
|
17
|
+
*/
|
|
18
|
+
export declare const USER_EXTERNAL_CONNECTION_STATE_SECRET_CONFIG_KEY = "USER_EXTERNAL_CONNECTION_STATE_SECRET";
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic secret used when running in a testing environment and no real secret is configured,
|
|
21
|
+
* so specs never need a live credential.
|
|
22
|
+
*/
|
|
23
|
+
export declare const TESTING_USER_EXTERNAL_CONNECTION_STATE_SECRET: AES256GCMEncryptionSecret;
|
|
24
|
+
/**
|
|
25
|
+
* How long a minted `state` stays valid.
|
|
26
|
+
*
|
|
27
|
+
* Long enough for a user to work through a provider's consent screen, short enough that a leaked
|
|
28
|
+
* state is not reusable later.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_USER_EXTERNAL_CONNECTION_STATE_EXPIRATION: Milliseconds;
|
|
31
|
+
/**
|
|
32
|
+
* The payload carried inside an encrypted external-connection OAuth `state`.
|
|
33
|
+
*/
|
|
34
|
+
export interface UserExternalConnectionStatePayload {
|
|
35
|
+
/**
|
|
36
|
+
* The user the handoff belongs to.
|
|
37
|
+
*/
|
|
38
|
+
readonly uid: FirebaseAuthUserId;
|
|
39
|
+
/**
|
|
40
|
+
* The provider the handoff was started for.
|
|
41
|
+
*
|
|
42
|
+
* Verified on the way back, so a state minted to connect one provider cannot be replayed against
|
|
43
|
+
* another provider's callback.
|
|
44
|
+
*/
|
|
45
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
46
|
+
/**
|
|
47
|
+
* Epoch milliseconds after which the state is rejected.
|
|
48
|
+
*/
|
|
49
|
+
readonly exp: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Who a verified state belongs to.
|
|
53
|
+
*/
|
|
54
|
+
export interface UserExternalConnectionStateActor {
|
|
55
|
+
readonly uid: FirebaseAuthUserId;
|
|
56
|
+
}
|
|
57
|
+
export interface MintUserExternalConnectionStateInput {
|
|
58
|
+
readonly uid: FirebaseAuthUserId;
|
|
59
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
60
|
+
}
|
|
61
|
+
export interface VerifyUserExternalConnectionStateInput {
|
|
62
|
+
readonly state: Maybe<string>;
|
|
63
|
+
/**
|
|
64
|
+
* The provider whose callback is verifying. A state minted for a different provider is rejected.
|
|
65
|
+
*/
|
|
66
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
67
|
+
}
|
|
68
|
+
export interface UserExternalConnectionStateCoderConfig {
|
|
69
|
+
readonly secret: AES256GCMEncryptionSecret;
|
|
70
|
+
/**
|
|
71
|
+
* How long a minted state stays valid. Defaults to {@link DEFAULT_USER_EXTERNAL_CONNECTION_STATE_EXPIRATION}.
|
|
72
|
+
*/
|
|
73
|
+
readonly expiresIn?: Maybe<Milliseconds>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Mints and verifies the OAuth `state` for external-connection handoffs.
|
|
77
|
+
*
|
|
78
|
+
* Declared as an abstract class so it is its own injection token, matching
|
|
79
|
+
* `UserExternalConnectionModuleConfig`. One coder is shared by every registered provider.
|
|
80
|
+
*/
|
|
81
|
+
export declare abstract class UserExternalConnectionStateCoder {
|
|
82
|
+
/**
|
|
83
|
+
* Mints a short-lived state for a user's connect handoff with a provider.
|
|
84
|
+
*/
|
|
85
|
+
abstract readonly mintState: (input: MintUserExternalConnectionStateInput) => string;
|
|
86
|
+
/**
|
|
87
|
+
* Resolves the user a state belongs to, or null when it is absent, tampered with, expired, or was
|
|
88
|
+
* minted for a different provider.
|
|
89
|
+
*/
|
|
90
|
+
abstract readonly verifyState: (input: VerifyUserExternalConnectionStateInput) => Maybe<UserExternalConnectionStateActor>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Creates the coder that mints and verifies the OAuth `state` for external-connection handoffs.
|
|
94
|
+
*
|
|
95
|
+
* The state is what lets a provider's redirect back to us be attributed to a user: the authorize
|
|
96
|
+
* request is a top-level browser navigation and carries no credentials of its own. Tamper-evidence
|
|
97
|
+
* is the requirement; AES-256-GCM is used because its auth tag provides that and additionally keeps
|
|
98
|
+
* the uid opaque to the browser.
|
|
99
|
+
*
|
|
100
|
+
* @param config - The encryption secret and optional expiration.
|
|
101
|
+
* @returns The state coder.
|
|
102
|
+
*
|
|
103
|
+
* @__NO_SIDE_EFFECTS__
|
|
104
|
+
*/
|
|
105
|
+
export declare function userExternalConnectionStateCoder(config: UserExternalConnectionStateCoderConfig): UserExternalConnectionStateCoder;
|
|
106
|
+
/**
|
|
107
|
+
* Builds the external-connection state coder from the environment.
|
|
108
|
+
*
|
|
109
|
+
* @param configService - The Nest config service used to read the state secret.
|
|
110
|
+
* @param envService - Used to detect a testing environment for the secret fallback.
|
|
111
|
+
* @returns The state coder.
|
|
112
|
+
* @throws {Error} When the configured secret is invalid outside a testing environment.
|
|
113
|
+
*/
|
|
114
|
+
export declare function userExternalConnectionStateCoderFactory(configService: ConfigService, envService: FirebaseServerEnvService): UserExternalConnectionStateCoder;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type FactoryWithRequiredInput, type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type FirebaseAuthUserId, type FirebaseAuthUserIdRef, type UserExternalConnectionEntry, type UserExternalConnectionFirestoreCollections, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
3
|
+
import { type UserExternalConnectionCredentials, type UserExternalConnectionServerFirestoreCollections } from './userexternalconnection.private';
|
|
4
|
+
/**
|
|
5
|
+
* Context required by {@link userExternalConnectionAccessor}.
|
|
6
|
+
*
|
|
7
|
+
* Carries both halves of the pair, the same as the server actions context. Unlike that context there
|
|
8
|
+
* is no `FirestoreContextReference` here: every read is a plain document read, so nothing in this
|
|
9
|
+
* file needs to start a transaction.
|
|
10
|
+
*/
|
|
11
|
+
export interface UserExternalConnectionAccessorContext extends UserExternalConnectionFirestoreCollections, UserExternalConnectionServerFirestoreCollections {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Identifies the one provider entry a read is about.
|
|
15
|
+
*/
|
|
16
|
+
export interface UserExternalConnectionReadParams {
|
|
17
|
+
readonly uid: FirebaseAuthUserId;
|
|
18
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Both halves of a user's connection state for a single provider.
|
|
22
|
+
*
|
|
23
|
+
* The public entry and the private credentials are returned together because a caller deciding
|
|
24
|
+
* whether it can act as this user needs both: the credentials alone cannot say whether the
|
|
25
|
+
* connection is `connected` or `error`, and the entry alone cannot be used to call anything.
|
|
26
|
+
*
|
|
27
|
+
* Either side may be null. A user with no connection document at all reads as both null.
|
|
28
|
+
*/
|
|
29
|
+
export interface UserExternalConnectionForProvider {
|
|
30
|
+
readonly uid: FirebaseAuthUserId;
|
|
31
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
32
|
+
/**
|
|
33
|
+
* The provider's entry on the client-readable document, when the user has one.
|
|
34
|
+
*/
|
|
35
|
+
readonly entry: Maybe<UserExternalConnectionEntry>;
|
|
36
|
+
/**
|
|
37
|
+
* The provider's stored credentials in plaintext, when the user has any.
|
|
38
|
+
*/
|
|
39
|
+
readonly credentials: Maybe<UserExternalConnectionCredentials>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Input identifying the user a {@link UserExternalConnectionAccessorUserInstance} reads for.
|
|
43
|
+
*/
|
|
44
|
+
export interface UserExternalConnectionAccessorUserInput extends FirebaseAuthUserIdRef {
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A {@link UserExternalConnectionAccessor} narrowed to ONE user and ONE provider.
|
|
48
|
+
*
|
|
49
|
+
* The accessor's entire read surface with `{ uid, providerType }` already applied.
|
|
50
|
+
*/
|
|
51
|
+
export interface UserExternalConnectionAccessorProviderInstance {
|
|
52
|
+
readonly uid: FirebaseAuthUserId;
|
|
53
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
54
|
+
/**
|
|
55
|
+
* Loads both halves of the pair.
|
|
56
|
+
*/
|
|
57
|
+
readUserExternalConnectionForProvider(): Promise<UserExternalConnectionForProvider>;
|
|
58
|
+
/**
|
|
59
|
+
* Loads only the stored credentials.
|
|
60
|
+
*/
|
|
61
|
+
readUserExternalConnectionCredentials(): Promise<Maybe<UserExternalConnectionCredentials>>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A {@link UserExternalConnectionAccessor} narrowed to one user, awaiting the provider to target.
|
|
65
|
+
*/
|
|
66
|
+
export type UserExternalConnectionAccessorUserInstance = FactoryWithRequiredInput<UserExternalConnectionAccessorProviderInstance, UserExternalConnectionProviderType>;
|
|
67
|
+
/**
|
|
68
|
+
* Server-only read surface for the UserExternalConnection document pair.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately the whole read surface and nothing more: no expiration policy, no refresh, no
|
|
71
|
+
* assertions. That keeps this usable by the OAuth provider services themselves — they need to read
|
|
72
|
+
* the credentials they are about to replace, and a tier that could refresh would have to know about
|
|
73
|
+
* the provider registry those services are registered in.
|
|
74
|
+
*
|
|
75
|
+
* {@link UserExternalConnectionReader} wraps this and adds the policy.
|
|
76
|
+
*/
|
|
77
|
+
export declare abstract class UserExternalConnectionAccessor {
|
|
78
|
+
/**
|
|
79
|
+
* Narrows this accessor to one user, returning a factory that narrows it further to one provider.
|
|
80
|
+
*
|
|
81
|
+
* The accessor's only entry point, and the same two levels
|
|
82
|
+
* {@link UserExternalConnectionReader.readerForUser} has, so a caller holding either states the user
|
|
83
|
+
* and provider it is reading for once:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const credentials = await accessor.accessorForUser({ uid })(CALCOM).readUserExternalConnectionCredentials();
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @param input - The user to read for.
|
|
90
|
+
* @returns A factory producing an accessor for whichever of that user's providers is needed.
|
|
91
|
+
*/
|
|
92
|
+
abstract accessorForUser(input: UserExternalConnectionAccessorUserInput): UserExternalConnectionAccessorUserInstance;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reference to a {@link UserExternalConnectionAccessor} instance.
|
|
96
|
+
*/
|
|
97
|
+
export interface UserExternalConnectionAccessorRef {
|
|
98
|
+
readonly userExternalConnectionAccessor: UserExternalConnectionAccessor;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates a {@link UserExternalConnectionAccessor} bound to the given context.
|
|
102
|
+
*
|
|
103
|
+
* Reads are NOT paired the way writes are. The pairing is a write invariant — the two documents are
|
|
104
|
+
* only ever written together, so a read of one cannot observe a state the other contradicts, and
|
|
105
|
+
* reading them in a transaction would buy nothing. Server paths load both halves; the client can only
|
|
106
|
+
* ever load the public one.
|
|
107
|
+
*
|
|
108
|
+
* @param context - The context carrying both halves of the pair.
|
|
109
|
+
* @returns A concrete UserExternalConnectionAccessor implementation.
|
|
110
|
+
*/
|
|
111
|
+
export declare function userExternalConnectionAccessor(context: UserExternalConnectionAccessorContext): UserExternalConnectionAccessor;
|