-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathclient.ts
More file actions
196 lines (174 loc) · 6.01 KB
/
client.ts
File metadata and controls
196 lines (174 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/*
* Copyright (c) 2025, Salesforce, Inc.
* SPDX-License-Identifier: Apache-2
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Command, Flags, ux} from '@oclif/core';
import cliui from 'cliui';
import {OAuthCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {createSlasClient, type SlasClient, type SlasComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
export type Client = SlasComponents['schemas']['Client'];
export type ClientRequest = SlasComponents['schemas']['ClientRequest'];
/**
* JSON output structure for SLAS client commands
*/
export interface ClientOutput {
clientId: string;
name: string;
secret?: string;
scopes: string[];
channels: string[];
redirectUri: string;
callbackUri?: string;
isPrivateClient: boolean;
}
/**
* Normalize a client response from the API.
* Handles scopes being returned as space-separated string.
*/
export function normalizeClientResponse(client: Client): ClientOutput {
// Normalize scopes - API returns space-separated string
const scopes =
typeof client.scopes === 'string'
? (client.scopes as string).split(' ')
: Array.isArray(client.scopes)
? client.scopes
: [];
const channels = Array.isArray(client.channels) ? client.channels : [];
// redirectUri can be returned as string or array from the API
const redirectUri = Array.isArray(client.redirectUri) ? client.redirectUri.join(', ') : (client.redirectUri ?? '');
return {
clientId: client.clientId ?? '',
name: client.name ?? '',
secret: client.secret,
scopes,
channels,
redirectUri,
callbackUri: client.callbackUri,
isPrivateClient: client.isPrivateClient ?? true,
};
}
/**
* Print client details in a formatted table.
*/
export function printClientDetails(output: ClientOutput, showSecret = true): void {
const ui = cliui({width: process.stdout.columns || 80});
const labelWidth = 14;
ui.div('');
ui.div({text: 'Client ID:', width: labelWidth}, {text: output.clientId});
ui.div({text: 'Name:', width: labelWidth}, {text: output.name});
ui.div({text: 'Private:', width: labelWidth}, {text: String(output.isPrivateClient)});
ui.div({text: 'Channels:', width: labelWidth}, {text: output.channels.join(', ')});
ui.div({text: 'Scopes:', width: labelWidth}, {text: output.scopes.join('\n' + ' '.repeat(labelWidth))});
ui.div({text: 'Redirect URI:', width: labelWidth}, {text: output.redirectUri});
if (output.callbackUri) {
ui.div({text: 'Callback URI:', width: labelWidth}, {text: output.callbackUri});
}
if (showSecret && output.secret) {
ui.div('');
ui.div({
text: t(
'commands.slas.client.create.secretWarning',
'IMPORTANT: Save the client secret - it will not be shown again:',
),
});
ui.div({text: 'Secret:', width: labelWidth}, {text: output.secret});
}
ux.stdout(ui.toString());
}
/**
* Format API error for display.
*/
export function formatApiError(error: unknown): string {
return typeof error === 'object' ? JSON.stringify(error) : String(error);
}
/**
* Base command for SLAS client operations.
* Provides common flags and helper methods.
*/
export abstract class SlasClientCommand<T extends typeof Command> extends OAuthCommand<T> {
static baseFlags = {
...OAuthCommand.baseFlags,
'tenant-id': Flags.string({
description: 'SLAS tenant ID (organization ID)',
env: 'SFCC_TENANT_ID',
required: true,
}),
};
/**
* Ensure tenant exists, creating it if necessary.
* This is required before creating SLAS clients.
*/
protected async ensureTenantExists(slasClient: SlasClient, tenantId: string): Promise<void> {
// Try to get the tenant first
const {error, response} = await slasClient.GET('/tenants/{tenantId}', {
params: {
path: {tenantId},
},
});
// If tenant exists, we're done
if (!error) {
return;
}
// Check if this is a "tenant not found" error (SLAS returns 400 with TenantNotFoundException)
const isTenantNotFound =
response.status === 404 ||
(response.status === 400 &&
typeof error === 'object' &&
error !== null &&
'exception_name' in error &&
(error as {exception_name?: string}).exception_name === 'TenantNotFoundException');
// If it's not a tenant-not-found error, something else went wrong
if (!isTenantNotFound) {
this.error(
t('commands.slas.client.create.tenantError', 'Failed to check tenant: {{message}}', {
message: formatApiError(error),
}),
);
}
// Tenant doesn't exist, create it with placeholder values
if (!this.jsonEnabled()) {
this.log(t('commands.slas.client.create.creatingTenant', 'Creating SLAS tenant {{tenantId}}...', {tenantId}));
}
const {error: createError} = await slasClient.PUT('/tenants/{tenantId}', {
params: {
path: {tenantId},
},
body: {
tenantId,
merchantName: 'B2C CLI Tenant',
description: 'Auto-created by b2c-cli',
contact: 'B2C CLI',
emailAddress: 'noreply@example.com',
phoneNo: '+1 000-000-0000',
},
});
if (createError) {
this.error(
t('commands.slas.client.create.tenantCreateError', 'Failed to create tenant: {{message}}', {
message: formatApiError(createError),
}),
);
}
if (!this.jsonEnabled()) {
this.log(t('commands.slas.client.create.tenantCreated', 'SLAS tenant created successfully.'));
}
}
/**
* Get the SLAS client, ensuring short code is configured.
*/
protected getSlasClient(): SlasClient {
const {shortCode} = this.resolvedConfig;
if (!shortCode) {
this.error(
t(
'error.shortCodeRequired',
'SCAPI short code required. Provide --short-code, set SFCC_SHORTCODE, or configure short-code in dw.json.',
),
);
}
const oauthStrategy = this.getOAuthStrategy();
return createSlasClient({shortCode}, oauthStrategy);
}
}