@dereekb/zoho 13.32.0 → 13.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import { type ZohoAccessTokenString, type ZohoAccountsAccessTokenResponse, type ZohoAccountsApiUrl, type ZohoAccountsConfigApiUrlInput, type ZohoAccountsOAuthClientContext, type ZohoAccountsRefreshTokenFromAuthorizationCodeResponse, type ZohoAccountsUserInfoResponse, type ZohoAuthorizationCode, type ZohoOAuthClientId, type ZohoRefreshToken } from '@dereekb/zoho';
2
+ import { type Maybe, type WebsiteUrl } from '@dereekb/util';
3
+ import { ZohoAccountsOAuthServiceConfig } from './accounts.oauth.config';
4
+ export interface ZohoAccountsExchangeAuthorizationCodeInput {
5
+ /**
6
+ * The single-use authorization code obtained from the Zoho consent flow.
7
+ */
8
+ readonly code: ZohoAuthorizationCode;
9
+ /**
10
+ * The redirect URI the code was issued against. Must match the authorize request byte-for-byte.
11
+ */
12
+ readonly redirectUri: WebsiteUrl;
13
+ /**
14
+ * The accounts host to exchange against. Defaults to the configured datacenter.
15
+ */
16
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
17
+ }
18
+ export interface ZohoAccountsUserInfoApiInput {
19
+ /**
20
+ * The access token to read the identity of.
21
+ */
22
+ readonly accessToken: ZohoAccessTokenString;
23
+ /**
24
+ * The accounts host to read from. Defaults to the configured datacenter.
25
+ */
26
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
27
+ }
28
+ export interface ZohoAccountsRefreshUserAccessTokenInput {
29
+ /**
30
+ * The user's refresh token.
31
+ */
32
+ readonly refreshToken: ZohoRefreshToken;
33
+ /**
34
+ * The accounts host to refresh against. Defaults to the configured datacenter.
35
+ *
36
+ * Should be the datacenter the grant was created at — a refresh token issued by one datacenter is
37
+ * not honored by another.
38
+ */
39
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
40
+ }
41
+ /**
42
+ * The Zoho Accounts endpoints a per-user connect flow needs: the authorization-code exchange, the
43
+ * per-user token refresh, and the identity lookup that labels the connection.
44
+ *
45
+ * Deliberately separate from {@link ZohoAccountsApi}, which requires a server refresh token and a
46
+ * token cache. A per-user handoff has neither — the handoff is how a refresh token is obtained.
47
+ */
48
+ export declare class ZohoAccountsOAuthApi {
49
+ readonly config: ZohoAccountsOAuthServiceConfig;
50
+ private readonly _clientFactory;
51
+ /**
52
+ * Per-host clients, memoized so a repeated datacenter reuses one client rather than rebuilding
53
+ * its fetch stack per callback.
54
+ */
55
+ private readonly _clients;
56
+ /**
57
+ * The accounts host configured for this app.
58
+ */
59
+ readonly apiUrl: ZohoAccountsApiUrl;
60
+ /**
61
+ * The OAuth client id the authorize request is composed with.
62
+ */
63
+ readonly clientId: ZohoOAuthClientId;
64
+ private readonly _clientSecret;
65
+ constructor(config: ZohoAccountsOAuthServiceConfig);
66
+ /**
67
+ * The client for the configured datacenter.
68
+ */
69
+ get oauthClientContext(): ZohoAccountsOAuthClientContext;
70
+ /**
71
+ * The client for a specific accounts host, memoized per host.
72
+ *
73
+ * Zoho echoes the issuing datacenter back as `accounts-server` on the callback, and a code issued
74
+ * by one datacenter cannot be exchanged at another. Callers MUST have checked the host against
75
+ * `isKnownZohoAccountsApiUrl` first — this method will happily build a client for any URL, and the
76
+ * client secret travels to whatever host it is given.
77
+ *
78
+ * @param apiUrl - The datacenter key or full accounts URL to build a client for.
79
+ * @returns The memoized client context for that host.
80
+ */
81
+ oauthClientContextForApiUrl(apiUrl: ZohoAccountsConfigApiUrlInput): ZohoAccountsOAuthClientContext;
82
+ /**
83
+ * Exchanges a single-use authorization code for tokens.
84
+ *
85
+ * @param input - The code, the redirect URI it was issued against, and the optional accounts host.
86
+ * @returns The Zoho token response. `refresh_token` may be absent on a re-consent.
87
+ */
88
+ exchangeAuthorizationCode(input: ZohoAccountsExchangeAuthorizationCodeInput): Promise<ZohoAccountsRefreshTokenFromAuthorizationCodeResponse>;
89
+ /**
90
+ * Exchanges a user's refresh token for a new access token.
91
+ *
92
+ * Zoho does not rotate refresh tokens, so the token passed in stays valid and the response carries
93
+ * no replacement — the caller keeps the one it already has. The response DOES carry `api_domain`,
94
+ * which is the host the new access token is usable against, so persist it.
95
+ *
96
+ * @param input - The user's refresh token and the optional accounts host.
97
+ * @returns The Zoho access token response.
98
+ */
99
+ refreshUserAccessToken(input: ZohoAccountsRefreshUserAccessTokenInput): Promise<ZohoAccountsAccessTokenResponse>;
100
+ /**
101
+ * Reads the Zoho identity an access token was issued to.
102
+ *
103
+ * @param input - The access token and the optional accounts host.
104
+ * @returns The user info response.
105
+ */
106
+ userInfo(input: ZohoAccountsUserInfoApiInput): Promise<ZohoAccountsUserInfoResponse>;
107
+ }
@@ -0,0 +1,45 @@
1
+ import { type ZohoAccountsConfigApiUrlInput, type ZohoAccountsOAuthClientFactoryConfig, type ZohoOAuthClientId, type ZohoOAuthClientSecret } from '@dereekb/zoho';
2
+ import { type Maybe } from '@dereekb/util';
3
+ import { type ConfigService } from '@nestjs/config';
4
+ /**
5
+ * Environment key naming the Zoho Accounts datacenter (or a full accounts URL) to authorize against.
6
+ */
7
+ export declare const ZOHO_ACCOUNTS_URL_CONFIG_KEY = "ZOHO_ACCOUNTS_URL";
8
+ /**
9
+ * Environment key holding the Zoho OAuth client id.
10
+ */
11
+ export declare const ZOHO_ACCOUNTS_CLIENT_ID_CONFIG_KEY = "ZOHO_ACCOUNTS_CLIENT_ID";
12
+ /**
13
+ * Environment key holding the Zoho OAuth client secret.
14
+ */
15
+ export declare const ZOHO_ACCOUNTS_CLIENT_SECRET_CONFIG_KEY = "ZOHO_ACCOUNTS_CLIENT_SECRET";
16
+ export interface ZohoAccountsOAuthServiceApiConfig {
17
+ readonly clientId?: Maybe<ZohoOAuthClientId>;
18
+ readonly clientSecret?: Maybe<ZohoOAuthClientSecret>;
19
+ /**
20
+ * The datacenter (or full accounts URL) to authorize and exchange against. Defaults to `us`.
21
+ */
22
+ readonly apiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
23
+ }
24
+ /**
25
+ * Configuration for {@link ZohoAccountsOAuthApi}.
26
+ *
27
+ * Read from the three existing flat `ZOHO_ACCOUNTS_*` keys rather than composed through
28
+ * `zohoConfigServiceReaderFunction`, which requires a `serviceAccessTokenKey` a per-user OAuth
29
+ * client does not have. Notably absent: `ZOHO_ACCOUNTS_REFRESH_TOKEN` (a per-user flow has no
30
+ * server refresh token) and any scope variable (scopes are declared in code).
31
+ */
32
+ export declare abstract class ZohoAccountsOAuthServiceConfig {
33
+ readonly zohoAccountsOAuth: ZohoAccountsOAuthServiceApiConfig;
34
+ readonly factoryConfig?: ZohoAccountsOAuthClientFactoryConfig;
35
+ static assertValidConfig(config: ZohoAccountsOAuthServiceConfig): void;
36
+ }
37
+ /**
38
+ * Factory function that creates a {@link ZohoAccountsOAuthServiceConfig} from NestJS ConfigService
39
+ * environment variables.
40
+ *
41
+ * @param configService - The NestJS ConfigService instance.
42
+ * @returns A validated ZohoAccountsOAuthServiceConfig.
43
+ * @throws {Error} When the client id or client secret is not configured.
44
+ */
45
+ export declare function zohoAccountsOAuthServiceConfigFactory(configService: ConfigService): ZohoAccountsOAuthServiceConfig;
@@ -0,0 +1,22 @@
1
+ import { type ModuleMetadata } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { ZohoAccountsOAuthServiceConfig } from './accounts.oauth.config';
4
+ export type ZohoAccountsOAuthServiceConfigFactory = (configService: ConfigService) => ZohoAccountsOAuthServiceConfig;
5
+ export interface ProvideAppZohoAccountsOAuthMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
6
+ /**
7
+ * Optional override for the ZohoAccountsOAuthServiceConfigFactory.
8
+ *
9
+ * @default zohoAccountsOAuthServiceConfigFactory
10
+ */
11
+ readonly zohoAccountsOAuthServiceConfigFactory?: ZohoAccountsOAuthServiceConfigFactory;
12
+ }
13
+ /**
14
+ * Convenience function used to generate ModuleMetadata for an app's ZohoAccountsOAuthModule.
15
+ *
16
+ * Unlike `appZohoCrmModuleMetadata`, this needs no `ZohoAccountsAccessTokenCacheService` and
17
+ * therefore no dependency module: a per-user connect flow has no server token to cache.
18
+ *
19
+ * @param config - The module metadata configuration including an optional config factory.
20
+ * @returns NestJS ModuleMetadata for registering the ZohoAccountsOAuthApi.
21
+ */
22
+ export declare function appZohoAccountsOAuthModuleMetadata(config: ProvideAppZohoAccountsOAuthMetadataConfig): ModuleMetadata;
@@ -1,3 +1,6 @@
1
1
  export * from './accounts.api';
2
2
  export * from './accounts.config';
3
+ export * from './accounts.oauth.api';
4
+ export * from './accounts.oauth.config';
5
+ export * from './accounts.oauth.module';
3
6
  export * from './accounts.service';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/zoho",
3
- "version": "13.32.0",
3
+ "version": "13.33.0",
4
4
  "bin": {
5
5
  "zoho-cli": "cli/index.js"
6
6
  },
@@ -23,13 +23,13 @@
23
23
  }
24
24
  },
25
25
  "peerDependencies": {
26
- "@dereekb/date": "13.32.0",
27
- "@dereekb/dbx-cli": "13.32.0",
28
- "@dereekb/firebase": "13.32.0",
29
- "@dereekb/model": "13.32.0",
30
- "@dereekb/nestjs": "13.32.0",
31
- "@dereekb/rxjs": "13.32.0",
32
- "@dereekb/util": "13.32.0",
26
+ "@dereekb/date": "13.33.0",
27
+ "@dereekb/dbx-cli": "13.33.0",
28
+ "@dereekb/firebase": "13.33.0",
29
+ "@dereekb/model": "13.33.0",
30
+ "@dereekb/nestjs": "13.33.0",
31
+ "@dereekb/rxjs": "13.33.0",
32
+ "@dereekb/util": "13.33.0",
33
33
  "@nestjs/common": "^11.1.19",
34
34
  "@nestjs/config": "^4.0.4",
35
35
  "express": "^5.2.1",
@@ -1,9 +1,22 @@
1
- import { type FetchJsonBody, type FetchJsonInput } from '@dereekb/util/fetch';
1
+ import { type FetchJsonBody, type FetchJsonFunction, type FetchJsonInput } from '@dereekb/util/fetch';
2
2
  import { type ZohoAccountsContext } from './accounts.config';
3
3
  import { type ZohoAuthClientIdAndSecretPair, type ZohoRefreshToken } from '../zoho.config';
4
4
  import { type ZohoAccessTokenApiDomain, type ZohoAccessTokenScopesString, type ZohoAccessTokenString } from './accounts';
5
5
  import { type Maybe, type Seconds } from '@dereekb/util';
6
6
  import { type ZohoAccountsAccessTokenErrorCode } from './accounts.error.api';
7
+ import { type ZohoOAuthScope } from './accounts.authorize';
8
+ /**
9
+ * The parts of a {@link ZohoAccountsContext} an authorization-code exchange actually needs.
10
+ *
11
+ * Narrower than ZohoAccountsContext on purpose: the exchange is how a refresh token is OBTAINED, so
12
+ * requiring a context that already has one (as `zohoAccountsFactory` does) is circular for a
13
+ * per-user connect flow. A full `ZohoAccountsContext` remains structurally assignable to this, so
14
+ * every existing caller is unaffected.
15
+ */
16
+ export interface ZohoAccountsOAuthClientContext {
17
+ readonly fetchJson: FetchJsonFunction;
18
+ readonly config: ZohoAuthClientIdAndSecretPair;
19
+ }
7
20
  /**
8
21
  * Optional overrides for the access token request. When omitted, values
9
22
  * are read from the {@link ZohoAccountsContext}'s config.
@@ -80,6 +93,53 @@ export interface ZohoAccountsAccessTokenErrorResponse {
80
93
  * ```
81
94
  */
82
95
  export declare function zohoAccountsAccessToken(context: ZohoAccountsContext): (input?: ZohoAccountsAccessTokenInput) => Promise<ZohoAccountsAccessTokenResponse>;
96
+ /**
97
+ * Input for exchanging a specific user's refresh token for a new access token.
98
+ *
99
+ * Unlike {@link ZohoAccountsAccessTokenInput}, the refresh token is REQUIRED: there is no config-level
100
+ * token to fall back to, because the whole point is that the token belongs to a user rather than to
101
+ * the app.
102
+ */
103
+ export interface ZohoAccountsUserAccessTokenInput {
104
+ /**
105
+ * The user's long-lived refresh token.
106
+ */
107
+ readonly refreshToken: ZohoRefreshToken;
108
+ /**
109
+ * Override client credentials. Falls back to the context config's `clientId`/`clientSecret`.
110
+ */
111
+ readonly client?: Maybe<ZohoAuthClientIdAndSecretPair>;
112
+ }
113
+ /**
114
+ * Exchanges a user's refresh token for a new access token.
115
+ */
116
+ export type ZohoAccountsUserAccessTokenFunction = (input: ZohoAccountsUserAccessTokenInput) => Promise<ZohoAccountsAccessTokenResponse>;
117
+ /**
118
+ * Creates a function that exchanges a SPECIFIC USER's refresh token for a new access token.
119
+ *
120
+ * The per-user counterpart of {@link zohoAccountsAccessToken}, and narrowed the same way
121
+ * {@link zohoAccountsRefreshTokenFromAuthorizationCode} is: it takes a
122
+ * {@link ZohoAccountsOAuthClientContext} rather than a full {@link ZohoAccountsContext}. That matters
123
+ * because a `ZohoAccountsContext`'s config REQUIRES a `refreshToken` — the app's own — so it cannot
124
+ * describe a client that refreshes on behalf of many users. A full context stays structurally
125
+ * assignable, so this is usable from either.
126
+ *
127
+ * Zoho does not rotate refresh tokens and its refresh response carries NO `refresh_token`, so the
128
+ * token passed in here stays valid and must be retained by the caller. The response does carry
129
+ * `api_domain`, which can differ from the one the grant was created with, so persist it.
130
+ *
131
+ * @param context - A Zoho Accounts client context providing fetch and client credentials.
132
+ * @returns Function that exchanges a user's refresh token for an access token.
133
+ *
134
+ * @see https://www.zoho.com/accounts/protocol/oauth/web-apps/access-token-expiry.html
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const userAccessToken = zohoAccountsUserAccessToken(accountsClientContext);
139
+ * const { access_token, api_domain, expires_in } = await userAccessToken({ refreshToken: user.zohoRefreshToken });
140
+ * ```
141
+ */
142
+ export declare function zohoAccountsUserAccessToken(context: ZohoAccountsOAuthClientContext): ZohoAccountsUserAccessTokenFunction;
83
143
  /**
84
144
  * OAuth authorization code received from the Zoho authorization server
85
145
  * after the user grants consent. Single-use and short-lived.
@@ -166,7 +226,57 @@ export type ZohoAccountsRefreshTokenFromAuthorizationCodeFunction = (input: Zoho
166
226
  * });
167
227
  * ```
168
228
  */
169
- export declare function zohoAccountsRefreshTokenFromAuthorizationCode(context: ZohoAccountsContext): ZohoAccountsRefreshTokenFromAuthorizationCodeFunction;
229
+ export declare function zohoAccountsRefreshTokenFromAuthorizationCode(context: ZohoAccountsOAuthClientContext): ZohoAccountsRefreshTokenFromAuthorizationCodeFunction;
230
+ /**
231
+ * Path of the Zoho Accounts endpoint that describes the authorizing user.
232
+ */
233
+ export declare const ZOHO_ACCOUNTS_USER_INFO_PATH = "/oauth/user/info";
234
+ /**
235
+ * Scope required to read {@link zohoAccountsUserInfo}.
236
+ */
237
+ export declare const ZOHO_ACCOUNTS_PROFILE_READ_SCOPE: ZohoOAuthScope;
238
+ /**
239
+ * Input for reading the identity an access token was issued to.
240
+ */
241
+ export interface ZohoAccountsUserInfoInput {
242
+ /**
243
+ * The access token to read the identity of.
244
+ */
245
+ readonly accessToken: ZohoAccessTokenString;
246
+ }
247
+ /**
248
+ * The authorizing user, as described by the Zoho Accounts user info endpoint.
249
+ *
250
+ * EVERY field is optional. The documented casing (`ZUID`, `Email`, `Display_Name`) cannot be
251
+ * verified without live credentials, so a consumer must tolerate a response whose shape differs —
252
+ * the worst case is a connection with no label, not a failed connect.
253
+ */
254
+ export interface ZohoAccountsUserInfoResponse {
255
+ /**
256
+ * The Zoho user id.
257
+ */
258
+ readonly ZUID?: Maybe<number | string>;
259
+ readonly Email?: Maybe<string>;
260
+ readonly Display_Name?: Maybe<string>;
261
+ readonly First_Name?: Maybe<string>;
262
+ readonly Last_Name?: Maybe<string>;
263
+ }
264
+ /**
265
+ * Reads the identity an access token was issued to.
266
+ */
267
+ export type ZohoAccountsUserInfoFunction = (input: ZohoAccountsUserInfoInput) => Promise<ZohoAccountsUserInfoResponse>;
268
+ /**
269
+ * Creates a function that reads the authorizing user's Zoho identity, so a connection can be
270
+ * labelled with the account it belongs to.
271
+ *
272
+ * Requires the {@link ZOHO_ACCOUNTS_PROFILE_READ_SCOPE} scope on the access token.
273
+ *
274
+ * @param context - Zoho Accounts OAuth client context providing fetch.
275
+ * @returns Function that reads the user info for an access token.
276
+ *
277
+ * @see https://www.zoho.com/accounts/protocol/oauth/web-apps/get-user-info.html
278
+ */
279
+ export declare function zohoAccountsUserInfo(context: ZohoAccountsOAuthClientContext): ZohoAccountsUserInfoFunction;
170
280
  /**
171
281
  * Constructs a standard {@link FetchJsonInput} for Zoho Accounts API calls with the given HTTP method and optional body.
172
282
  *
@@ -0,0 +1,119 @@
1
+ import { type Maybe, type WebsiteUrl } from '@dereekb/util';
2
+ import { type ZohoOAuthClientId } from '../zoho.config';
3
+ import { type ZohoAccountsConfigApiUrlInput } from './accounts.config';
4
+ /**
5
+ * Path of the Zoho Accounts authorization (consent screen) endpoint.
6
+ */
7
+ export declare const ZOHO_ACCOUNTS_AUTHORIZE_PATH = "/oauth/v2/auth";
8
+ /**
9
+ * Path of the Zoho Accounts token endpoint.
10
+ */
11
+ export declare const ZOHO_ACCOUNTS_TOKEN_PATH = "/oauth/v2/token";
12
+ /**
13
+ * The delimiter Zoho joins and returns granted scopes with.
14
+ *
15
+ * A comma, NOT the space OAuth 2.0 specifies. Both halves of the round trip use this: the authorize
16
+ * request joins on it, and the granted `scope` string comes back split on it.
17
+ */
18
+ export declare const ZOHO_OAUTH_SCOPE_DELIMITER = ",";
19
+ /**
20
+ * The `response_type` used by the authorization-code flow.
21
+ */
22
+ export declare const ZOHO_OAUTH_AUTHORIZE_RESPONSE_TYPE = "code";
23
+ /**
24
+ * Required for Zoho to return a `refresh_token` at all.
25
+ */
26
+ export declare const ZOHO_OAUTH_OFFLINE_ACCESS_TYPE = "offline";
27
+ /**
28
+ * Forces the consent screen.
29
+ *
30
+ * Without it Zoho returns a refresh token only on a user's FIRST authorization, so a reconnect would
31
+ * come back with an access token alone — and a persisted exchange that omits the refresh token
32
+ * silently breaks the connection.
33
+ */
34
+ export declare const ZOHO_OAUTH_CONSENT_PROMPT = "consent";
35
+ /**
36
+ * A Zoho OAuth scope, e.g. `ZohoCRM.modules.READ` or `AaaServer.profile.READ`.
37
+ *
38
+ * Deliberately an open string alias and NOT a closed union: Zoho's scope namespace is per-product,
39
+ * dotted, and open-ended, so a runtime list would be stale on arrival. Cal.com can enumerate its
40
+ * twelve; Zoho cannot.
41
+ */
42
+ export type ZohoOAuthScope = string;
43
+ export interface ZohoAccountsAuthorizeUrlFactoryConfig {
44
+ /**
45
+ * The OAuth client id to authorize as.
46
+ */
47
+ readonly clientId: ZohoOAuthClientId;
48
+ /**
49
+ * The redirect URI to return to after the user consents.
50
+ *
51
+ * Must match the URI registered on the Zoho OAuth client byte-for-byte, including the port, and
52
+ * must be identical to the `redirectUri` later passed to the token exchange.
53
+ */
54
+ readonly redirectUri: WebsiteUrl;
55
+ /**
56
+ * The scopes to request.
57
+ */
58
+ readonly scopes: readonly ZohoOAuthScope[];
59
+ /**
60
+ * Accounts host to authorize against. Defaults to the `us` datacenter.
61
+ */
62
+ readonly accountsApiUrl?: Maybe<ZohoAccountsConfigApiUrlInput>;
63
+ /**
64
+ * Defaults to {@link ZOHO_OAUTH_OFFLINE_ACCESS_TYPE}, without which no refresh token is issued.
65
+ */
66
+ readonly accessType?: Maybe<string>;
67
+ /**
68
+ * Defaults to {@link ZOHO_OAUTH_CONSENT_PROMPT}, without which a RE-consent issues no refresh token.
69
+ */
70
+ readonly prompt?: Maybe<string>;
71
+ }
72
+ export interface ZohoAccountsAuthorizeUrlParams {
73
+ /**
74
+ * Opaque state echoed back to the redirect URI.
75
+ *
76
+ * Carries the acting user and is the CSRF defense for the handoff, so it should be signed and
77
+ * short-lived.
78
+ */
79
+ readonly state?: Maybe<string>;
80
+ }
81
+ export type ZohoAccountsAuthorizeUrlFactory = (params?: Maybe<ZohoAccountsAuthorizeUrlParams>) => WebsiteUrl;
82
+ /**
83
+ * Creates a {@link ZohoAccountsAuthorizeUrlFactory} that composes the Zoho authorize URL a user's
84
+ * browser is redirected to in order to begin the authorization-code flow.
85
+ *
86
+ * The client id, redirect URI, and scopes are fixed by the config, since a consumer holds those
87
+ * constant and varies only the per-request `state`.
88
+ *
89
+ * @param config - The client id, redirect URI, scopes, and optional datacenter/consent overrides.
90
+ * @returns A factory that builds an authorize URL for the given params.
91
+ * @throws {Error} When no client id is given, or when no scopes are requested — Zoho refuses an
92
+ * authorize request carrying no scope, and failing at construction beats failing at the consent
93
+ * screen.
94
+ *
95
+ * @see https://www.zoho.com/accounts/protocol/oauth/web-apps/authorization.html
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const authorizeUrlFactory = zohoAccountsAuthorizeUrlFactory({
100
+ * clientId: 'client-id',
101
+ * redirectUri: 'http://localhost:9901/oauth/zoho/callback',
102
+ * scopes: ['AaaServer.profile.READ']
103
+ * });
104
+ *
105
+ * const url = authorizeUrlFactory({ state: 'signed-state' });
106
+ * ```
107
+ *
108
+ * @__NO_SIDE_EFFECTS__
109
+ */
110
+ export declare function zohoAccountsAuthorizeUrlFactory(config: ZohoAccountsAuthorizeUrlFactoryConfig): ZohoAccountsAuthorizeUrlFactory;
111
+ /**
112
+ * Splits a granted Zoho `scope` string on the same delimiter the authorize request joins with.
113
+ *
114
+ * @param scope - The granted scope string returned on a token response.
115
+ * @returns The granted scopes, or undefined when none were granted.
116
+ *
117
+ * @__NO_SIDE_EFFECTS__
118
+ */
119
+ export declare function zohoOAuthScopesFromScopeString(scope: Maybe<string>): Maybe<ZohoOAuthScope[]>;
@@ -6,6 +6,34 @@ import { type ZohoAccessTokenCache, type ZohoAccessTokenFactory } from './accoun
6
6
  * The Zoho Accounts API URL for the US datacenter.
7
7
  */
8
8
  export declare const ZOHO_ACCOUNTS_US_API_URL = "https://accounts.zoho.com";
9
+ /**
10
+ * The Zoho Accounts API URL for the EU datacenter.
11
+ */
12
+ export declare const ZOHO_ACCOUNTS_EU_API_URL = "https://accounts.zoho.eu";
13
+ /**
14
+ * The Zoho Accounts API URL for the India datacenter.
15
+ */
16
+ export declare const ZOHO_ACCOUNTS_IN_API_URL = "https://accounts.zoho.in";
17
+ /**
18
+ * The Zoho Accounts API URL for the Australia datacenter.
19
+ */
20
+ export declare const ZOHO_ACCOUNTS_AU_API_URL = "https://accounts.zoho.com.au";
21
+ /**
22
+ * The Zoho Accounts API URL for the Japan datacenter.
23
+ */
24
+ export declare const ZOHO_ACCOUNTS_JP_API_URL = "https://accounts.zoho.jp";
25
+ /**
26
+ * The Zoho Accounts API URL for the United Kingdom datacenter.
27
+ */
28
+ export declare const ZOHO_ACCOUNTS_UK_API_URL = "https://accounts.zoho.uk";
29
+ /**
30
+ * The Zoho Accounts API URL for the Canada datacenter.
31
+ */
32
+ export declare const ZOHO_ACCOUNTS_CA_API_URL = "https://accounts.zohocloud.ca";
33
+ /**
34
+ * The Zoho Accounts API URL for the Saudi Arabia datacenter.
35
+ */
36
+ export declare const ZOHO_ACCOUNTS_SA_API_URL = "https://accounts.zoho.sa";
9
37
  /**
10
38
  * Url for the Zoho Accounts API.
11
39
  *
@@ -14,15 +42,50 @@ export declare const ZOHO_ACCOUNTS_US_API_URL = "https://accounts.zoho.com";
14
42
  * https://help.zoho.com/portal/en/kb/creator/developer-guide/others/url-patterns/articles/know-your-creator-account-s-base-url
15
43
  */
16
44
  export type ZohoAccountsApiUrl = ZohoApiUrl;
17
- export type ZohoAccountsApiUrlKey = 'us';
45
+ export type ZohoAccountsApiUrlKey = 'us' | 'eu' | 'in' | 'au' | 'jp' | 'uk' | 'ca' | 'sa';
18
46
  export type ZohoAccountsConfigApiUrlInput = ZohoAccountsApiUrlKey | ZohoAccountsApiUrl;
19
47
  /**
20
- * Resolves a Zoho Accounts API URL input to the full base URL. The 'us' key maps to the US datacenter; custom URLs pass through unchanged.
48
+ * Every Zoho Accounts host this package will talk to, keyed by datacenter.
49
+ *
50
+ * A closed set rather than an open string, because a value echoed back on an OAuth callback
51
+ * (`accounts-server`) is checked against it before being used as a token-exchange target — an
52
+ * unchecked host there would receive the client secret.
53
+ */
54
+ export declare const ZOHO_ACCOUNTS_API_URLS: Readonly<Record<ZohoAccountsApiUrlKey, ZohoAccountsApiUrl>>;
55
+ /**
56
+ * Resolves a Zoho Accounts API URL input to the full base URL. A datacenter key maps to that
57
+ * datacenter's host; custom URLs pass through unchanged.
21
58
  *
22
59
  * @param input - A well-known datacenter key or a custom Zoho Accounts API URL.
23
60
  * @returns The resolved full Zoho Accounts API base URL.
24
61
  */
25
62
  export declare function zohoAccountsConfigApiUrl(input: ZohoAccountsConfigApiUrlInput): ZohoApiUrl;
63
+ /**
64
+ * Returns whether the input is one of the known Zoho Accounts hosts.
65
+ *
66
+ * Exists to gate a value that arrives from OUTSIDE the process: Zoho echoes the issuing datacenter
67
+ * back as the `accounts-server` OAuth callback parameter, and that host becomes the POST target the
68
+ * client secret is sent to. An attacker can compose that redirect, so only an exact match against
69
+ * {@link ZOHO_ACCOUNTS_API_URLS} may be honored.
70
+ *
71
+ * @param url - The candidate accounts host.
72
+ * @returns True when the value is exactly one of the known Zoho Accounts hosts.
73
+ *
74
+ * @__NO_SIDE_EFFECTS__
75
+ */
76
+ export declare function isKnownZohoAccountsApiUrl(url: Maybe<string>): boolean;
77
+ /**
78
+ * Returns the datacenter key for a known Zoho Accounts host.
79
+ *
80
+ * A trailing slash is tolerated, since Zoho's `accounts-server` value is URL-encoded and some
81
+ * datacenters echo it back with one; nothing else about the value is normalized.
82
+ *
83
+ * @param url - The candidate accounts host.
84
+ * @returns The matching datacenter key, or undefined when the host is not a known one.
85
+ *
86
+ * @__NO_SIDE_EFFECTS__
87
+ */
88
+ export declare function zohoAccountsApiUrlKeyForApiUrl(url: Maybe<string>): Maybe<ZohoAccountsApiUrlKey>;
26
89
  /**
27
90
  * Configuration for ZohoAccounts.
28
91
  */
@@ -1,7 +1,10 @@
1
+ import { type FetchHandler } from '@dereekb/util/fetch';
1
2
  import { type ZohoAccountsConfig, type ZohoAccountsContextRef, type ZohoAccountsFetchFactory } from './accounts.config';
3
+ import { type ZohoAuthClientIdAndSecretPair, type ZohoConfig } from '../zoho.config';
2
4
  import { type LogZohoServerErrorFunction } from '../zoho.error.api';
3
5
  import { type ZohoAccessTokenCache, type ZohoAccessTokenFactory, type ZohoAccessTokenRefresher } from './accounts';
4
6
  import { type Maybe, type Milliseconds } from '@dereekb/util';
7
+ import { type ZohoAccountsOAuthClientContext } from './accounts.api';
5
8
  /**
6
9
  * Top-level Zoho Accounts client instance, providing access to the authenticated {@link ZohoAccountsContext}
7
10
  * used for OAuth token management.
@@ -26,6 +29,15 @@ export interface ZohoAccountsFactoryConfig {
26
29
  * Factory function that creates a {@link ZohoAccounts} client from a {@link ZohoAccountsConfig}.
27
30
  */
28
31
  export type ZohoAccountsFactory = (config: ZohoAccountsConfig) => ZohoAccounts;
32
+ /**
33
+ * Creates the {@link ZohoAccountsFetchFactory} the Zoho Accounts clients use when none is supplied.
34
+ *
35
+ * @param fetchHandler - The handler the produced fetches route through.
36
+ * @returns The default Zoho Accounts fetch factory.
37
+ *
38
+ * @__NO_SIDE_EFFECTS__
39
+ */
40
+ export declare function defaultZohoAccountsFetchFactory(fetchHandler: FetchHandler): ZohoAccountsFetchFactory;
29
41
  /**
30
42
  * Creates a {@link ZohoAccountsFactory} from the given configuration.
31
43
  *
@@ -65,6 +77,63 @@ export declare function zohoAccountsFactory(factoryConfig: ZohoAccountsFactoryCo
65
77
  * Configuration for {@link zohoAccountsZohoAccessTokenFactory}, controlling token refresh,
66
78
  * caching, and expiration buffer behavior.
67
79
  */
80
+ /**
81
+ * Configuration for a {@link ZohoAccountsOAuthClient}.
82
+ *
83
+ * Notably absent: a refresh token. A per-user authorization-code handoff is how one is obtained.
84
+ */
85
+ export interface ZohoAccountsOAuthClientConfig extends ZohoConfig, ZohoAuthClientIdAndSecretPair {
86
+ }
87
+ /**
88
+ * A Zoho Accounts client authenticated by client credentials alone.
89
+ */
90
+ export interface ZohoAccountsOAuthClient {
91
+ readonly oauthClientContext: ZohoAccountsOAuthClientContext;
92
+ }
93
+ /**
94
+ * Configuration for creating a {@link ZohoAccountsOAuthClientFactory}.
95
+ */
96
+ export interface ZohoAccountsOAuthClientFactoryConfig {
97
+ /**
98
+ * Custom fetch factory for creating the underlying HTTP client.
99
+ */
100
+ readonly fetchFactory?: ZohoAccountsFetchFactory;
101
+ /**
102
+ * Custom FetchHandler to use with the default fetchFactory.
103
+ *
104
+ * Intercepts requests before they leave the process, so specs can assert the RESOLVED url.
105
+ * Ignored when a `fetchFactory` is provided.
106
+ */
107
+ readonly fetchHandler?: Maybe<FetchHandler>;
108
+ /**
109
+ * Custom error logging function invoked when Zoho API errors are encountered.
110
+ */
111
+ readonly logZohoServerErrorFunction?: LogZohoServerErrorFunction;
112
+ }
113
+ /**
114
+ * Factory function that creates a {@link ZohoAccountsOAuthClient} from a {@link ZohoAccountsOAuthClientConfig}.
115
+ */
116
+ export type ZohoAccountsOAuthClientFactory = (config: ZohoAccountsOAuthClientConfig) => ZohoAccountsOAuthClient;
117
+ /**
118
+ * Creates a {@link ZohoAccountsOAuthClientFactory}, producing Zoho Accounts clients authenticated by
119
+ * CLIENT CREDENTIALS ALONE.
120
+ *
121
+ * {@link zohoAccountsFactory} requires a refresh token, which a per-user authorization-code handoff
122
+ * does not have yet — the handoff is how the refresh token is obtained. This client covers exactly
123
+ * the two endpoints that need no user token: the authorization-code exchange and
124
+ * `/oauth/user/info`.
125
+ *
126
+ * It builds its fetch through the same error handling as the full client, which matters more than it
127
+ * looks: Zoho answers a failed token exchange with HTTP 200 and an `{ "error": … }` body, and
128
+ * `interceptZohoAccounts200StatusWithErrorResponse` is what turns that into a thrown error instead
129
+ * of a "successful" exchange with an undefined access token.
130
+ *
131
+ * @param factoryConfig - Configuration providing optional fetch and logging overrides.
132
+ * @returns A factory function that creates client-credentials-only Zoho Accounts clients.
133
+ *
134
+ * @__NO_SIDE_EFFECTS__
135
+ */
136
+ export declare function zohoAccountsOAuthClientFactory(factoryConfig: ZohoAccountsOAuthClientFactoryConfig): ZohoAccountsOAuthClientFactory;
68
137
  export interface ZohoAccountsZohoAccessTokenFactoryConfig {
69
138
  /**
70
139
  * Number of milliseconds before the expiration time a token should be discarded
@@ -1,5 +1,6 @@
1
1
  export * from './accounts';
2
2
  export * from './accounts.api';
3
+ export * from './accounts.authorize';
3
4
  export * from './accounts.config';
4
5
  export * from './accounts.error.api';
5
6
  export * from './accounts.factory';