@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
@@ -1,8 +1,26 @@
1
- import { type FirebaseAuthUserId, type UserExternalConnectionProviderType } from '@dereekb/firebase';
2
- export declare const USER_EXTERNAL_CONNECTION_PROVIDER_NOT_CONNECTED_ERROR_CODE = "USER_EXTERNAL_CONNECTION_PROVIDER_NOT_CONNECTED";
3
- export declare const USER_EXTERNAL_CONNECTION_PROVIDER_NOT_ALLOWED_ERROR_CODE = "USER_EXTERNAL_CONNECTION_PROVIDER_NOT_ALLOWED";
4
- export declare const USER_EXTERNAL_CONNECTION_ALREADY_EXISTS_ERROR_CODE = "USER_EXTERNAL_CONNECTION_ALREADY_EXISTS";
5
- export declare const USER_EXTERNAL_CONNECTION_CREDENTIALS_EXPIRED_ERROR_CODE = "USER_EXTERNAL_CONNECTION_CREDENTIALS_EXPIRED";
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.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/firebase": "14.0.1",
9
- "@dereekb/firebase-server": "14.0.1",
10
- "@dereekb/model": "14.0.1",
11
- "@dereekb/nestjs": "14.0.1",
12
- "@dereekb/rxjs": "14.0.1",
13
- "@dereekb/util": "14.0.1",
14
- "@dereekb/zoho": "14.0.1",
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",