Skip to content

Commit 6235ba2

Browse files
committed
normalize tenant ids
1 parent f00470b commit 6235ba2

File tree

8 files changed

+109
-59
lines changed

8 files changed

+109
-59
lines changed

.claude/skills/api-client-development/SKILL.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,22 @@ SCAPI APIs use an `organizationId` path parameter with the `f_ecom_` prefix, but
172172

173173
```typescript
174174
// From @salesforce/b2c-tooling-sdk (or clients/custom-apis.ts)
175-
import {toOrganizationId, toTenantId, buildTenantScope} from '@salesforce/b2c-tooling-sdk';
175+
import {toOrganizationId, normalizeTenantId, buildTenantScope} from '@salesforce/b2c-tooling-sdk';
176176

177-
// Convert tenant ID to organization ID (adds f_ecom_ prefix)
177+
// Convert tenant ID to organization ID (normalizes + adds f_ecom_ prefix)
178178
toOrganizationId('zzxy_prd') // Returns 'f_ecom_zzxy_prd'
179179
toOrganizationId('f_ecom_zzxy_prd') // Returns 'f_ecom_zzxy_prd' (unchanged)
180+
toOrganizationId('zzxy-prd') // Returns 'f_ecom_zzxy_prd' (hyphen normalized)
180181

181-
// Extract raw tenant ID (strips f_ecom_ prefix)
182-
toTenantId('f_ecom_zzxy_prd') // Returns 'zzxy_prd'
183-
toTenantId('zzxy_prd') // Returns 'zzxy_prd' (unchanged)
182+
// Normalize any tenant/org ID form to canonical underscore format
183+
normalizeTenantId('f_ecom_zzxy_prd') // Returns 'zzxy_prd'
184+
normalizeTenantId('zzxy-prd') // Returns 'zzxy_prd'
185+
normalizeTenantId('zzxy-prd.dx.commercecloud.salesforce.com') // Returns 'zzxy_prd'
184186

185-
// Build tenant-specific OAuth scope
187+
// Build tenant-specific OAuth scope (normalizes input)
186188
buildTenantScope('zzxy_prd') // Returns 'SALESFORCE_COMMERCE_API:zzxy_prd'
187189
buildTenantScope('f_ecom_zzxy_prd') // Returns 'SALESFORCE_COMMERCE_API:zzxy_prd'
190+
buildTenantScope('zzxy-prd') // Returns 'SALESFORCE_COMMERCE_API:zzxy_prd'
188191
```
189192

190193
### Constants
@@ -272,20 +275,19 @@ export function createCustomApisClient(
272275
export const ORGANIZATION_ID_PREFIX = 'f_ecom_';
273276
export const SCAPI_TENANT_SCOPE_PREFIX = 'SALESFORCE_COMMERCE_API:';
274277

275-
export function toOrganizationId(tenantId: string): string {
276-
return tenantId.startsWith(ORGANIZATION_ID_PREFIX)
277-
? tenantId
278-
: `${ORGANIZATION_ID_PREFIX}${tenantId}`;
278+
export function normalizeTenantId(value: string): string {
279+
let id = value.trim();
280+
if (id.includes('.')) id = id.split('.')[0];
281+
if (id.startsWith(ORGANIZATION_ID_PREFIX)) id = id.slice(ORGANIZATION_ID_PREFIX.length);
282+
return id.replaceAll('-', '_');
279283
}
280284

281-
export function toTenantId(value: string): string {
282-
return value.startsWith(ORGANIZATION_ID_PREFIX)
283-
? value.slice(ORGANIZATION_ID_PREFIX.length)
284-
: value;
285+
export function toOrganizationId(tenantId: string): string {
286+
return `${ORGANIZATION_ID_PREFIX}${normalizeTenantId(tenantId)}`;
285287
}
286288

287289
export function buildTenantScope(tenantId: string): string {
288-
return `${SCAPI_TENANT_SCOPE_PREFIX}${toTenantId(tenantId)}`;
290+
return `${SCAPI_TENANT_SCOPE_PREFIX}${normalizeTenantId(tenantId)}`;
289291
}
290292
```
291293

@@ -440,13 +442,13 @@ const {data, error} = await client.GET('/endpoint', {...});
440442

441443
## Troubleshooting
442444

443-
**OAuth scope errors (401/403 from SCAPI)**: Ensure the client factory calls `auth.withAdditionalScopes()` with both the domain scope (e.g., `sfcc.custom-apis`) and the tenant-specific scope (`SALESFORCE_COMMERCE_API:<tenantId>`). Use `buildTenantScope()` to strip the `f_ecom_` prefix from tenant IDs before building scopes.
445+
**OAuth scope errors (401/403 from SCAPI)**: Ensure the client factory calls `auth.withAdditionalScopes()` with both the domain scope (e.g., `sfcc.custom-apis`) and the tenant-specific scope (`SALESFORCE_COMMERCE_API:<tenantId>`). Use `buildTenantScope()` which normalizes any tenant ID form (hyphenated, hostname, org ID) to canonical underscores before building scopes.
444446

445447
**Type generation failures**: Check that the OpenAPI spec in `specs/` is valid YAML/JSON. Run `pnpm --filter @salesforce/b2c-tooling-sdk run generate:types` and inspect the output. Common issues: spec references external files that aren't present, or uses OpenAPI features not supported by openapi-typescript.
446448

447449
**Middleware ordering issues**: Auth middleware should be added first (`client.use(createAuthMiddleware(...))`), then logging. In openapi-fetch, middleware runs in reverse registration order for requests, so auth registered first means it runs last — ensuring the logging middleware sees the final request with auth headers.
448450

449-
**`organizationId` mismatch**: SCAPI path parameters need the `f_ecom_` prefix (use `toOrganizationId()`), while OAuth scopes need the raw tenant ID (use `toTenantId()`). Mixing these up causes 404s or scope errors.
451+
**`organizationId` mismatch**: SCAPI path parameters need the `f_ecom_` prefix (use `toOrganizationId()`), while OAuth scopes need the raw tenant ID (use `normalizeTenantId()`). Both functions accept any parseable form (hyphenated, hostname, org ID). Mixing these up causes 404s or scope errors.
450452

451453
## Checklist: New SCAPI Client
452454

packages/b2c-tooling-sdk/src/cli/oauth-command.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {OAuthStrategy} from '../auth/oauth.js';
1212
import {ImplicitOAuthStrategy} from '../auth/oauth-implicit.js';
1313
import {t} from '../i18n/index.js';
1414
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
15+
import {normalizeTenantId} from '../clients/custom-apis.js';
1516

1617
/**
1718
* Default OAuth authentication methods array used by getOAuthStrategy.
@@ -222,10 +223,6 @@ export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand
222223
),
223224
);
224225
}
225-
// Strip optional f_ecom_ prefix so users can pass either the organization ID or tenant ID
226-
if (tenantId.startsWith('f_ecom_')) {
227-
return tenantId.slice('f_ecom_'.length);
228-
}
229-
return tenantId;
226+
return normalizeTenantId(tenantId);
230227
}
231228
}

packages/b2c-tooling-sdk/src/clients/cdn-zones.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {OAuthStrategy} from '../auth/oauth.js';
1919
import type {paths, components} from './cdn-zones.generated.js';
2020
import {createAuthMiddleware, createLoggingMiddleware} from './middleware.js';
2121
import {globalMiddlewareRegistry, type MiddlewareRegistry} from './middleware-registry.js';
22-
import {toOrganizationId, toTenantId, buildTenantScope} from './custom-apis.js';
22+
import {toOrganizationId, normalizeTenantId, buildTenantScope} from './custom-apis.js';
2323

2424
/**
2525
* Re-export generated types for external use.
@@ -29,7 +29,7 @@ export type {paths, components};
2929
/**
3030
* Re-export organization/tenant utilities for convenience.
3131
*/
32-
export {toOrganizationId, toTenantId, buildTenantScope};
32+
export {toOrganizationId, normalizeTenantId, buildTenantScope};
3333

3434
/**
3535
* The typed CDN Zones client for eCDN management.

packages/b2c-tooling-sdk/src/clients/custom-apis.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -183,27 +183,44 @@ export const SCAPI_TENANT_SCOPE_PREFIX = 'SALESFORCE_COMMERCE_API:';
183183
* toOrganizationId('f_ecom_zzxy_prd') // Returns 'f_ecom_zzxy_prd' (unchanged)
184184
*/
185185
export function toOrganizationId(tenantId: string): string {
186-
if (tenantId.startsWith(ORGANIZATION_ID_PREFIX)) {
187-
return tenantId;
188-
}
189-
return `${ORGANIZATION_ID_PREFIX}${tenantId}`;
186+
return `${ORGANIZATION_ID_PREFIX}${normalizeTenantId(tenantId)}`;
190187
}
191188

192189
/**
193-
* Extracts the raw tenant ID by stripping the f_ecom_ prefix if present.
190+
* Normalizes any parseable tenant/organization ID form to the canonical underscore format.
191+
*
192+
* Supported input forms (all resolve to `abcd_123`):
193+
* - `abcd_123` — canonical tenant ID (returned as-is)
194+
* - `abcd-123` — hyphenated tenant ID
195+
* - `f_ecom_abcd_123` — organization ID
196+
* - `f_ecom_abcd-123` — org ID with hyphenated tenant
197+
* - `abcd-123.dx.commercecloud.salesforce.com` — sandbox hostname
194198
*
195-
* @param value - The tenant ID or organization ID (e.g., "zzxy_prd" or "f_ecom_zzxy_prd")
196-
* @returns The raw tenant ID without the prefix (e.g., "zzxy_prd")
199+
* @param value - The tenant ID, organization ID, or hostname
200+
* @returns The normalized tenant ID in canonical underscore format (e.g., "abcd_123")
197201
*
198202
* @example
199-
* toTenantId('f_ecom_zzxy_prd') // Returns 'zzxy_prd'
200-
* toTenantId('zzxy_prd') // Returns 'zzxy_prd' (unchanged)
203+
* normalizeTenantId('f_ecom_zzxy_prd') // Returns 'zzxy_prd'
204+
* normalizeTenantId('zzxy-prd') // Returns 'zzxy_prd'
205+
* normalizeTenantId('zzxy-prd.dx.commercecloud.salesforce.com') // Returns 'zzxy_prd'
201206
*/
202-
export function toTenantId(value: string): string {
203-
if (value.startsWith(ORGANIZATION_ID_PREFIX)) {
204-
return value.slice(ORGANIZATION_ID_PREFIX.length);
207+
export function normalizeTenantId(value: string): string {
208+
let id = value.trim();
209+
210+
// Extract hostname prefix: "abcd-123.dx.commercecloud.salesforce.com" → "abcd-123"
211+
if (id.includes('.')) {
212+
id = id.split('.')[0];
205213
}
206-
return value;
214+
215+
// Strip f_ecom_ prefix (handles org ID form)
216+
if (id.startsWith(ORGANIZATION_ID_PREFIX)) {
217+
id = id.slice(ORGANIZATION_ID_PREFIX.length);
218+
}
219+
220+
// Convert hyphens to underscores
221+
id = id.replaceAll('-', '_');
222+
223+
return id;
207224
}
208225

209226
/**
@@ -217,5 +234,5 @@ export function toTenantId(value: string): string {
217234
* buildTenantScope('f_ecom_zzxy_prd') // Returns 'SALESFORCE_COMMERCE_API:zzxy_prd'
218235
*/
219236
export function buildTenantScope(tenantId: string): string {
220-
return `${SCAPI_TENANT_SCOPE_PREFIX}${toTenantId(tenantId)}`;
237+
return `${SCAPI_TENANT_SCOPE_PREFIX}${normalizeTenantId(tenantId)}`;
221238
}

packages/b2c-tooling-sdk/src/clients/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export type {
183183
export {
184184
createCustomApisClient,
185185
toOrganizationId,
186-
toTenantId,
186+
normalizeTenantId,
187187
buildTenantScope,
188188
ORGANIZATION_ID_PREFIX,
189189
SCAPI_TENANT_SCOPE_PREFIX,

packages/b2c-tooling-sdk/src/clients/scapi-schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {OAuthStrategy} from '../auth/oauth.js';
1818
import type {paths, components} from './scapi-schemas.generated.js';
1919
import {createAuthMiddleware, createLoggingMiddleware} from './middleware.js';
2020
import {globalMiddlewareRegistry, type MiddlewareRegistry} from './middleware-registry.js';
21-
import {toOrganizationId, toTenantId, buildTenantScope} from './custom-apis.js';
21+
import {toOrganizationId, normalizeTenantId, buildTenantScope} from './custom-apis.js';
2222

2323
/**
2424
* Re-export generated types for external use.
@@ -28,7 +28,7 @@ export type {paths, components};
2828
/**
2929
* Re-export organization/tenant utilities for convenience.
3030
*/
31-
export {toOrganizationId, toTenantId, buildTenantScope};
31+
export {toOrganizationId, normalizeTenantId, buildTenantScope};
3232

3333
/**
3434
* The typed SCAPI Schemas client for discovering available SCAPI APIs.

packages/b2c-tooling-sdk/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export {
7373
createCdnZonesClient,
7474
createCipClient,
7575
toOrganizationId,
76-
toTenantId,
76+
normalizeTenantId,
7777
buildTenantScope,
7878
getApiErrorMessage,
7979
isValidRoleTenantFilter,

packages/b2c-tooling-sdk/test/clients/custom-apis.test.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {setupServer} from 'msw/node';
1111
import {
1212
createCustomApisClient,
1313
toOrganizationId,
14-
toTenantId,
14+
normalizeTenantId,
1515
buildTenantScope,
1616
ORGANIZATION_ID_PREFIX,
1717
SCAPI_TENANT_SCOPE_PREFIX,
@@ -329,15 +329,55 @@ describe('clients/custom-apis', () => {
329329
});
330330
});
331331

332+
describe('normalizeTenantId', () => {
333+
it('returns canonical tenant ID unchanged', () => {
334+
expect(normalizeTenantId('abcd_123')).to.equal('abcd_123');
335+
});
336+
337+
it('converts hyphenated tenant ID to underscores', () => {
338+
expect(normalizeTenantId('abcd-123')).to.equal('abcd_123');
339+
});
340+
341+
it('strips f_ecom_ prefix from organization ID', () => {
342+
expect(normalizeTenantId('f_ecom_abcd_123')).to.equal('abcd_123');
343+
});
344+
345+
it('strips f_ecom_ prefix and converts hyphens', () => {
346+
expect(normalizeTenantId('f_ecom_abcd-123')).to.equal('abcd_123');
347+
});
348+
349+
it('extracts tenant from hostname', () => {
350+
expect(normalizeTenantId('abcd-123.dx.commercecloud.salesforce.com')).to.equal('abcd_123');
351+
});
352+
353+
it('trims whitespace', () => {
354+
expect(normalizeTenantId(' abcd_123 ')).to.equal('abcd_123');
355+
});
356+
357+
it('handles various formats', () => {
358+
expect(normalizeTenantId('f_ecom_zzxy_prd')).to.equal('zzxy_prd');
359+
expect(normalizeTenantId('zzxy_prd')).to.equal('zzxy_prd');
360+
expect(normalizeTenantId('f_ecom_test')).to.equal('test');
361+
});
362+
});
363+
332364
describe('toOrganizationId', () => {
333365
it('adds f_ecom_ prefix to tenant ID', () => {
334366
expect(toOrganizationId('zzxy_prd')).to.equal('f_ecom_zzxy_prd');
335367
});
336368

337-
it('returns unchanged if already has f_ecom_ prefix', () => {
369+
it('normalizes and adds prefix for already-prefixed input', () => {
338370
expect(toOrganizationId('f_ecom_zzxy_prd')).to.equal('f_ecom_zzxy_prd');
339371
});
340372

373+
it('normalizes hyphenated input', () => {
374+
expect(toOrganizationId('abcd-123')).to.equal('f_ecom_abcd_123');
375+
});
376+
377+
it('normalizes hostname input', () => {
378+
expect(toOrganizationId('abcd-123.dx.commercecloud.salesforce.com')).to.equal('f_ecom_abcd_123');
379+
});
380+
341381
it('handles various tenant ID formats', () => {
342382
expect(toOrganizationId('abcd_001')).to.equal('f_ecom_abcd_001');
343383
expect(toOrganizationId('test')).to.equal('f_ecom_test');
@@ -348,21 +388,6 @@ describe('clients/custom-apis', () => {
348388
});
349389
});
350390

351-
describe('toTenantId', () => {
352-
it('strips f_ecom_ prefix from organization ID', () => {
353-
expect(toTenantId('f_ecom_zzxy_prd')).to.equal('zzxy_prd');
354-
});
355-
356-
it('returns unchanged if no f_ecom_ prefix', () => {
357-
expect(toTenantId('zzxy_prd')).to.equal('zzxy_prd');
358-
});
359-
360-
it('handles various formats', () => {
361-
expect(toTenantId('f_ecom_abcd_001')).to.equal('abcd_001');
362-
expect(toTenantId('f_ecom_test')).to.equal('test');
363-
});
364-
});
365-
366391
describe('buildTenantScope', () => {
367392
it('builds scope from tenant ID', () => {
368393
expect(buildTenantScope('zzxy_prd')).to.equal('SALESFORCE_COMMERCE_API:zzxy_prd');
@@ -372,6 +397,15 @@ describe('clients/custom-apis', () => {
372397
expect(buildTenantScope('f_ecom_zzxy_prd')).to.equal('SALESFORCE_COMMERCE_API:zzxy_prd');
373398
});
374399

400+
it('normalizes hyphenated input', () => {
401+
expect(buildTenantScope('abcd-123')).to.equal('SALESFORCE_COMMERCE_API:abcd_123');
402+
});
403+
404+
it('normalizes hostname input', () => {
405+
const input = 'abcd-123.dx.commercecloud.salesforce.com';
406+
expect(buildTenantScope(input)).to.equal('SALESFORCE_COMMERCE_API:abcd_123');
407+
});
408+
375409
it('uses SCAPI_TENANT_SCOPE_PREFIX constant', () => {
376410
expect(SCAPI_TENANT_SCOPE_PREFIX).to.equal('SALESFORCE_COMMERCE_API:');
377411
});

0 commit comments

Comments
 (0)