|
15 | 15 | export const CLIENT_CREDENTIAL_ACCOUNT_SECRET_TYPE = 'client_credential_account' as const |
16 | 16 |
|
17 | 17 | /** Contract field ids a client-credential connect modal collects. */ |
18 | | -export type ClientCredentialAccountFieldId = 'clientId' | 'clientSecret' | 'orgId' |
| 18 | +export type ClientCredentialAccountFieldId = 'clientId' | 'clientSecret' | 'orgId' | 'dataCenter' |
19 | 19 |
|
20 | 20 | export interface ClientCredentialAccountField { |
21 | 21 | id: ClientCredentialAccountFieldId |
22 | 22 | label: string |
23 | 23 | placeholder: string |
24 | 24 | /** Rendered with SecretInput and never echoed back. */ |
25 | 25 | secret: boolean |
| 26 | + /** |
| 27 | + * Field the connect modal may submit empty; excluded from |
| 28 | + * {@link CLIENT_CREDENTIAL_ACCOUNT_REQUIRED_FIELDS} so create/reconnect |
| 29 | + * validation never demands it. Omitted (default) means required. |
| 30 | + */ |
| 31 | + optional?: boolean |
26 | 32 | /** Soft-format hint shown while the current value doesn't match `hintPattern`. */ |
27 | 33 | hintPattern?: RegExp |
28 | 34 | hintMessage?: string |
@@ -116,6 +122,66 @@ export function normalizeZohoDeskSoid(rawOrgId: string): string { |
116 | 122 | /** A normalized `soid`: a service-name prefix plus a numeric Zoho org id. */ |
117 | 123 | export const ZOHO_DESK_SOID_REGEX = /^[A-Za-z]+\.\d+$/ |
118 | 124 |
|
| 125 | +/** |
| 126 | + * Zoho data centers the Self Client (client-credentials) flow supports. Each |
| 127 | + * entry pairs the accounts server that mints the token with the Desk REST host |
| 128 | + * in the same region, so the minter never has to guess either one. |
| 129 | + * |
| 130 | + * Only regions where BOTH hosts are confirmed are listed. CA, SA, JP, CN, and |
| 131 | + * UK are deliberately absent: Zoho's accounts documentation and Zoho's own Desk |
| 132 | + * SDK disagree on Canada (`accounts.zohocloud.ca` vs `accounts.zoho.ca`), and |
| 133 | + * the Desk hosts for the others are not confirmed. More regions can be added |
| 134 | + * once both the accounts server and the Desk host are confirmed for them. |
| 135 | + * |
| 136 | + * This applies to the service account only — the interactive OAuth flow's |
| 137 | + * authorize/token URLs are static per provider and remain US-only. |
| 138 | + */ |
| 139 | +export const ZOHO_DESK_DATA_CENTERS = { |
| 140 | + us: { accountsBase: 'https://accounts.zoho.com', deskBase: 'https://desk.zoho.com' }, |
| 141 | + eu: { accountsBase: 'https://accounts.zoho.eu', deskBase: 'https://desk.zoho.eu' }, |
| 142 | + in: { accountsBase: 'https://accounts.zoho.in', deskBase: 'https://desk.zoho.in' }, |
| 143 | + au: { accountsBase: 'https://accounts.zoho.com.au', deskBase: 'https://desk.zoho.com.au' }, |
| 144 | +} as const satisfies Record<string, { accountsBase: string; deskBase: string }> |
| 145 | + |
| 146 | +export type ZohoDeskDataCenterId = keyof typeof ZOHO_DESK_DATA_CENTERS |
| 147 | + |
| 148 | +/** Region used when the admin leaves the field blank, so existing credentials are unaffected. */ |
| 149 | +export const DEFAULT_ZOHO_DESK_DATA_CENTER: ZohoDeskDataCenterId = 'us' |
| 150 | + |
| 151 | +export const ZOHO_DESK_DATA_CENTER_IDS = Object.keys( |
| 152 | + ZOHO_DESK_DATA_CENTERS |
| 153 | +) as ZohoDeskDataCenterId[] |
| 154 | + |
| 155 | +/** Accepts exactly the supported region codes, case-insensitively after normalization. */ |
| 156 | +export const ZOHO_DESK_DATA_CENTER_REGEX = new RegExp(`^(${ZOHO_DESK_DATA_CENTER_IDS.join('|')})$`) |
| 157 | + |
| 158 | +/** |
| 159 | + * Normalizes a pasted data-center value to its lowercase region code. Shared by |
| 160 | + * the connect modal's format hint and the server-side minter so both judge the |
| 161 | + * same normalized value. |
| 162 | + */ |
| 163 | +export function normalizeZohoDeskDataCenter(rawDataCenter: string): string { |
| 164 | + return rawDataCenter.trim().toLowerCase() |
| 165 | +} |
| 166 | + |
| 167 | +/** |
| 168 | + * Resolves a stored data-center value to its accounts/Desk host pair. A blank, |
| 169 | + * absent, or unrecognized value falls back to {@link DEFAULT_ZOHO_DESK_DATA_CENTER} |
| 170 | + * so credentials created before this field existed keep minting against the US |
| 171 | + * accounts server exactly as they did. |
| 172 | + */ |
| 173 | +export function resolveZohoDeskDataCenter(rawDataCenter?: string): { |
| 174 | + id: ZohoDeskDataCenterId |
| 175 | + accountsBase: string |
| 176 | + deskBase: string |
| 177 | +} { |
| 178 | + const normalized = normalizeZohoDeskDataCenter(rawDataCenter ?? '') |
| 179 | + const id: ZohoDeskDataCenterId = Object.hasOwn(ZOHO_DESK_DATA_CENTERS, normalized) |
| 180 | + ? (normalized as ZohoDeskDataCenterId) |
| 181 | + : DEFAULT_ZOHO_DESK_DATA_CENTER |
| 182 | + return { id, ...ZOHO_DESK_DATA_CENTERS[id] } |
| 183 | +} |
| 184 | + |
119 | 185 | export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< |
120 | 186 | ClientCredentialAccountProviderId, |
121 | 187 | ClientCredentialAccountDescriptor |
@@ -237,25 +303,36 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< |
237 | 303 | hintMessage: |
238 | 304 | 'Paste the numeric Zoho Desk organization ID from Setup → Developer Space → API, or the full ZohoDesk.<orgId> value.', |
239 | 305 | }, |
| 306 | + { |
| 307 | + id: 'dataCenter', |
| 308 | + label: 'Data center', |
| 309 | + placeholder: 'us', |
| 310 | + secret: false, |
| 311 | + optional: true, |
| 312 | + hintPattern: ZOHO_DESK_DATA_CENTER_REGEX, |
| 313 | + hintNormalize: normalizeZohoDeskDataCenter, |
| 314 | + hintMessage: `Enter one of ${ZOHO_DESK_DATA_CENTER_IDS.join(', ')}. Leave blank for us (accounts.zoho.com).`, |
| 315 | + }, |
240 | 316 | ], |
241 | 317 | docsUrl: 'https://docs.sim.ai/integrations/zoho-desk-service-account', |
242 | 318 | helpText: |
243 | | - 'Create the Self Client in the Zoho API Console, add the Zoho Desk scopes, and use the organization ID from Setup → Developer Space → API. Only Zoho accounts in the US data center (accounts.zoho.com) are supported, and Zoho Desk triggers still require an OAuth connection.', |
| 319 | + 'Create the Self Client in the Zoho API Console, add the Zoho Desk scopes, and use the organization ID from Setup → Developer Space → API. Self Clients work with the US, EU, IN, and AU data centers — leave the data center blank for US. Zoho Desk triggers still require an OAuth connection, which is US-only.', |
244 | 320 | }, |
245 | 321 | } |
246 | 322 |
|
247 | 323 | /** |
248 | 324 | * Required contract fields per client-credential provider, consumed by the |
249 | 325 | * `createCredentialBodySchema` superRefine so validation errors name the exact |
250 | | - * missing field. Derived from each descriptor's field list. |
| 326 | + * missing field. Derived from each descriptor's field list, minus the fields |
| 327 | + * marked `optional`. |
251 | 328 | */ |
252 | 329 | export const CLIENT_CREDENTIAL_ACCOUNT_REQUIRED_FIELDS: Record< |
253 | 330 | string, |
254 | 331 | ClientCredentialAccountFieldId[] |
255 | 332 | > = Object.fromEntries( |
256 | 333 | Object.values(CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS).map((descriptor) => [ |
257 | 334 | descriptor.providerId, |
258 | | - descriptor.fields.map((field) => field.id), |
| 335 | + descriptor.fields.filter((field) => !field.optional).map((field) => field.id), |
259 | 336 | ]) |
260 | 337 | ) |
261 | 338 |
|
|
0 commit comments