@unboundcx/sdk 4.8.18 → 4.9.2

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,352 @@
1
+ import { internalRequest } from '../base.js';
2
+
3
+ const LOGO_KINDS = ['main', 'icon', 'favicon'];
4
+
5
+ const EXT_CONTENT_TYPES = {
6
+ png: 'image/png',
7
+ jpg: 'image/jpeg',
8
+ jpeg: 'image/jpeg',
9
+ gif: 'image/gif',
10
+ svg: 'image/svg+xml',
11
+ webp: 'image/webp',
12
+ ico: 'image/x-icon',
13
+ };
14
+
15
+ function guessContentType(fileName) {
16
+ const ext = (fileName || '').split('.').pop()?.toLowerCase();
17
+ return EXT_CONTENT_TYPES[ext] || 'application/octet-stream';
18
+ }
19
+
20
+ /**
21
+ * Public `branding` service — white-label brand settings, logo/domain
22
+ * management, and per-brand email template overrides.
23
+ *
24
+ * @see /workspace/code/app1/plans/white-label-plan.md §3
25
+ * @see app1-api src/services/branding/routes.js
26
+ */
27
+ export class BrandingService {
28
+ constructor(sdk) {
29
+ this.sdk = sdk;
30
+ this.emailTemplates = new BrandingEmailTemplatesService(sdk);
31
+ }
32
+
33
+ /**
34
+ * Host-resolved brand for the current request (public-safe fields only).
35
+ * No auth required.
36
+ *
37
+ * @returns {Promise<Object>} Brand
38
+ */
39
+ async current() {
40
+ const result = await internalRequest(this.sdk, '/branding/current', 'GET', {});
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Get a brand by id. Requires brand-owner auth.
46
+ *
47
+ * @param {string} id - Brand id
48
+ * @returns {Promise<Object>} Brand
49
+ */
50
+ async get(id) {
51
+ this.sdk.validateParams(
52
+ { id },
53
+ { id: { type: 'string', required: true } },
54
+ );
55
+
56
+ const result = await internalRequest(this.sdk, `/branding/${id}`, 'GET', {});
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Update a brand's settings. Requires brand-owner auth.
62
+ *
63
+ * @param {string} id - Brand id
64
+ * @param {Object} patch - Whitelisted fields to update (displayName, colors,
65
+ * baseUrl/baseAppUrl/enrollmentUrl, primaryEmail/supportEmail/supportPhone, etc.)
66
+ * @returns {Promise<Object>} Updated brand
67
+ */
68
+ async update(id, patch) {
69
+ this.sdk.validateParams(
70
+ { id, patch },
71
+ {
72
+ id: { type: 'string', required: true },
73
+ patch: { type: 'object', required: true },
74
+ },
75
+ );
76
+
77
+ const params = {
78
+ body: patch,
79
+ };
80
+
81
+ const result = await internalRequest(this.sdk, `/branding/${id}`, 'PATCH', params);
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Upload a brand logo image. Requires brand-owner auth.
87
+ *
88
+ * @param {string} id - Brand id
89
+ * @param {('main'|'icon'|'favicon')} kind - Which logo slot to upload into
90
+ * @param {(Buffer|Blob|File|{buffer: Buffer, fileName?: string, originalname?: string, contentType?: string, mimetype?: string})} file -
91
+ * The image. In Node, pass a Buffer (a generic filename is used) or a
92
+ * multer-style object (`{ buffer, originalname, mimetype }`). In the
93
+ * browser, pass a `File`/`Blob` (its `.name`/`.type` are used).
94
+ * @returns {Promise<Object>} Updated brand with the new logo URL
95
+ */
96
+ async uploadLogo(id, kind, file) {
97
+ this.sdk.validateParams(
98
+ { id, kind, file },
99
+ {
100
+ id: { type: 'string', required: true },
101
+ kind: { type: 'string', required: true },
102
+ file: { type: 'object', required: true },
103
+ },
104
+ );
105
+
106
+ if (!LOGO_KINDS.includes(kind)) {
107
+ throw new Error(
108
+ `branding.uploadLogo: kind must be one of ${LOGO_KINDS.join(', ')}, got: ${kind}`,
109
+ );
110
+ }
111
+
112
+ const { body, headers } = this._buildLogoFormData(file);
113
+
114
+ const params = {
115
+ body,
116
+ headers,
117
+ };
118
+
119
+ const result = await internalRequest(
120
+ this.sdk,
121
+ `/branding/${id}/logos/${kind}`,
122
+ 'POST',
123
+ params,
124
+ true,
125
+ );
126
+ return result;
127
+ }
128
+
129
+ // Private helper — builds a single-file multipart body under field name
130
+ // "file" (matches app1-api's `upload.single('file')`). Mirrors the
131
+ // Node/browser split in services/storage.js's upload helpers, trimmed to
132
+ // the single-small-image case (no streaming/progress paths needed here).
133
+ _buildLogoFormData(file) {
134
+ const isNode = typeof window === 'undefined';
135
+
136
+ if (!isNode) {
137
+ const formData = new FormData();
138
+ const fileName = file?.name || 'logo';
139
+ formData.append('file', file, fileName);
140
+ return { body: formData, headers: {} };
141
+ }
142
+
143
+ const buffer = Buffer.isBuffer(file) ? file : file?.buffer;
144
+ if (!Buffer.isBuffer(buffer)) {
145
+ throw new Error(
146
+ 'branding.uploadLogo: file must be a Buffer, or an object with a Buffer .buffer property, in Node',
147
+ );
148
+ }
149
+ const fileName = file?.fileName || file?.originalname || 'logo';
150
+ const contentType =
151
+ file?.contentType || file?.mimetype || guessContentType(fileName);
152
+
153
+ const boundary = `----formdata-${Date.now()}-${Math.random().toString(36).slice(2)}`;
154
+ const CRLF = '\r\n';
155
+
156
+ const header = Buffer.from(
157
+ `--${boundary}${CRLF}` +
158
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"${CRLF}` +
159
+ `Content-Type: ${contentType}${CRLF}${CRLF}`,
160
+ 'utf8',
161
+ );
162
+ const footer = Buffer.from(`${CRLF}--${boundary}--${CRLF}`, 'utf8');
163
+ const body = Buffer.concat([header, buffer, footer]);
164
+
165
+ return {
166
+ body,
167
+ headers: { 'content-type': `multipart/form-data; boundary=${boundary}` },
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Trigger DNS verification of a brand's custom domain(s). Requires
173
+ * brand-owner auth.
174
+ *
175
+ * @param {string} id - Brand id
176
+ * @returns {Promise<Object>} Verification result / updated `domainStatus`
177
+ */
178
+ async verifyDomain(id) {
179
+ this.sdk.validateParams(
180
+ { id },
181
+ { id: { type: 'string', required: true } },
182
+ );
183
+
184
+ const result = await internalRequest(this.sdk, `/branding/${id}/domains/verify`, 'POST', {});
185
+ return result;
186
+ }
187
+
188
+ /**
189
+ * List accounts on a brand. Requires brand-owner auth.
190
+ *
191
+ * @param {string} id - Brand id
192
+ * @returns {Promise<Array>} Accounts
193
+ */
194
+ async accounts(id) {
195
+ this.sdk.validateParams(
196
+ { id },
197
+ { id: { type: 'string', required: true } },
198
+ );
199
+
200
+ const result = await internalRequest(this.sdk, `/branding/${id}/accounts`, 'GET', {});
201
+ return result;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Per-brand system email template overrides. `sdk.branding.emailTemplates.*`.
207
+ * A brand template is a system default + optional brand override; these
208
+ * methods manage the override.
209
+ */
210
+ export class BrandingEmailTemplatesService {
211
+ constructor(sdk) {
212
+ this.sdk = sdk;
213
+ }
214
+
215
+ /**
216
+ * List all system email template types for a brand, each with a
217
+ * `source: 'default'|'override'` flag.
218
+ *
219
+ * @param {string} id - Brand id
220
+ * @returns {Promise<Array>} Templates
221
+ */
222
+ async list(id) {
223
+ this.sdk.validateParams(
224
+ { id },
225
+ { id: { type: 'string', required: true } },
226
+ );
227
+
228
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates`, 'GET', {});
229
+ return result;
230
+ }
231
+
232
+ /**
233
+ * Get the resolved template (override, falling back to system default)
234
+ * for one type.
235
+ *
236
+ * @param {string} id - Brand id
237
+ * @param {string} type - Template type (newUser, newUserInvite, verification, forgotPassword, passwordChanged)
238
+ * @returns {Promise<Object>} Resolved template, with `isOverride` flag
239
+ */
240
+ async get(id, type) {
241
+ this.sdk.validateParams(
242
+ { id, type },
243
+ {
244
+ id: { type: 'string', required: true },
245
+ type: { type: 'string', required: true },
246
+ },
247
+ );
248
+
249
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates/${type}`, 'GET', {});
250
+ return result;
251
+ }
252
+
253
+ /**
254
+ * Create or replace a brand's override for one template type.
255
+ *
256
+ * @param {string} id - Brand id
257
+ * @param {string} type - Template type
258
+ * @param {Object} body - Template fields (fromEmail, subject, html, regions, redirects, ...)
259
+ * @returns {Promise<Object>} Updated override
260
+ */
261
+ async update(id, type, body) {
262
+ this.sdk.validateParams(
263
+ { id, type, body },
264
+ {
265
+ id: { type: 'string', required: true },
266
+ type: { type: 'string', required: true },
267
+ body: { type: 'object', required: true },
268
+ },
269
+ );
270
+
271
+ const params = {
272
+ body,
273
+ };
274
+
275
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates/${type}`, 'PUT', params);
276
+ return result;
277
+ }
278
+
279
+ /**
280
+ * Remove a brand's override for one template type, reverting to the
281
+ * system default.
282
+ *
283
+ * @param {string} id - Brand id
284
+ * @param {string} type - Template type
285
+ * @returns {Promise<Object>} Confirmation
286
+ */
287
+ async reset(id, type) {
288
+ this.sdk.validateParams(
289
+ { id, type },
290
+ {
291
+ id: { type: 'string', required: true },
292
+ type: { type: 'string', required: true },
293
+ },
294
+ );
295
+
296
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates/${type}`, 'DELETE', {});
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * Render a template (override or draft `body`) with sample variables for
302
+ * preview, without saving.
303
+ *
304
+ * @param {string} id - Brand id
305
+ * @param {string} type - Template type
306
+ * @param {Object} body - Draft template fields to render
307
+ * @returns {Promise<Object>} Rendered preview
308
+ */
309
+ async preview(id, type, body) {
310
+ this.sdk.validateParams(
311
+ { id, type, body },
312
+ {
313
+ id: { type: 'string', required: true },
314
+ type: { type: 'string', required: true },
315
+ body: { type: 'object', required: true },
316
+ },
317
+ );
318
+
319
+ const params = {
320
+ body,
321
+ };
322
+
323
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates/${type}/preview`, 'POST', params);
324
+ return result;
325
+ }
326
+
327
+ /**
328
+ * Send a test email of a template to an address.
329
+ *
330
+ * @param {string} id - Brand id
331
+ * @param {string} type - Template type
332
+ * @param {string} to - Destination email address
333
+ * @returns {Promise<Object>} Send confirmation
334
+ */
335
+ async sendTest(id, type, to) {
336
+ this.sdk.validateParams(
337
+ { id, type, to },
338
+ {
339
+ id: { type: 'string', required: true },
340
+ type: { type: 'string', required: true },
341
+ to: { type: 'string', required: true },
342
+ },
343
+ );
344
+
345
+ const params = {
346
+ body: { to },
347
+ };
348
+
349
+ const result = await internalRequest(this.sdk, `/branding/${id}/email-templates/${type}/test`, 'POST', params);
350
+ return result;
351
+ }
352
+ }