@dereekb/firebase-server 13.32.0 → 13.34.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 (79) hide show
  1. package/calcom/index.cjs.default.js +1 -0
  2. package/calcom/index.cjs.js +1451 -0
  3. package/calcom/index.cjs.mjs +2 -0
  4. package/calcom/index.d.ts +1 -0
  5. package/calcom/index.esm.js +1437 -0
  6. package/calcom/package.json +29 -0
  7. package/calcom/src/index.d.ts +1 -0
  8. package/calcom/src/lib/calcom.oauth.connection.cache.d.ts +49 -0
  9. package/calcom/src/lib/calcom.oauth.connection.config.d.ts +75 -0
  10. package/calcom/src/lib/calcom.oauth.connection.context.d.ts +84 -0
  11. package/calcom/src/lib/calcom.oauth.connection.controller.d.ts +18 -0
  12. package/calcom/src/lib/calcom.oauth.connection.module.d.ts +55 -0
  13. package/calcom/src/lib/calcom.oauth.connection.service.d.ts +33 -0
  14. package/calcom/src/lib/index.d.ts +6 -0
  15. package/discord/index.cjs.default.js +1 -0
  16. package/discord/index.cjs.js +761 -0
  17. package/discord/index.cjs.mjs +2 -0
  18. package/discord/index.d.ts +1 -0
  19. package/discord/index.esm.js +753 -0
  20. package/discord/package.json +29 -0
  21. package/discord/src/index.d.ts +1 -0
  22. package/discord/src/lib/discord.oauth.connection.config.d.ts +69 -0
  23. package/discord/src/lib/discord.oauth.connection.controller.d.ts +18 -0
  24. package/discord/src/lib/discord.oauth.connection.module.d.ts +39 -0
  25. package/discord/src/lib/discord.oauth.connection.service.d.ts +43 -0
  26. package/discord/src/lib/index.d.ts +4 -0
  27. package/index.cjs.js +42 -9
  28. package/index.esm.js +42 -9
  29. package/mailgun/package.json +9 -9
  30. package/mcp/package.json +11 -11
  31. package/model/index.cjs.js +4243 -855
  32. package/model/index.esm.js +4183 -862
  33. package/model/package.json +10 -9
  34. package/model/src/lib/index.d.ts +2 -0
  35. package/model/src/lib/system/index.d.ts +2 -0
  36. package/model/src/lib/system/system.private.d.ts +63 -0
  37. package/model/src/lib/system/system.private.module.d.ts +70 -0
  38. package/model/src/lib/userexternalconnection/index.d.ts +8 -0
  39. package/model/src/lib/userexternalconnection/oauth/index.d.ts +7 -0
  40. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.config.d.ts +110 -0
  41. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.controller.d.ts +68 -0
  42. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.error.d.ts +29 -0
  43. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.refresh.d.ts +24 -0
  44. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.registry.d.ts +54 -0
  45. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.service.d.ts +216 -0
  46. package/model/src/lib/userexternalconnection/oauth/userexternalconnection.oauth.state.d.ts +114 -0
  47. package/model/src/lib/userexternalconnection/userexternalconnection.accessor.service.d.ts +111 -0
  48. package/model/src/lib/userexternalconnection/userexternalconnection.action.server.d.ts +172 -0
  49. package/model/src/lib/userexternalconnection/userexternalconnection.error.d.ts +44 -0
  50. package/model/src/lib/userexternalconnection/userexternalconnection.module.d.ts +129 -0
  51. package/model/src/lib/userexternalconnection/userexternalconnection.private.d.ts +192 -0
  52. package/model/src/lib/userexternalconnection/userexternalconnection.reader.service.d.ts +157 -0
  53. package/model/src/lib/userexternalconnection/userexternalconnection.refresh.service.d.ts +46 -0
  54. package/oidc/index.cjs.js +104 -78
  55. package/oidc/index.esm.js +104 -78
  56. package/oidc/package.json +10 -10
  57. package/package.json +24 -10
  58. package/src/lib/env/env.config.d.ts +15 -0
  59. package/src/lib/env/env.service.d.ts +7 -0
  60. package/src/lib/firestore/snapshot/snapshot.field.encrypt.d.ts +21 -2
  61. package/src/lib/nest/env/env.service.d.ts +1 -0
  62. package/test/index.cjs.js +14 -2
  63. package/test/index.esm.js +15 -3
  64. package/test/package.json +11 -11
  65. package/test/src/lib/firebase/firebase.admin.auth.d.ts +1 -1
  66. package/twilio/package.json +8 -8
  67. package/zoho/README.md +38 -0
  68. package/zoho/index.cjs.js +1418 -64
  69. package/zoho/index.esm.js +1401 -67
  70. package/zoho/package.json +13 -9
  71. package/zoho/src/lib/index.d.ts +6 -0
  72. package/zoho/src/lib/zoho.accounts.firebase.d.ts +10 -3
  73. package/zoho/src/lib/zoho.accounts.firebase.module.d.ts +51 -0
  74. package/zoho/src/lib/zoho.accounts.firebase.system.d.ts +51 -8
  75. package/zoho/src/lib/zoho.oauth.connection.cache.d.ts +45 -0
  76. package/zoho/src/lib/zoho.oauth.connection.config.d.ts +80 -0
  77. package/zoho/src/lib/zoho.oauth.connection.controller.d.ts +18 -0
  78. package/zoho/src/lib/zoho.oauth.connection.module.d.ts +43 -0
  79. package/zoho/src/lib/zoho.oauth.connection.service.d.ts +107 -0
package/zoho/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/zoho",
3
- "version": "13.32.0",
3
+ "version": "13.34.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.32.0",
6
- "@dereekb/date": "13.32.0",
7
- "@dereekb/model": "13.32.0",
8
- "@dereekb/nestjs": "13.32.0",
9
- "@dereekb/rxjs": "13.32.0",
10
- "@dereekb/firebase": "13.32.0",
11
- "@dereekb/util": "13.32.0",
12
- "@dereekb/zoho": "13.32.0"
5
+ "@dereekb/analytics": "13.34.0",
6
+ "@dereekb/date": "13.34.0",
7
+ "@dereekb/model": "13.34.0",
8
+ "@dereekb/nestjs": "13.34.0",
9
+ "@dereekb/rxjs": "13.34.0",
10
+ "@dereekb/firebase": "13.34.0",
11
+ "@dereekb/firebase-server": "13.34.0",
12
+ "@dereekb/util": "13.34.0",
13
+ "@dereekb/zoho": "13.34.0",
14
+ "@nestjs/common": "^11.1.19",
15
+ "@nestjs/config": "^4.0.4",
16
+ "express": "^5.2.1"
13
17
  },
14
18
  "exports": {
15
19
  "./package.json": "./package.json",
@@ -1,2 +1,8 @@
1
1
  export * from './zoho.accounts.firebase';
2
+ export * from './zoho.accounts.firebase.module';
2
3
  export * from './zoho.accounts.firebase.system';
4
+ export * from './zoho.oauth.connection.cache';
5
+ export * from './zoho.oauth.connection.config';
6
+ export * from './zoho.oauth.connection.controller';
7
+ export * from './zoho.oauth.connection.module';
8
+ export * from './zoho.oauth.connection.service';
@@ -1,4 +1,4 @@
1
- import { type SystemStateFirestoreCollection } from '@dereekb/firebase';
1
+ import { type FirestoreDocument, type SystemState, type SystemStateFirestoreCollectionLike, type SystemStateStoredData } from '@dereekb/firebase';
2
2
  import { type ZohoAccountsAccessTokenCacheService } from '@dereekb/zoho/nestjs';
3
3
  /**
4
4
  * Creates a {@link ZohoAccountsAccessTokenCacheService} backed by Firestore {@link SystemState} documents.
@@ -7,14 +7,21 @@ import { type ZohoAccountsAccessTokenCacheService } from '@dereekb/zoho/nestjs';
7
7
  * Tokens are stored in a single {@link SystemState} document (type {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE})
8
8
  * and token updates/clears use Firestore transactions for concurrency safety.
9
9
  *
10
+ * The access token is a credential, so pass a SERVER-ONLY collection — a
11
+ * `systemStatePrivateFirestoreCollection()` from `@dereekb/firebase-server/model`, whose converter
12
+ * map registers {@link zohoAccessTokenSystemStateDataConverterFactory} and encrypts the token at
13
+ * rest. Passing an app's client-shared `systemStateCollection` still type-checks (for backwards
14
+ * compatibility) but requires the converter to be registered in the client-shared map, which drags
15
+ * `@dereekb/firebase-server` into browser builds.
16
+ *
10
17
  * @param systemStateCollection - The Firestore collection for system state documents.
11
18
  * @returns A cache service backed by Firestore system state documents.
12
19
  *
13
20
  * @example
14
21
  * ```ts
15
- * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStateCollection);
22
+ * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStatePrivateCollection);
16
23
  * const cache = cacheService.loadZohoAccessTokenCache('my-zoho-service');
17
24
  * const token = await cache.loadCachedToken();
18
25
  * ```
19
26
  */
20
- export declare function firebaseZohoAccountsAccessTokenCacheService(systemStateCollection: SystemStateFirestoreCollection): ZohoAccountsAccessTokenCacheService;
27
+ export declare function firebaseZohoAccountsAccessTokenCacheService<D extends FirestoreDocument<SystemState<SystemStateStoredData>>>(systemStateCollection: SystemStateFirestoreCollectionLike<SystemStateStoredData, D>): ZohoAccountsAccessTokenCacheService;
@@ -0,0 +1,51 @@
1
+ import { type ConfigService } from '@nestjs/config';
2
+ import { type SystemStateStoredDataConverterMap } from '@dereekb/firebase';
3
+ import { type FirebaseServerEnvService } from '@dereekb/firebase-server';
4
+ import { type AES256GCMEncryptionSecret } from '@dereekb/nestjs';
5
+ import { type ZohoAccessTokenSystemStateDataConverterConfig } from './zoho.accounts.firebase.system';
6
+ /**
7
+ * Environment variable name for the Zoho access token cache encryption secret
8
+ * (hex-encoded AES-256 key).
9
+ *
10
+ * There is NO key rotation — `firestoreEncryptedField` resolves and validates the key once at
11
+ * converter construction and closes over it. Unlike the `uecp` and `oidcJwksKey` secrets, however,
12
+ * rotating this one is SURVIVABLE: the Zoho converter supplies an `onDecodeFailure` handler, so
13
+ * every entry written under the old key simply degrades to a cache miss and the next Zoho call
14
+ * re-mints a token. Rotation costs one extra token request per service key, not an outage.
15
+ */
16
+ export declare const ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY = "ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET";
17
+ /**
18
+ * Deterministic secret used when running in a testing environment and no real secret is configured,
19
+ * so specs never need a live credential.
20
+ *
21
+ * Deliberately distinct from the OIDC JWKS and UserExternalConnection testing secrets so a leaked
22
+ * emulator blob is attributable. ("Zoho Access Token Cache Test Key", hex-encoded.)
23
+ */
24
+ export declare const TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET: AES256GCMEncryptionSecret;
25
+ /**
26
+ * Reads the Zoho access token encryption secret from the environment.
27
+ *
28
+ * @param configService - The Nest config service used to read the encryption secret.
29
+ * @param envService - Used to detect a testing environment for the secret fallback.
30
+ * @returns The validated encryption secret.
31
+ * @throws {Error} When the configured secret is invalid outside a testing environment.
32
+ */
33
+ export declare function zohoAccessTokenEncryptionSecretFactory(configService: ConfigService, envService: FirebaseServerEnvService): AES256GCMEncryptionSecret;
34
+ /**
35
+ * Builds the converter map entry for the Zoho access token cache, for use in a SERVER-ONLY
36
+ * SystemState converter map.
37
+ *
38
+ * @param config - The encryption configuration.
39
+ * @returns A partial converter map containing only the Zoho access token entry.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const collections = systemStatePrivateFirestoreCollection({
44
+ * firestoreContext,
45
+ * converters: {
46
+ * ...zohoAccessTokenSystemStatePrivateConverterEntry({ encryptionSecret })
47
+ * }
48
+ * });
49
+ * ```
50
+ */
51
+ export declare function zohoAccessTokenSystemStatePrivateConverterEntry(config: ZohoAccessTokenSystemStateDataConverterConfig): SystemStateStoredDataConverterMap;
@@ -1,5 +1,6 @@
1
1
  import { type ZohoAccessToken, type ZohoServiceAccessTokenKey } from '@dereekb/zoho';
2
- import { type FirestoreDocumentAccessor, type SystemState, type SystemStateDocument, type SystemStateStoredData, type SystemStateStoredDataFieldConverterConfig } from '@dereekb/firebase';
2
+ import { type FirestoreDocument, type FirestoreDocumentAccessor, type FirestoreModelFieldMapFunctionsConfig, type SystemState, type SystemStateDocument, type SystemStateStoredData, type SystemStateStoredDataFieldConverterConfig } from '@dereekb/firebase';
3
+ import { type AES256GCMEncryptionSecretSource } from '@dereekb/nestjs';
3
4
  import { type Configurable } from '@dereekb/util';
4
5
  /**
5
6
  * {@link SystemState} type identifier for storing Zoho access tokens in Firestore.
@@ -15,7 +16,35 @@ export interface ZohoAccessTokenSystemStateEmbeddedToken extends Configurable<Zo
15
16
  */
16
17
  key: ZohoServiceAccessTokenKey;
17
18
  }
18
- export declare const zohoAccessTokenSystemStateEmbeddedTokenConverter: import("@dereekb/firebase").FirestoreSubObjectFieldMapFunctionsConfig<ZohoAccessTokenSystemStateEmbeddedToken, Partial<import("@dereekb/util").ReplaceType<ZohoAccessTokenSystemStateEmbeddedToken, import("@dereekb/util").MaybeMap<object>, any>>>;
19
+ /**
20
+ * Configuration for the encrypted Zoho access token converters.
21
+ */
22
+ export interface ZohoAccessTokenSystemStateDataConverterConfig {
23
+ /**
24
+ * Encryption secret source for the `accessToken` field.
25
+ */
26
+ readonly encryptionSecret: AES256GCMEncryptionSecretSource;
27
+ }
28
+ /**
29
+ * Creates the embedded-token converter, encrypting the `accessToken` at rest.
30
+ *
31
+ * This is a factory rather than a module-level const because `firestoreEncryptedField` resolves and
32
+ * validates the encryption key eagerly at construction — the secret must be known at runtime.
33
+ *
34
+ * ONLY `accessToken` is encrypted, deliberately. `firestoreEncryptedField` round-trips through
35
+ * `JSON.stringify`/`JSON.parse`, so anything placed inside the encrypted blob loses its type — and
36
+ * `expiresAt` is a `Date`. Encrypting the token string alone keeps every date outside the blob,
37
+ * which is what lets {@link zohoAccessTokenSystemStateDataConverterFactory} keep filtering expired
38
+ * entries without having to decrypt them first. Do not "improve" this by encrypting the whole
39
+ * token object or the `tokens` array.
40
+ *
41
+ * Accepted trade-off: `key`, `scope`, `apiDomain`, `expiresIn` and `expiresAt` remain plaintext at
42
+ * rest. None of them is a credential.
43
+ *
44
+ * @param config - The encryption configuration.
45
+ * @returns The embedded token field converter.
46
+ */
47
+ export declare function zohoAccessTokenSystemStateEmbeddedTokenConverterFactory(config: ZohoAccessTokenSystemStateDataConverterConfig): FirestoreModelFieldMapFunctionsConfig<ZohoAccessTokenSystemStateEmbeddedToken, any>;
19
48
  /**
20
49
  * Data shape stored within a {@link SystemState} document for caching multiple Zoho access tokens.
21
50
  *
@@ -33,13 +62,17 @@ export interface ZohoAccessTokenSystemStateData extends SystemStateStoredData {
33
62
  lat: Date;
34
63
  }
35
64
  /**
36
- * Firestore field converter for {@link ZohoAccessTokenSystemStateData}.
65
+ * Creates the {@link ZohoAccessTokenSystemStateData} converter, encrypting each token's
66
+ * `accessToken` at rest.
67
+ *
68
+ * Register the result in a SERVER-ONLY converter map under {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} —
69
+ * see `systemStatePrivateFirestoreCollection()` in `@dereekb/firebase-server/model`. It must never be
70
+ * registered in an app's client-shared `SystemStateStoredDataConverterMap`.
37
71
  *
38
- * Automatically filters out expired tokens on read and enforces uniqueness by service key.
39
- * Must be registered in the app's {@link SystemStateStoredDataConverterMap} under
40
- * the {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} key.
72
+ * @param config - The encryption configuration.
73
+ * @returns The stored-data field converter.
41
74
  */
42
- export declare const zohoAccessTokenSystemStateDataConverter: SystemStateStoredDataFieldConverterConfig<ZohoAccessTokenSystemStateData>;
75
+ export declare function zohoAccessTokenSystemStateDataConverterFactory(config: ZohoAccessTokenSystemStateDataConverterConfig): SystemStateStoredDataFieldConverterConfig<ZohoAccessTokenSystemStateData>;
43
76
  /**
44
77
  * Loads the {@link SystemStateDocument} that stores {@link ZohoAccessTokenSystemStateData},
45
78
  * using {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} as the document ID.
@@ -53,4 +86,14 @@ export declare const zohoAccessTokenSystemStateDataConverter: SystemStateStoredD
53
86
  * const data = await doc.snapshotData();
54
87
  * ```
55
88
  */
56
- export declare function loadZohoAccessTokenSystemState(accessor: FirestoreDocumentAccessor<SystemState<SystemStateStoredData>, SystemStateDocument<SystemStateStoredData>>): SystemStateDocument<ZohoAccessTokenSystemStateData>;
89
+ export declare function loadZohoAccessTokenSystemState<D extends FirestoreDocument<SystemState<SystemStateStoredData>>>(accessor: FirestoreDocumentAccessor<SystemState<SystemStateStoredData>, D>): SystemStateDocument<ZohoAccessTokenSystemStateData>;
90
+ /**
91
+ * @deprecated stores the access token in PLAINTEXT. Use
92
+ * {@link zohoAccessTokenSystemStateEmbeddedTokenConverterFactory} instead, which encrypts it at rest.
93
+ */
94
+ export declare const zohoAccessTokenSystemStateEmbeddedTokenConverter: import("@dereekb/firebase").FirestoreSubObjectFieldMapFunctionsConfig<ZohoAccessTokenSystemStateEmbeddedToken, Partial<import("@dereekb/util").ReplaceType<ZohoAccessTokenSystemStateEmbeddedToken, import("@dereekb/util").MaybeMap<object>, any>>>;
95
+ /**
96
+ * @deprecated stores access tokens in PLAINTEXT. Use {@link zohoAccessTokenSystemStateDataConverterFactory}
97
+ * instead, and register it on a server-only SystemStatePrivate collection.
98
+ */
99
+ export declare const zohoAccessTokenSystemStateDataConverter: SystemStateStoredDataFieldConverterConfig<ZohoAccessTokenSystemStateData>;
@@ -0,0 +1,45 @@
1
+ import { type ZohoAccessToken, type ZohoAccessTokenCache } from '@dereekb/zoho';
2
+ import { type FirebaseAuthUserId } from '@dereekb/firebase';
3
+ import { type UserExternalConnectionAccessor, type UserExternalConnectionCredentials, type UserExternalConnectionCredentialsWriter } from '@dereekb/firebase-server/model';
4
+ import { type Maybe } from '@dereekb/util';
5
+ /**
6
+ * Maps stored connection credentials to a {@link ZohoAccessToken}.
7
+ *
8
+ * @param credentials - The credentials stored for the `zoho` provider.
9
+ * @returns The equivalent Zoho access token, or null when the credentials cannot form one.
10
+ */
11
+ export declare function zohoAccessTokenFromUserExternalConnectionCredentials(credentials: Maybe<UserExternalConnectionCredentials>): Maybe<ZohoAccessToken>;
12
+ /**
13
+ * Configuration for {@link userExternalConnectionZohoAccessTokenCache}.
14
+ */
15
+ export interface UserExternalConnectionZohoAccessTokenCacheConfig {
16
+ /**
17
+ * Used to read the user's currently stored Zoho credentials.
18
+ *
19
+ * The accessor rather than `UserExternalConnectionReader`: the contract below is explicitly that an
20
+ * EXPIRED token may be returned, so a surface that could refresh would be the wrong tool — see
21
+ * `loadCachedToken`.
22
+ */
23
+ readonly accessor: UserExternalConnectionAccessor;
24
+ /**
25
+ * Used to persist a renewed token back onto the connection pair, and to record a cleared one.
26
+ */
27
+ readonly actions: UserExternalConnectionCredentialsWriter;
28
+ readonly uid: FirebaseAuthUserId;
29
+ }
30
+ /**
31
+ * Creates a {@link ZohoAccessTokenCache} backed by a user's UserExternalConnection pair.
32
+ *
33
+ * The Zoho counterpart of `userExternalConnectionCalcomAccessTokenCache`, and the per-user counterpart
34
+ * of `firebaseZohoAccountsAccessTokenCacheService` — which caches the APP's token in a `SystemState`
35
+ * document and has no notion of a user.
36
+ *
37
+ * Zoho does not rotate refresh tokens, so unlike Cal.com there is no token here that is destroyed by
38
+ * being used. What this buys instead is that a renewed access token is shared: without it every Cloud
39
+ * Function instance holds its own in-memory token and refreshes independently, so a user's grant is
40
+ * exercised once per instance per hour rather than once per hour.
41
+ *
42
+ * @param config - The accessor, the actions, and the user the cache is for.
43
+ * @returns A ZohoAccessTokenCache reading and writing the user's connection pair.
44
+ */
45
+ export declare function userExternalConnectionZohoAccessTokenCache(config: UserExternalConnectionZohoAccessTokenCacheConfig): ZohoAccessTokenCache;
@@ -0,0 +1,80 @@
1
+ import { type ZohoAccountsConfigApiUrlInput, type ZohoOAuthScope } from '@dereekb/zoho';
2
+ import { type FirebaseServerEnvService } from '@dereekb/firebase-server';
3
+ import { UserExternalConnectionOAuthServiceConfig } from '@dereekb/firebase-server/model';
4
+ import { type Maybe } from '@dereekb/util';
5
+ /**
6
+ * Controller path the Zoho external-connection OAuth endpoints are mounted at.
7
+ *
8
+ * Derived from the framework's path factory, the same expression the redirect URI and the
9
+ * global-prefix exclusion are built from, so the three cannot drift apart.
10
+ */
11
+ export declare const ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH: string;
12
+ /**
13
+ * Routes to exclude from an app's global API route prefix so the Zoho callback controller stays
14
+ * mounted at `/oauth/zoho/*`.
15
+ *
16
+ * Spread this into the `exclude` list of the app's `globalApiRoutePrefix` config, alongside
17
+ * `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`.
18
+ */
19
+ export declare const ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE: string[];
20
+ /**
21
+ * The scopes requested when an app does not declare its own.
22
+ *
23
+ * Declared in code, deliberately NOT read from the environment: the set to request follows from what
24
+ * the integration actually does, so it belongs where it is reviewable.
25
+ *
26
+ * Least privilege for a connect that proves the handoff and labels the connection: read the
27
+ * authorizing user's Zoho identity, and nothing else. No `ZohoCRM.*` / `ZohoRecruit.*` scope is
28
+ * requested because the default integration makes no product API call — requesting one would be
29
+ * privilege the code never uses, which is the same error as under-requesting, in the other
30
+ * direction. An app that actually calls a Zoho product declares its own set on the module metadata,
31
+ * in code.
32
+ *
33
+ * Unlike Cal.com, Zoho does not pre-register scopes on the OAuth client, so nothing has to be
34
+ * registered in the API console to request this.
35
+ */
36
+ export declare const DEFAULT_ZOHO_OAUTH_SCOPES: readonly ZohoOAuthScope[];
37
+ /**
38
+ * Configuration for the {@link ZohoUserExternalConnectionOAuthService}.
39
+ *
40
+ * Extends the framework config with what is Zoho's own: which scopes to request, and which
41
+ * datacenter to authorize against.
42
+ */
43
+ export declare abstract class ZohoUserExternalConnectionOAuthServiceConfig extends UserExternalConnectionOAuthServiceConfig {
44
+ readonly scopes: readonly ZohoOAuthScope[];
45
+ /**
46
+ * Datacenter to authorize against. Defaults to the api's configured one.
47
+ */
48
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
49
+ }
50
+ export interface ZohoUserExternalConnectionOAuthServiceConfigFactoryConfig {
51
+ readonly envService: FirebaseServerEnvService;
52
+ /**
53
+ * Path on the app URL the user is returned to after connecting, e.g. `/app/settings`.
54
+ */
55
+ readonly successPath: string;
56
+ /**
57
+ * Path on the app URL the user is returned to after a failed connect. Defaults to `successPath`.
58
+ */
59
+ readonly failurePath?: Maybe<string>;
60
+ /**
61
+ * The scopes to request. Defaults to {@link DEFAULT_ZOHO_OAUTH_SCOPES}.
62
+ */
63
+ readonly scopes?: Maybe<readonly ZohoOAuthScope[]>;
64
+ /**
65
+ * Datacenter to authorize against. Defaults to the api's configured one.
66
+ */
67
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
68
+ }
69
+ /**
70
+ * Builds the Zoho connect flow's configuration from the app's configured origins.
71
+ *
72
+ * Nothing here is read from the environment as a value: the redirect URI is derived from the app's
73
+ * OAuth origin plus the mounted controller path, and the return URLs from the app URL plus
74
+ * code-declared paths. Registering Zoho therefore adds no deployment configuration beyond the client
75
+ * credentials the OAuth api already reads.
76
+ *
77
+ * @param config - The env service, the return paths, and the optional scope/datacenter overrides.
78
+ * @returns The validated service configuration.
79
+ */
80
+ export declare function zohoUserExternalConnectionOAuthServiceConfigFactory(config: ZohoUserExternalConnectionOAuthServiceConfigFactoryConfig): ZohoUserExternalConnectionOAuthServiceConfig;
@@ -0,0 +1,18 @@
1
+ import { AbstractUserExternalConnectionOAuthController } from '@dereekb/firebase-server/model';
2
+ import { ZohoUserExternalConnectionOAuthService } from './zoho.oauth.connection.service';
3
+ /**
4
+ * Endpoints for the Zoho external-connection authorization-code handoff.
5
+ *
6
+ * Mounted at `/oauth/zoho`, matching the Angular registry's default authorize path of
7
+ * `/oauth/<providerType>/authorize`. Hosting rewrites do not strip the path, so this prefix is the
8
+ * public path — but an app with a global API route prefix must ALSO exclude these routes from it via
9
+ * {@link ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE}, or they land under
10
+ * that prefix instead and no longer match the redirect URI registered with Zoho.
11
+ *
12
+ * The `authorize` and `callback` routes come from the base class, so this declares only where they
13
+ * mount and which service serves them.
14
+ */
15
+ export declare class ZohoUserExternalConnectionOAuthController extends AbstractUserExternalConnectionOAuthController {
16
+ readonly oauthService: ZohoUserExternalConnectionOAuthService;
17
+ constructor(oauthService: ZohoUserExternalConnectionOAuthService);
18
+ }
@@ -0,0 +1,43 @@
1
+ import { type ModuleMetadata } from '@nestjs/common';
2
+ import { type ZohoAccountsConfigApiUrlInput, type ZohoOAuthScope } from '@dereekb/zoho';
3
+ import { type Maybe } from '@dereekb/util';
4
+ export interface ProvideAppZohoUserExternalConnectionOAuthMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
5
+ /**
6
+ * This module requires the following dependencies in order to initialize properly:
7
+ * - ZohoAccountsOAuthApi
8
+ *
9
+ * This module declaration makes it easier to import a module that exports that dependency.
10
+ */
11
+ readonly dependencyModule?: Maybe<Required<ModuleMetadata>['imports']['0']>;
12
+ /**
13
+ * Path on the app URL the user is returned to after connecting, e.g. `/app/settings`.
14
+ */
15
+ readonly successPath: string;
16
+ /**
17
+ * Path on the app URL the user is returned to after a failed connect. Defaults to `successPath`.
18
+ */
19
+ readonly failurePath?: Maybe<string>;
20
+ /**
21
+ * The scopes to request. Defaults to `DEFAULT_ZOHO_OAUTH_SCOPES`.
22
+ */
23
+ readonly scopes?: Maybe<readonly ZohoOAuthScope[]>;
24
+ /**
25
+ * Datacenter to authorize against. Defaults to the api's configured one.
26
+ */
27
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
28
+ }
29
+ /**
30
+ * Convenience function used to generate ModuleMetadata for an app's Zoho external-connection OAuth
31
+ * module.
32
+ *
33
+ * Opt-in: importing the Zoho OAuth module alone never mounts HTTP routes, so an app that only makes
34
+ * outbound Zoho calls exposes no endpoints.
35
+ *
36
+ * The importing module must also supply `UserExternalConnectionServerActions` and
37
+ * `UserExternalConnectionStateCoder` — both exported by `appUserExternalConnectionModuleMetadata`,
38
+ * so pass that module in `imports`.
39
+ *
40
+ * @param config - The module metadata configuration.
41
+ * @returns NestJS ModuleMetadata mounting the Zoho connect endpoints.
42
+ */
43
+ export declare function appZohoUserExternalConnectionOAuthModuleMetadata(config: ProvideAppZohoUserExternalConnectionOAuthMetadataConfig): ModuleMetadata;
@@ -0,0 +1,107 @@
1
+ import { type ZohoAccountsApiUrl, type ZohoAccountsAuthorizeUrlFactory, type ZohoAccountsRefreshTokenFromAuthorizationCodeResponse, type ZohoAccountsUserInfoResponse } from '@dereekb/zoho';
2
+ import { ZohoAccountsOAuthApi } from '@dereekb/zoho/nestjs';
3
+ import { AbstractUserExternalConnectionOAuthService, UserExternalConnectionAccessor, UserExternalConnectionServerActions, UserExternalConnectionStateCoder, type UserExternalConnectionCredentials, type UserExternalConnectionOAuthCallbackQueryValues, type UserExternalConnectionOAuthExchangeInput, type UserExternalConnectionOAuthRefreshCredentialsInput, type UserExternalConnectionOAuthState } from '@dereekb/firebase-server/model';
4
+ import { type Maybe, type WebsiteUrl } from '@dereekb/util';
5
+ import { ZohoUserExternalConnectionOAuthServiceConfig } from './zoho.oauth.connection.config';
6
+ /**
7
+ * The callback parameter naming the datacenter whose accounts server issued the code.
8
+ */
9
+ export declare const ZOHO_OAUTH_CALLBACK_ACCOUNTS_SERVER_PARAM = "accounts-server";
10
+ /**
11
+ * The callback parameter naming the datacenter's short location code, e.g. `us`.
12
+ */
13
+ export declare const ZOHO_OAUTH_CALLBACK_LOCATION_PARAM = "location";
14
+ /**
15
+ * `extra` key holding the api domain a Zoho access token is usable against.
16
+ *
17
+ * Named constants because these keys are written on connect and read back on refresh — a literal in
18
+ * one place and a typo in the other would silently send the refresh to the wrong datacenter.
19
+ */
20
+ export declare const ZOHO_EXTRA_API_DOMAIN_KEY = "apiDomain";
21
+ /**
22
+ * `extra` key holding the accounts host a later refresh must be sent to.
23
+ */
24
+ export declare const ZOHO_EXTRA_ACCOUNTS_SERVER_KEY = "accountsServer";
25
+ /**
26
+ * `extra` key holding Zoho's short location code for the datacenter.
27
+ */
28
+ export declare const ZOHO_EXTRA_LOCATION_KEY = "location";
29
+ export interface ZohoUserExternalConnectionCredentialsInput {
30
+ /**
31
+ * The token response the exchange returned.
32
+ */
33
+ readonly response: ZohoAccountsRefreshTokenFromAuthorizationCodeResponse;
34
+ /**
35
+ * The accounts host the code was exchanged against.
36
+ */
37
+ readonly accountsApiUrl: ZohoAccountsApiUrl;
38
+ /**
39
+ * Zoho's short location code for that datacenter, when the callback carried one.
40
+ */
41
+ readonly location?: Maybe<string>;
42
+ /**
43
+ * The Zoho account the token belongs to, when one could be read.
44
+ *
45
+ * Optional because the identity call is best-effort: a connection is fully usable unlabeled.
46
+ */
47
+ readonly userInfo?: Maybe<ZohoAccountsUserInfoResponse>;
48
+ }
49
+ /**
50
+ * Maps an exchanged Zoho token response to the credentials stored on the private connection document.
51
+ *
52
+ * Two things differ from Cal.com's mapper. Zoho does not rotate its refresh token, so there is
53
+ * nothing to prefer over the token we already hold — and `refresh_token` can be ABSENT entirely on a
54
+ * re-consent, which is why it is passed through as `Maybe` rather than asserted (the framework's
55
+ * `credentialsRetainingStoredRefreshToken` is what keeps that from destroying a working token). And
56
+ * `api_domain` / `accountsServer` / `location` are retained in `extra`: a Zoho access token is only
57
+ * usable against the api domain it was issued for, and a later refresh must go back to the same
58
+ * datacenter's accounts server, so dropping them would leave the stored credentials unusable.
59
+ *
60
+ * @param input - The token response, the host it came from, and the identity when one was read.
61
+ * @returns The credentials to store.
62
+ */
63
+ export declare function zohoUserExternalConnectionCredentials(input: ZohoUserExternalConnectionCredentialsInput): UserExternalConnectionCredentials;
64
+ /**
65
+ * Zoho's half of the external-connection authorization-code handoff.
66
+ *
67
+ * Everything else — resolving who is connecting, surfacing a refusal, retaining a refresh token the
68
+ * exchange did not return, persisting the credentials, choosing the redirect — is the framework's.
69
+ */
70
+ export declare class ZohoUserExternalConnectionOAuthService extends AbstractUserExternalConnectionOAuthService {
71
+ readonly config: ZohoUserExternalConnectionOAuthServiceConfig;
72
+ readonly stateCoder: UserExternalConnectionStateCoder;
73
+ readonly userExternalConnectionActions: UserExternalConnectionServerActions;
74
+ readonly userExternalConnectionAccessor: UserExternalConnectionAccessor;
75
+ readonly oauthApi: ZohoAccountsOAuthApi;
76
+ readonly authorizeUrlFactory: ZohoAccountsAuthorizeUrlFactory;
77
+ /**
78
+ * The accounts host this app authorizes against, and the fallback for an exchange whose callback
79
+ * named no usable one.
80
+ */
81
+ readonly accountsApiUrl: ZohoAccountsApiUrl;
82
+ constructor(config: ZohoUserExternalConnectionOAuthServiceConfig, stateCoder: UserExternalConnectionStateCoder, userExternalConnectionActions: UserExternalConnectionServerActions, userExternalConnectionAccessor: UserExternalConnectionAccessor, oauthApi: ZohoAccountsOAuthApi);
83
+ protected authorizeUrlForState(state: UserExternalConnectionOAuthState): WebsiteUrl;
84
+ protected credentialsForAuthorizationCode(input: UserExternalConnectionOAuthExchangeInput): Promise<UserExternalConnectionCredentials>;
85
+ refreshCredentials(input: UserExternalConnectionOAuthRefreshCredentialsInput): Promise<UserExternalConnectionCredentials>;
86
+ /**
87
+ * The accounts host to exchange against, taken from Zoho's `accounts-server` callback parameter.
88
+ *
89
+ * @param query - The raw callback query.
90
+ * @returns The allowlisted accounts host the callback named, if any.
91
+ */
92
+ protected accountsApiUrlForCallbackQuery(query: Maybe<UserExternalConnectionOAuthCallbackQueryValues>): Maybe<ZohoAccountsApiUrl>;
93
+ /**
94
+ * Resolves an untrusted accounts-host string back to one of the canonical Zoho hosts.
95
+ *
96
+ * Only an allowlisted Zoho host is honored. Every value passed here originated on a redirect an
97
+ * attacker can compose — either the live callback query or a value persisted from an earlier one —
98
+ * and it becomes the POST target the CLIENT SECRET is sent to, so an unchecked value would hand out
99
+ * the secret. Resolving through the allowlist rather than comparing to it also guarantees the host
100
+ * used is a canonical constant and never a caller-shaped variant of one. An unrecognized value is
101
+ * dropped (and logged) rather than trusted.
102
+ *
103
+ * @param accountsServer - The untrusted host string, if any.
104
+ * @returns The allowlisted accounts host, or null when there was none or it was not recognized.
105
+ */
106
+ protected allowlistedAccountsApiUrl(accountsServer: Maybe<string>): Maybe<ZohoAccountsApiUrl>;
107
+ }