@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.
- package/calcom/index.esm.js +3 -2
- package/calcom/package.json +11 -10
- package/calcom/src/lib/calcom.oauth.connection.service.d.ts +2 -2
- package/discord/index.esm.js +174 -19
- package/discord/package.json +11 -10
- package/discord/src/lib/discord.oauth.connection.config.d.ts +43 -2
- package/discord/src/lib/discord.oauth.connection.module.d.ts +18 -1
- package/discord/src/lib/discord.oauth.connection.service.d.ts +40 -3
- package/index.esm.js +9 -1
- package/mailgun/package.json +10 -9
- package/mcp/package.json +12 -11
- package/model/index.esm.js +5182 -2369
- package/model/package.json +10 -9
- package/model/src/lib/userexternalconnection/index.d.ts +2 -0
- package/model/src/lib/userexternalconnection/oauth/index.d.ts +1 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.config.d.ts +51 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.controller.d.ts +49 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.refresh.d.ts +35 -0
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.service.d.ts +300 -7
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.state.d.ts +213 -12
- package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.throttle.d.ts +81 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.action.server.d.ts +214 -1
- package/model/src/lib/userexternalconnection/userexternalconnection.error.d.ts +118 -5
- package/model/src/lib/userexternalconnection/userexternalconnection.module.d.ts +52 -2
- package/model/src/lib/userexternalconnection/userexternalconnection.policy.d.ts +117 -0
- package/model/src/lib/userexternalconnection/userexternalconnection.signin.d.ts +286 -0
- package/oidc/package.json +11 -10
- package/package.json +12 -12
- package/src/lib/auth/auth.service.d.ts +26 -0
- package/test/package.json +12 -11
- package/twilio/package.json +9 -8
- package/zoho/index.esm.js +3 -2
- package/zoho/package.json +11 -10
- package/zoho/src/lib/zoho.oauth.connection.service.d.ts +2 -2
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import { type FirebaseAuthUserId, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type FirebaseAuthUserId, type UserExternalConnectionExternalAccountId, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
2
|
+
import { type Maybe } from '@dereekb/util';
|
|
3
|
+
/**
|
|
4
|
+
* The UserExternalConnection error codes, re-exported from `@dereekb/firebase`.
|
|
5
|
+
*
|
|
6
|
+
* They live in the shared package because the BROWSER branches on them too — a login page deciding
|
|
7
|
+
* what to say about a refused sign-in cannot import from a server package. Re-exported here so the
|
|
8
|
+
* codes stay importable beside the factories that throw them.
|
|
9
|
+
*/
|
|
10
|
+
export { USER_EXTERNAL_CONNECTION_PROVIDER_NOT_CONNECTED_ERROR_CODE, USER_EXTERNAL_CONNECTION_PROVIDER_NOT_ALLOWED_ERROR_CODE, USER_EXTERNAL_CONNECTION_ALREADY_EXISTS_ERROR_CODE, USER_EXTERNAL_CONNECTION_CREDENTIALS_EXPIRED_ERROR_CODE, USER_EXTERNAL_CONNECTION_EXTERNAL_ACCOUNT_IN_USE_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_NOT_ENABLED_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_DENIED_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_EMAIL_CONFLICT_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_USER_MISSING_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_IDENTITY_UNAVAILABLE_ERROR_CODE, USER_EXTERNAL_CONNECTION_LINK_NOT_ENABLED_ERROR_CODE, USER_EXTERNAL_CONNECTION_UNLINK_LAST_LOGIN_METHOD_ERROR_CODE, USER_EXTERNAL_CONNECTION_SIGN_IN_REPORTABLE_ERROR_CODES } from '@dereekb/firebase';
|
|
11
|
+
/**
|
|
12
|
+
* Reads the reportable error code off a thrown sign-in failure.
|
|
13
|
+
*
|
|
14
|
+
* Reads `HttpsError.details.code` — where every helper in `@dereekb/firebase-server` puts it — and
|
|
15
|
+
* returns it only when it is in `USER_EXTERNAL_CONNECTION_SIGN_IN_REPORTABLE_ERROR_CODES`. Anything
|
|
16
|
+
* else, including a plain `Error`, yields null.
|
|
17
|
+
*
|
|
18
|
+
* @param e - The thrown value.
|
|
19
|
+
* @returns The reportable code, or null when there is none.
|
|
20
|
+
*
|
|
21
|
+
* @__NO_SIDE_EFFECTS__
|
|
22
|
+
*/
|
|
23
|
+
export declare function userExternalConnectionSignInErrorCode(e: unknown): Maybe<string>;
|
|
6
24
|
/**
|
|
7
25
|
* Creates an error indicating the user already has a connection document.
|
|
8
26
|
*
|
|
@@ -42,3 +60,98 @@ export declare function userExternalConnectionCredentialsExpiredError(providerTy
|
|
|
42
60
|
* @returns A precondition-conflict HttpsError.
|
|
43
61
|
*/
|
|
44
62
|
export declare function userExternalConnectionProviderNotAllowedError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
63
|
+
/**
|
|
64
|
+
* Creates an error indicating another user already holds the external account being connected.
|
|
65
|
+
*
|
|
66
|
+
* Raised only for a provider whose policy declares its connections `unique` with `onCollision: 'block'`.
|
|
67
|
+
* The holder's uid is deliberately absent from the message and present only in `data`, which is
|
|
68
|
+
* server-side: telling a caller which account owns a Discord id is an account-enumeration oracle.
|
|
69
|
+
*
|
|
70
|
+
* @param providerType - The provider whose account is contested.
|
|
71
|
+
* @param externalAccountId - The contested external account id.
|
|
72
|
+
* @param existingUid - The uid that already holds it.
|
|
73
|
+
* @returns A precondition-conflict HttpsError.
|
|
74
|
+
*/
|
|
75
|
+
export declare function userExternalConnectionExternalAccountInUseError(providerType: UserExternalConnectionProviderType, externalAccountId: UserExternalConnectionExternalAccountId, existingUid: FirebaseAuthUserId): import("firebase-functions/https").HttpsError;
|
|
76
|
+
/**
|
|
77
|
+
* Creates an error indicating the provider is not configured to be used for signing in.
|
|
78
|
+
*
|
|
79
|
+
* Distinct from {@link userExternalConnectionProviderNotAllowedError}: the provider IS allowed, just
|
|
80
|
+
* not in the sign-in direction. Sign-in is opt-in per provider precisely so an app cannot acquire an
|
|
81
|
+
* unauthenticated account-creation surface by registering a connect provider.
|
|
82
|
+
*
|
|
83
|
+
* @param providerType - The provider a sign-in was attempted with.
|
|
84
|
+
* @returns A forbidden HttpsError.
|
|
85
|
+
*/
|
|
86
|
+
export declare function userExternalConnectionSignInNotEnabledError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
87
|
+
/**
|
|
88
|
+
* Creates an error indicating the app's sign-in delegate refused the identity.
|
|
89
|
+
*
|
|
90
|
+
* The delegate's `reason` rides in `data` rather than the message: it typically describes why an
|
|
91
|
+
* account does not qualify (no subscription, not a guild member), which is not something to hand back
|
|
92
|
+
* to an unauthenticated caller.
|
|
93
|
+
*
|
|
94
|
+
* @param providerType - The provider the sign-in was attempted with.
|
|
95
|
+
* @param reason - The delegate's reason, for the server log.
|
|
96
|
+
* @returns A forbidden HttpsError.
|
|
97
|
+
*/
|
|
98
|
+
export declare function userExternalConnectionSignInDeniedError(providerType: UserExternalConnectionProviderType, reason: string): import("firebase-functions/https").HttpsError;
|
|
99
|
+
/**
|
|
100
|
+
* Creates an error indicating a new user could not be created because the provider's email already
|
|
101
|
+
* belongs to a Firebase user.
|
|
102
|
+
*
|
|
103
|
+
* Adopting that account would let whoever controls the third-party email take over the Firebase one,
|
|
104
|
+
* so the sign-in fails instead. The remedy is an explicit link performed by the already-signed-in
|
|
105
|
+
* user — which is what the connect flow is.
|
|
106
|
+
*
|
|
107
|
+
* @param providerType - The provider the sign-in was attempted with.
|
|
108
|
+
* @returns A precondition-conflict HttpsError.
|
|
109
|
+
*/
|
|
110
|
+
export declare function userExternalConnectionSignInEmailConflictError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
111
|
+
/**
|
|
112
|
+
* Creates an error indicating the uid a sign-in resolved to no longer exists in Firebase Auth.
|
|
113
|
+
*
|
|
114
|
+
* Reachable when a user is deleted from Auth without their connection documents being cleaned up:
|
|
115
|
+
* minting a token for a deleted uid produces a signed-in session with no user record behind it.
|
|
116
|
+
*
|
|
117
|
+
* @param providerType - The provider the sign-in was attempted with.
|
|
118
|
+
* @returns A precondition-conflict HttpsError.
|
|
119
|
+
*/
|
|
120
|
+
export declare function userExternalConnectionSignInUserMissingError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
121
|
+
/**
|
|
122
|
+
* Creates an error indicating the provider returned no stable account id to identify the user by.
|
|
123
|
+
*
|
|
124
|
+
* A connect can proceed without one (it only costs the settings row its label), but a sign-in cannot:
|
|
125
|
+
* with no stable id there is nothing to key the identity on, and falling back to a mutable username
|
|
126
|
+
* or an email would be the takeover vector this design exists to avoid.
|
|
127
|
+
*
|
|
128
|
+
* @param providerType - The provider the sign-in was attempted with.
|
|
129
|
+
* @returns A precondition-conflict HttpsError.
|
|
130
|
+
*/
|
|
131
|
+
export declare function userExternalConnectionSignInIdentityUnavailableError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
132
|
+
/**
|
|
133
|
+
* Creates an error indicating the provider is not configured to be used as a login method.
|
|
134
|
+
*
|
|
135
|
+
* The same `policy.signIn` opt-in that gates the sign-in direction gates this one: linking a provider
|
|
136
|
+
* as a login method is what MAKES a later sign-in through it resolve to this account, so an app that
|
|
137
|
+
* has not enabled sign-in must not be able to acquire the binding by another route.
|
|
138
|
+
*
|
|
139
|
+
* @param providerType - The provider a link was attempted with.
|
|
140
|
+
* @returns A forbidden HttpsError.
|
|
141
|
+
*/
|
|
142
|
+
export declare function userExternalConnectionLinkNotEnabledError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
143
|
+
/**
|
|
144
|
+
* Creates an error indicating the unlink would leave the account with no way to sign back in.
|
|
145
|
+
*
|
|
146
|
+
* Deliberately CONSERVATIVE: it refuses whenever the account would be left with no remaining login
|
|
147
|
+
* link and no Firebase-native provider, which is exactly the state a custom-token-only user is in.
|
|
148
|
+
* Guessing wrong in the other direction produces an account nobody — including support — can get back
|
|
149
|
+
* into, so the ambiguous case is refused rather than allowed.
|
|
150
|
+
*
|
|
151
|
+
* A rare edge rather than the normal outcome of unlinking: the sign-in service gives every user it
|
|
152
|
+
* creates with an email a password credential, so "forgot password" is a working way back in.
|
|
153
|
+
*
|
|
154
|
+
* @param providerType - The provider the unlink was attempted on.
|
|
155
|
+
* @returns A precondition-conflict HttpsError.
|
|
156
|
+
*/
|
|
157
|
+
export declare function userExternalConnectionUnlinkLastLoginMethodError(providerType: UserExternalConnectionProviderType): import("firebase-functions/https").HttpsError;
|
|
@@ -2,10 +2,11 @@ import { type InjectionToken, type ModuleMetadata, type Provider } from '@nestjs
|
|
|
2
2
|
import { ConfigService } from '@nestjs/config';
|
|
3
3
|
import { type Maybe, type Milliseconds } from '@dereekb/util';
|
|
4
4
|
import { type FirestoreContext, type FirestoreContextReference, type UserExternalConnectionFirestoreCollections } from '@dereekb/firebase';
|
|
5
|
-
import { FirebaseServerEnvService } from '@dereekb/firebase-server';
|
|
5
|
+
import { type FirebaseServerAuthService, FirebaseServerEnvService } from '@dereekb/firebase-server';
|
|
6
6
|
import { type AES256GCMEncryptionSecret } from '@dereekb/nestjs';
|
|
7
7
|
import { type UserExternalConnectionPrivateConverterConfig, UserExternalConnectionServerFirestoreCollections } from './userexternalconnection.private';
|
|
8
8
|
import { type UserExternalConnectionServerActionsContext } from './userexternalconnection.action.server';
|
|
9
|
+
import { type UserExternalConnectionProviderPolicy } from './userexternalconnection.policy';
|
|
9
10
|
/**
|
|
10
11
|
* Environment variable name for the external-connection credentials encryption secret
|
|
11
12
|
* (hex-encoded AES-256 key).
|
|
@@ -55,9 +56,11 @@ export declare function userExternalConnectionServerFirestoreCollectionsFactory(
|
|
|
55
56
|
*
|
|
56
57
|
* @param appCollections - The app's collections, carrying the public UserExternalConnection collection.
|
|
57
58
|
* @param serverCollections - The module-owned private collection.
|
|
59
|
+
* @param authService - The app's auth service, when sign-in is configured. Only the unlink lockout
|
|
60
|
+
* guard reads it; an app with no sign-in writes no login links and never reaches the guard.
|
|
58
61
|
* @returns The assembled server actions context.
|
|
59
62
|
*/
|
|
60
|
-
export declare function userExternalConnectionServerActionsContextFactory(appCollections: UserExternalConnectionFirestoreCollections & FirestoreContextReference, serverCollections: UserExternalConnectionServerFirestoreCollections): UserExternalConnectionServerActionsContext;
|
|
63
|
+
export declare function userExternalConnectionServerActionsContextFactory(appCollections: UserExternalConnectionFirestoreCollections & FirestoreContextReference, serverCollections: UserExternalConnectionServerFirestoreCollections, authService?: Maybe<FirebaseServerAuthService>): UserExternalConnectionServerActionsContext;
|
|
61
64
|
/**
|
|
62
65
|
* NestJS injection token for the assembled {@link UserExternalConnectionServerActionsContext}.
|
|
63
66
|
*/
|
|
@@ -75,6 +78,53 @@ export interface ProvideAppUserExternalConnectionModuleMetadataConfig extends Pi
|
|
|
75
78
|
* Taking this as a token is what keeps the package from ever naming an app's collections class.
|
|
76
79
|
*/
|
|
77
80
|
readonly appCollectionsToken: InjectionToken;
|
|
81
|
+
/**
|
|
82
|
+
* Per-provider rules the connect and sign-in paths enforce.
|
|
83
|
+
*
|
|
84
|
+
* Omitted means every provider takes {@link DEFAULT_USER_EXTERNAL_CONNECTION_PROVIDER_POLICY} —
|
|
85
|
+
* shared accounts permitted, connect-only — which is exactly how this module behaved before
|
|
86
|
+
* policies existed.
|
|
87
|
+
*/
|
|
88
|
+
readonly providerPolicies?: Maybe<readonly UserExternalConnectionProviderPolicy[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Configuration for the sign-in half of the module.
|
|
91
|
+
*
|
|
92
|
+
* Omitted means no {@link UserExternalConnectionSignInService} is provided at all, and every
|
|
93
|
+
* provider's sign-in routes refuse. That is the correct shape for an app that only CONNECTS
|
|
94
|
+
* providers: enabling sign-in creates an unauthenticated account-creation surface, and it should
|
|
95
|
+
* take a deliberate declaration to acquire one.
|
|
96
|
+
*/
|
|
97
|
+
readonly signIn?: Maybe<ProvideAppUserExternalConnectionSignInConfig>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Configuration for the sign-in half of the UserExternalConnection module.
|
|
101
|
+
*/
|
|
102
|
+
export interface ProvideAppUserExternalConnectionSignInConfig {
|
|
103
|
+
/**
|
|
104
|
+
* Token resolving the app's `FirebaseServerAuthService`.
|
|
105
|
+
*
|
|
106
|
+
* Taken as a token because the auth service is the app's own subclass — this package must never
|
|
107
|
+
* name it.
|
|
108
|
+
*/
|
|
109
|
+
readonly authServiceToken: InjectionToken;
|
|
110
|
+
/**
|
|
111
|
+
* Token resolving the app's {@link UserExternalConnectionSignInDelegate}.
|
|
112
|
+
*
|
|
113
|
+
* Omitted means {@link denyNewUserSignInDelegate}: a returning user signs in, a stranger is
|
|
114
|
+
* refused. Provisioning strangers is an explicit opt-in.
|
|
115
|
+
*/
|
|
116
|
+
readonly delegateToken?: Maybe<InjectionToken>;
|
|
117
|
+
/**
|
|
118
|
+
* Whether a created user may ADOPT an existing Firebase account whose email matches the provider's
|
|
119
|
+
* VERIFIED email. Defaults to false — see the field docs on the service config.
|
|
120
|
+
*/
|
|
121
|
+
readonly allowVerifiedEmailLinking?: Maybe<boolean>;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a created user is also given a password credential, so the account keeps a
|
|
124
|
+
* Firebase-native recovery path after the third-party provider is unlinked. Defaults to TRUE — see the
|
|
125
|
+
* field docs on the service config.
|
|
126
|
+
*/
|
|
127
|
+
readonly provisionPasswordCredential?: Maybe<boolean>;
|
|
78
128
|
}
|
|
79
129
|
/**
|
|
80
130
|
* Convenience function used to generate ModuleMetadata for an app's UserExternalConnectionModule.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
3
|
+
/**
|
|
4
|
+
* What happens when the external account being connected is already held by a different user.
|
|
5
|
+
*
|
|
6
|
+
* - `block` — refuse the connect. The default, and the only one that is safe without thought.
|
|
7
|
+
* - `transfer` — disconnect the prior holder in the same transaction and connect the new one. Right
|
|
8
|
+
* when the third-party account is the identity of record and a person may have created a stray
|
|
9
|
+
* Firebase user by another route.
|
|
10
|
+
* - `allow` — permit both. Correct only for a provider whose accounts are legitimately shared
|
|
11
|
+
* (a team's shared Zoom account, say), and incompatible with using that provider to sign in.
|
|
12
|
+
*/
|
|
13
|
+
export type UserExternalConnectionCollisionPolicy = 'block' | 'transfer' | 'allow';
|
|
14
|
+
/**
|
|
15
|
+
* Per-provider rules the connect and sign-in paths enforce.
|
|
16
|
+
*
|
|
17
|
+
* Declared by the app rather than by the provider adapter: whether two users may share a Discord
|
|
18
|
+
* account, and whether Discord may be used to log in at all, are product decisions, not facts about
|
|
19
|
+
* Discord's API.
|
|
20
|
+
*/
|
|
21
|
+
export interface UserExternalConnectionProviderPolicy {
|
|
22
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
23
|
+
/**
|
|
24
|
+
* At most one Firebase user per external account. Defaults to false.
|
|
25
|
+
*
|
|
26
|
+
* A provider used for SIGN-IN should almost always be unique — otherwise "who is this account?" has
|
|
27
|
+
* more than one answer and the sign-in resolves arbitrarily.
|
|
28
|
+
*
|
|
29
|
+
* Do not turn this on for a provider with existing connections until the `ec` backfill has run:
|
|
30
|
+
* uniqueness is enforced against `ec`, and a document written before that field existed has none.
|
|
31
|
+
*/
|
|
32
|
+
readonly unique?: Maybe<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* The provider may be used to sign in. Defaults to FALSE.
|
|
35
|
+
*
|
|
36
|
+
* Opt-in because enabling it turns `/oauth/<provider>/signin` into an unauthenticated
|
|
37
|
+
* account-creation surface; registering a provider for the connect direction must not silently
|
|
38
|
+
* grant that.
|
|
39
|
+
*/
|
|
40
|
+
readonly signIn?: Maybe<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* A SIGN-IN through this provider also establishes the DATA connection, storing the credentials the
|
|
43
|
+
* sign-in exchange produced. Defaults to FALSE.
|
|
44
|
+
*
|
|
45
|
+
* Off by default because the two grants are not the same grant. A sign-in requests the identity
|
|
46
|
+
* scopes (`identify`, `email`); a data connection requests whatever the integration actually reads.
|
|
47
|
+
* Writing the sign-in's credentials into the data connection therefore REPLACES a broad grant with a
|
|
48
|
+
* narrow one every time the user signs in — silently downgrading a working integration.
|
|
49
|
+
*
|
|
50
|
+
* Turn it on only for an app whose sign-in scopes are a superset of its data scopes, or one that has
|
|
51
|
+
* no data integration and just wants the connection row to show up.
|
|
52
|
+
*/
|
|
53
|
+
readonly signInConnects?: Maybe<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* What to do when `unique` is set and another user already holds the account. Defaults to `block`.
|
|
56
|
+
*/
|
|
57
|
+
readonly onCollision?: Maybe<UserExternalConnectionCollisionPolicy>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The policy applied to a provider the app declared nothing for.
|
|
61
|
+
*
|
|
62
|
+
* Every field is the restrictive option: an unlisted provider behaves exactly as it did before
|
|
63
|
+
* policies existed (shared accounts permitted, connect-only), so adding the registry changes no
|
|
64
|
+
* existing app's behavior.
|
|
65
|
+
*/
|
|
66
|
+
export declare const DEFAULT_USER_EXTERNAL_CONNECTION_PROVIDER_POLICY: Omit<Required<UserExternalConnectionProviderPolicy>, 'providerType'>;
|
|
67
|
+
/**
|
|
68
|
+
* Resolves the {@link UserExternalConnectionProviderPolicy} for a provider.
|
|
69
|
+
*
|
|
70
|
+
* An abstract class so it is its own injection token. Optional to provide — a missing registry reads
|
|
71
|
+
* as "every provider takes the default policy".
|
|
72
|
+
*/
|
|
73
|
+
export declare abstract class UserExternalConnectionProviderPolicyRegistry {
|
|
74
|
+
abstract readonly policyForProviderType: (providerType: UserExternalConnectionProviderType) => UserExternalConnectionResolvedProviderPolicy;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A policy with every optional field resolved, so enforcement sites never re-apply defaults.
|
|
78
|
+
*/
|
|
79
|
+
export interface UserExternalConnectionResolvedProviderPolicy {
|
|
80
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
81
|
+
readonly unique: boolean;
|
|
82
|
+
readonly signIn: boolean;
|
|
83
|
+
readonly signInConnects: boolean;
|
|
84
|
+
readonly onCollision: UserExternalConnectionCollisionPolicy;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves a declared policy against {@link DEFAULT_USER_EXTERNAL_CONNECTION_PROVIDER_POLICY}.
|
|
88
|
+
*
|
|
89
|
+
* @param providerType - The provider being resolved.
|
|
90
|
+
* @param policy - The app's declaration for it, when there is one.
|
|
91
|
+
* @returns The policy with every field populated.
|
|
92
|
+
*
|
|
93
|
+
* @__NO_SIDE_EFFECTS__
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveUserExternalConnectionProviderPolicy(providerType: UserExternalConnectionProviderType, policy?: Maybe<UserExternalConnectionProviderPolicy>): UserExternalConnectionResolvedProviderPolicy;
|
|
96
|
+
/**
|
|
97
|
+
* Creates a {@link UserExternalConnectionProviderPolicyRegistry} from the app's declarations.
|
|
98
|
+
*
|
|
99
|
+
* @param policies - The per-provider policies the app declares. Providers absent from the list take
|
|
100
|
+
* the default policy.
|
|
101
|
+
* @returns The registry.
|
|
102
|
+
*
|
|
103
|
+
* @__NO_SIDE_EFFECTS__
|
|
104
|
+
*/
|
|
105
|
+
export declare function userExternalConnectionProviderPolicyRegistry(policies?: Maybe<readonly UserExternalConnectionProviderPolicy[]>): UserExternalConnectionProviderPolicyRegistry;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves a provider's policy, treating a missing registry as "all defaults".
|
|
108
|
+
*
|
|
109
|
+
* The registry is optional, so every enforcement site would otherwise repeat this fallback.
|
|
110
|
+
*
|
|
111
|
+
* @param registry - The registry, when the app provided one.
|
|
112
|
+
* @param providerType - The provider to resolve.
|
|
113
|
+
* @returns The resolved policy.
|
|
114
|
+
*
|
|
115
|
+
* @__NO_SIDE_EFFECTS__
|
|
116
|
+
*/
|
|
117
|
+
export declare function userExternalConnectionPolicyForProviderType(registry: Maybe<UserExternalConnectionProviderPolicyRegistry>, providerType: UserExternalConnectionProviderType): UserExternalConnectionResolvedProviderPolicy;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { type EmailAddress, type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type FirebaseAuthUserId, type UserExternalConnectionExternalAccountId, type UserExternalConnectionFirestoreCollections, type UserExternalConnectionProviderType } from '@dereekb/firebase';
|
|
3
|
+
import { type FirebaseServerAuthService } from '@dereekb/firebase-server';
|
|
4
|
+
/**
|
|
5
|
+
* What a provider reported about the account a sign-in is being attempted with.
|
|
6
|
+
*
|
|
7
|
+
* Every field is read SERVER-SIDE from a token the server itself obtained. Nothing here may come
|
|
8
|
+
* from the client: an `id_token` handed over by a browser is unverified (nothing in this workspace
|
|
9
|
+
* is a JWKS verifier), and accepting one would let a caller assert any identity it liked.
|
|
10
|
+
*/
|
|
11
|
+
export interface UserExternalConnectionSignInIdentity {
|
|
12
|
+
/**
|
|
13
|
+
* The provider's STABLE id for the account — a Discord snowflake, not a username.
|
|
14
|
+
*
|
|
15
|
+
* Identity is keyed on this and nothing else. Usernames are mutable on most providers (Discord
|
|
16
|
+
* made them so in 2023), so keying on one would hand an account over to whoever claimed the name
|
|
17
|
+
* next.
|
|
18
|
+
*/
|
|
19
|
+
readonly externalAccountId: UserExternalConnectionExternalAccountId;
|
|
20
|
+
/**
|
|
21
|
+
* The account's email, when the provider reported one.
|
|
22
|
+
*/
|
|
23
|
+
readonly email?: Maybe<EmailAddress>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the PROVIDER considers that email verified.
|
|
26
|
+
*
|
|
27
|
+
* Load-bearing: an unverified third-party email matching an existing Firebase user is an
|
|
28
|
+
* account-takeover vector, so linking on email is refused unless this is true AND the app opted in.
|
|
29
|
+
*/
|
|
30
|
+
readonly emailVerified?: Maybe<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Human-readable label for the account, e.g. a display name.
|
|
33
|
+
*/
|
|
34
|
+
readonly label?: Maybe<string>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Input handed to a {@link UserExternalConnectionSignInDelegate}.
|
|
38
|
+
*/
|
|
39
|
+
export interface UserExternalConnectionSignInInput {
|
|
40
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
41
|
+
readonly identity: UserExternalConnectionSignInIdentity;
|
|
42
|
+
/**
|
|
43
|
+
* The uid already holding this external account, when one does.
|
|
44
|
+
*
|
|
45
|
+
* Set means "a returning user"; absent means "nobody in this project has ever signed in as this
|
|
46
|
+
* third-party account", which is the decision the delegate exists to make.
|
|
47
|
+
*/
|
|
48
|
+
readonly existingUid?: Maybe<FirebaseAuthUserId>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What the app decided to do about a sign-in attempt.
|
|
52
|
+
*/
|
|
53
|
+
export type UserExternalConnectionSignInResolution = UserExternalConnectionSignInResolutionSignIn | UserExternalConnectionSignInResolutionCreateUser | UserExternalConnectionSignInResolutionDeny;
|
|
54
|
+
export interface UserExternalConnectionSignInResolutionSignIn {
|
|
55
|
+
readonly action: 'signIn';
|
|
56
|
+
readonly uid: FirebaseAuthUserId;
|
|
57
|
+
}
|
|
58
|
+
export interface UserExternalConnectionSignInResolutionCreateUser {
|
|
59
|
+
readonly action: 'createUser';
|
|
60
|
+
readonly email?: Maybe<EmailAddress>;
|
|
61
|
+
readonly displayName?: Maybe<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Claims to set on the newly created user, before the custom token is minted.
|
|
64
|
+
*/
|
|
65
|
+
readonly claims?: Maybe<object>;
|
|
66
|
+
}
|
|
67
|
+
export interface UserExternalConnectionSignInResolutionDeny {
|
|
68
|
+
readonly action: 'deny';
|
|
69
|
+
/**
|
|
70
|
+
* Why the sign-in was refused. Logged server-side; never returned to the browser, since it may
|
|
71
|
+
* describe why an account does not qualify.
|
|
72
|
+
*/
|
|
73
|
+
readonly reason: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decides whether a third-party identity may sign in, and as whom.
|
|
77
|
+
*
|
|
78
|
+
* This is the app's policy hook and the ONLY place that decision is made. It is an abstract class so
|
|
79
|
+
* it is its own injection token, matching the other members of this module.
|
|
80
|
+
*
|
|
81
|
+
* The library default is {@link denyNewUserSignInDelegate} — a returning user signs in, a stranger is
|
|
82
|
+
* refused. Provisioning strangers is an explicit opt-in, because a sign-in endpoint that creates
|
|
83
|
+
* users is an unauthenticated account-creation surface.
|
|
84
|
+
*/
|
|
85
|
+
export declare abstract class UserExternalConnectionSignInDelegate {
|
|
86
|
+
abstract readonly resolveSignIn: (input: UserExternalConnectionSignInInput) => Promise<UserExternalConnectionSignInResolution>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The DEFAULT sign-in delegate: an existing user signs in, an unrecognized one is refused.
|
|
90
|
+
*
|
|
91
|
+
* Deny-by-default because the alternative — creating a Firebase user for anyone who can complete a
|
|
92
|
+
* consent screen at a third party — is a decision an app has to make deliberately. Pair with
|
|
93
|
+
* {@link autoCreateUserSignInDelegate} when open registration IS the intent.
|
|
94
|
+
*
|
|
95
|
+
* @returns A delegate that never provisions a new user.
|
|
96
|
+
*
|
|
97
|
+
* @__NO_SIDE_EFFECTS__
|
|
98
|
+
*/
|
|
99
|
+
export declare function denyNewUserSignInDelegate(): UserExternalConnectionSignInDelegate;
|
|
100
|
+
/**
|
|
101
|
+
* How much of an email the provider must report before a new Firebase user may be provisioned.
|
|
102
|
+
*
|
|
103
|
+
* - `verified` — the provider must report an email AND consider it verified.
|
|
104
|
+
* - `any` — the provider must report an email; whether it is verified is not checked.
|
|
105
|
+
* - `none` — no email is required, and an emailless user may be created.
|
|
106
|
+
*/
|
|
107
|
+
export type UserExternalConnectionSignInEmailRequirement = 'verified' | 'any' | 'none';
|
|
108
|
+
/**
|
|
109
|
+
* The default {@link UserExternalConnectionSignInEmailRequirement}.
|
|
110
|
+
*/
|
|
111
|
+
export declare const DEFAULT_USER_EXTERNAL_CONNECTION_SIGN_IN_EMAIL_REQUIREMENT: UserExternalConnectionSignInEmailRequirement;
|
|
112
|
+
/**
|
|
113
|
+
* Configuration for {@link autoCreateUserSignInDelegate}.
|
|
114
|
+
*/
|
|
115
|
+
export interface AutoCreateUserSignInDelegateConfig {
|
|
116
|
+
/**
|
|
117
|
+
* Whether to carry the provider's email onto the created Firebase user.
|
|
118
|
+
*
|
|
119
|
+
* Defaults to true, and applies ONLY to the created user's own record — it does not permit
|
|
120
|
+
* adopting an existing account that already holds the email. That is
|
|
121
|
+
* {@link UserExternalConnectionSignInServiceConfig.allowVerifiedEmailLinking}.
|
|
122
|
+
*
|
|
123
|
+
* NOTE that setting this false while {@link requireEmailToCreateUser} is anything other than
|
|
124
|
+
* `'none'` still GATES on the provider's email but omits it from the created record — which also
|
|
125
|
+
* suppresses the service's own `getUserByEmail` collision check, since that check reads the email
|
|
126
|
+
* this delegate returns.
|
|
127
|
+
*/
|
|
128
|
+
readonly useProviderEmail?: Maybe<boolean>;
|
|
129
|
+
/**
|
|
130
|
+
* What the provider must report about the account's email before a user is created.
|
|
131
|
+
*
|
|
132
|
+
* Defaults to {@link DEFAULT_USER_EXTERNAL_CONNECTION_SIGN_IN_EMAIL_REQUIREMENT} (`'verified'`).
|
|
133
|
+
* Provisioning an account off an UNVERIFIED third-party email is the takeover vector the service's
|
|
134
|
+
* own docs warn about, and it is the one case the service-side collision check cannot cover: an
|
|
135
|
+
* absent email skips the check entirely, and Discord's default scopes report none at all.
|
|
136
|
+
*/
|
|
137
|
+
readonly requireEmailToCreateUser?: Maybe<UserExternalConnectionSignInEmailRequirement>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns why an identity's email does not meet a requirement, or null when it does.
|
|
141
|
+
*
|
|
142
|
+
* @param requirement - The requirement to check against.
|
|
143
|
+
* @param identity - The identity the provider reported.
|
|
144
|
+
* @returns The refusal reason, or null when the requirement is met.
|
|
145
|
+
*
|
|
146
|
+
* @__NO_SIDE_EFFECTS__
|
|
147
|
+
*/
|
|
148
|
+
export declare function userExternalConnectionSignInEmailRequirementDenyReason(requirement: UserExternalConnectionSignInEmailRequirement, identity: UserExternalConnectionSignInIdentity): Maybe<string>;
|
|
149
|
+
/**
|
|
150
|
+
* An OPT-IN sign-in delegate that provisions a new user for an unrecognized third-party account.
|
|
151
|
+
*
|
|
152
|
+
* Open registration. Use it when anyone able to authenticate at the provider is meant to get an
|
|
153
|
+
* account; use a bespoke delegate when they are not (checking a subscription, a guild membership, or
|
|
154
|
+
* an invite list before returning `createUser`).
|
|
155
|
+
*
|
|
156
|
+
* By default a new user is created ONLY for an identity carrying a provider-VERIFIED email — see
|
|
157
|
+
* {@link AutoCreateUserSignInDelegateConfig.requireEmailToCreateUser}.
|
|
158
|
+
*
|
|
159
|
+
* @param config - Optional configuration.
|
|
160
|
+
* @returns A delegate that creates a user on a miss.
|
|
161
|
+
*
|
|
162
|
+
* @__NO_SIDE_EFFECTS__
|
|
163
|
+
*/
|
|
164
|
+
export declare function autoCreateUserSignInDelegate(config?: Maybe<AutoCreateUserSignInDelegateConfig>): UserExternalConnectionSignInDelegate;
|
|
165
|
+
/**
|
|
166
|
+
* Input for {@link UserExternalConnectionSignInService.resolveSignIn}.
|
|
167
|
+
*/
|
|
168
|
+
export interface ResolveUserExternalConnectionSignInInput {
|
|
169
|
+
readonly providerType: UserExternalConnectionProviderType;
|
|
170
|
+
readonly identity: UserExternalConnectionSignInIdentity;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The uid a sign-in resolved to.
|
|
174
|
+
*/
|
|
175
|
+
export interface UserExternalConnectionSignInResult {
|
|
176
|
+
readonly uid: FirebaseAuthUserId;
|
|
177
|
+
/**
|
|
178
|
+
* Whether a new Firebase user was provisioned for this sign-in.
|
|
179
|
+
*/
|
|
180
|
+
readonly created: boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolves a third-party identity to a Firebase uid and mints the custom token that signs them in.
|
|
184
|
+
*
|
|
185
|
+
* Split out of the OAuth service so the identity → uid decision, which is app policy, is not tangled
|
|
186
|
+
* with any one provider's OAuth mechanics — and so a provider adapter needs no knowledge of Firebase
|
|
187
|
+
* Auth at all.
|
|
188
|
+
*
|
|
189
|
+
* Optional to provide: an app that only ever CONNECTS providers never registers one, and its OAuth
|
|
190
|
+
* services then reject sign-in requests outright.
|
|
191
|
+
*/
|
|
192
|
+
export declare abstract class UserExternalConnectionSignInService {
|
|
193
|
+
/**
|
|
194
|
+
* Resolves the uid a third-party identity signs in as, provisioning one if the app's delegate says
|
|
195
|
+
* to.
|
|
196
|
+
*/
|
|
197
|
+
abstract readonly resolveSignIn: (input: ResolveUserExternalConnectionSignInInput) => Promise<UserExternalConnectionSignInResult>;
|
|
198
|
+
/**
|
|
199
|
+
* Mints the Firebase custom token the client exchanges via `signInWithCustomToken`.
|
|
200
|
+
*/
|
|
201
|
+
abstract readonly mintCustomTokenForUser: (input: UserExternalConnectionMintCustomTokenInput) => Promise<string>;
|
|
202
|
+
}
|
|
203
|
+
export interface UserExternalConnectionMintCustomTokenInput {
|
|
204
|
+
readonly uid: FirebaseAuthUserId;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Configuration for {@link userExternalConnectionSignInService}.
|
|
208
|
+
*/
|
|
209
|
+
export interface UserExternalConnectionSignInServiceConfig extends UserExternalConnectionFirestoreCollections {
|
|
210
|
+
readonly authService: FirebaseServerAuthService;
|
|
211
|
+
/**
|
|
212
|
+
* The app's policy hook. Defaults to {@link denyNewUserSignInDelegate}.
|
|
213
|
+
*/
|
|
214
|
+
readonly delegate?: Maybe<UserExternalConnectionSignInDelegate>;
|
|
215
|
+
/**
|
|
216
|
+
* Whether a `createUser` resolution may ADOPT an existing Firebase user whose email matches the
|
|
217
|
+
* provider's — and only when the provider reported that email VERIFIED.
|
|
218
|
+
*
|
|
219
|
+
* Defaults to false. Even with a verified email this is a policy choice rather than an obviously
|
|
220
|
+
* safe one: it means whoever controls the third-party account controls the Firebase account. The
|
|
221
|
+
* safe alternative is an explicit link step performed by an already-signed-in user, which is what
|
|
222
|
+
* the connect flow already is.
|
|
223
|
+
*/
|
|
224
|
+
readonly allowVerifiedEmailLinking?: Maybe<boolean>;
|
|
225
|
+
/**
|
|
226
|
+
* Whether a newly created user is also given a PASSWORD credential, so the account has a
|
|
227
|
+
* Firebase-native way back in that does not depend on the third-party provider.
|
|
228
|
+
*
|
|
229
|
+
* Defaults to TRUE, and only applies when the user is created with an email — a password credential
|
|
230
|
+
* on an emailless user is unreachable, since there is no address to sign in with or reset against.
|
|
231
|
+
*
|
|
232
|
+
* The password is high-entropy, generated per user, and DISCARDED — nobody, including this server,
|
|
233
|
+
* ever learns it. It is not a credential the user is expected to use directly: it exists so
|
|
234
|
+
* "forgot password" against their own verified email is a working recovery path. That is what makes
|
|
235
|
+
* removing the third-party provider a safe operation rather than a lockout, which is why
|
|
236
|
+
* {@link userExternalConnectionUnlinkLastLoginMethodError} is a rare edge rather than the normal
|
|
237
|
+
* outcome of unlinking.
|
|
238
|
+
*
|
|
239
|
+
* Turn it off for an app that wants federated-only accounts and accepts that unlinking the last
|
|
240
|
+
* provider will be refused.
|
|
241
|
+
*/
|
|
242
|
+
readonly provisionPasswordCredential?: Maybe<boolean>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* How many random bytes back a provisioned password credential.
|
|
246
|
+
*
|
|
247
|
+
* 48 bytes is 384 bits. Deliberately NOT the six-digit `generateRandomSetupPassword()` the new-user
|
|
248
|
+
* service uses: that is an INVITATION password, delivered to the user and meant to be typed once. This
|
|
249
|
+
* one is never delivered to anyone, so its only job is to be unguessable — and a six-digit password
|
|
250
|
+
* sitting on an account nobody is watching would be a genuine takeover vector.
|
|
251
|
+
*/
|
|
252
|
+
export declare const USER_EXTERNAL_CONNECTION_PROVISIONED_PASSWORD_BYTES = 48;
|
|
253
|
+
/**
|
|
254
|
+
* Generates the password credential a newly created federated user is provisioned with.
|
|
255
|
+
*
|
|
256
|
+
* The value is returned to exactly one caller, handed straight to `auth.createUser()`, and never
|
|
257
|
+
* stored, logged, or returned to the client. Recovery goes through the user's own verified email.
|
|
258
|
+
*
|
|
259
|
+
* @returns A high-entropy password.
|
|
260
|
+
*/
|
|
261
|
+
export declare function generateUserExternalConnectionProvisionedPassword(): string;
|
|
262
|
+
/**
|
|
263
|
+
* Creates the {@link UserExternalConnectionSignInService}.
|
|
264
|
+
*
|
|
265
|
+
* User creation deliberately does NOT go through `AbstractFirebaseServerNewUserService.initializeNewUser()`:
|
|
266
|
+
* that assigns a random six-digit password and writes a setup-password claim, which are INVITATION
|
|
267
|
+
* semantics — the password is delivered to the user and the account is expected to complete a setup
|
|
268
|
+
* step. A federated sign-in has neither, so `auth.createUser()` is called directly.
|
|
269
|
+
*
|
|
270
|
+
* It DOES provision a password credential of its own (see `provisionPasswordCredential`), but for the
|
|
271
|
+
* opposite reason: that one is never delivered to anyone. It exists so the account has a
|
|
272
|
+
* Firebase-native recovery path via the user's own verified email, which is what makes unlinking the
|
|
273
|
+
* third-party provider a safe operation instead of a lockout.
|
|
274
|
+
*
|
|
275
|
+
* @param config - The auth service, the public collection, and the app's delegate.
|
|
276
|
+
* @returns The sign-in service.
|
|
277
|
+
*
|
|
278
|
+
* @__NO_SIDE_EFFECTS__
|
|
279
|
+
*/
|
|
280
|
+
export declare function userExternalConnectionSignInService(config: UserExternalConnectionSignInServiceConfig): UserExternalConnectionSignInService;
|
|
281
|
+
/**
|
|
282
|
+
* Reference to a {@link UserExternalConnectionSignInService} instance.
|
|
283
|
+
*/
|
|
284
|
+
export interface UserExternalConnectionSignInServiceRef {
|
|
285
|
+
readonly userExternalConnectionSignInService: UserExternalConnectionSignInService;
|
|
286
|
+
}
|
package/oidc/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/oidc",
|
|
3
|
-
"version": "14.0
|
|
3
|
+
"version": "14.2.0",
|
|
4
|
+
"sideEffects": false,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"peerDependencies": {
|
|
6
|
-
"@dereekb/analytics": "14.0
|
|
7
|
-
"@dereekb/date": "14.0
|
|
8
|
-
"@dereekb/firebase": "14.0
|
|
9
|
-
"@dereekb/firebase-server": "14.0
|
|
10
|
-
"@dereekb/model": "14.0
|
|
11
|
-
"@dereekb/nestjs": "14.0
|
|
12
|
-
"@dereekb/rxjs": "14.0
|
|
13
|
-
"@dereekb/util": "14.0
|
|
14
|
-
"@dereekb/zoho": "14.0
|
|
7
|
+
"@dereekb/analytics": "14.2.0",
|
|
8
|
+
"@dereekb/date": "14.2.0",
|
|
9
|
+
"@dereekb/firebase": "14.2.0",
|
|
10
|
+
"@dereekb/firebase-server": "14.2.0",
|
|
11
|
+
"@dereekb/model": "14.2.0",
|
|
12
|
+
"@dereekb/nestjs": "14.2.0",
|
|
13
|
+
"@dereekb/rxjs": "14.2.0",
|
|
14
|
+
"@dereekb/util": "14.2.0",
|
|
15
|
+
"@dereekb/zoho": "14.2.0",
|
|
15
16
|
"@nestjs/common": "^12.0.1",
|
|
16
17
|
"@nestjs/config": "^12.0.0",
|
|
17
18
|
"express": "^5.2.1",
|