brainerce 1.25.0 → 1.26.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.
- package/README.md +37 -1
- package/dist/index.d.mts +387 -1
- package/dist/index.d.ts +387 -1
- package/dist/index.js +249 -3
- package/dist/index.mjs +245 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,14 +32,17 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BrainerceClient: () => BrainerceClient,
|
|
34
34
|
BrainerceError: () => BrainerceError,
|
|
35
|
+
RTL_LOCALES: () => RTL_LOCALES,
|
|
35
36
|
SDK_VERSION: () => SDK_VERSION,
|
|
36
37
|
createWebhookHandler: () => createWebhookHandler,
|
|
38
|
+
deriveSeoDescription: () => deriveSeoDescription,
|
|
37
39
|
enableDevGuards: () => enableDevGuards,
|
|
38
40
|
formatPrice: () => formatPrice,
|
|
39
41
|
getCartItemImage: () => getCartItemImage,
|
|
40
42
|
getCartItemName: () => getCartItemName,
|
|
41
43
|
getCartTotals: () => getCartTotals,
|
|
42
44
|
getDescriptionContent: () => getDescriptionContent,
|
|
45
|
+
getDirectionForLocale: () => getDirectionForLocale,
|
|
43
46
|
getPriceDisplay: () => formatPrice,
|
|
44
47
|
getProductCustomizationFields: () => getProductCustomizationFields,
|
|
45
48
|
getProductMetafield: () => getProductMetafield,
|
|
@@ -57,6 +60,7 @@ __export(index_exports, {
|
|
|
57
60
|
isWebhookEventType: () => isWebhookEventType,
|
|
58
61
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
59
62
|
safePaymentRedirect: () => safePaymentRedirect,
|
|
63
|
+
stripHtml: () => stripHtml,
|
|
60
64
|
verifyWebhook: () => verifyWebhook
|
|
61
65
|
});
|
|
62
66
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -183,6 +187,12 @@ var SDK_VERSION = "1.21.0";
|
|
|
183
187
|
// src/client.ts
|
|
184
188
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
185
189
|
var DEFAULT_TIMEOUT = 3e4;
|
|
190
|
+
var RTL_LOCALES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "yi"]);
|
|
191
|
+
function getDirectionForLocale(locale) {
|
|
192
|
+
if (!locale) return "ltr";
|
|
193
|
+
const primary = locale.split("-")[0].toLowerCase();
|
|
194
|
+
return RTL_LOCALES.has(primary) ? "rtl" : "ltr";
|
|
195
|
+
}
|
|
186
196
|
var BrainerceClient = class {
|
|
187
197
|
constructor(options) {
|
|
188
198
|
this.customerToken = null;
|
|
@@ -256,6 +266,204 @@ var BrainerceClient = class {
|
|
|
256
266
|
);
|
|
257
267
|
}
|
|
258
268
|
};
|
|
269
|
+
// -------------------- Content (typed merchant content) --------------------
|
|
270
|
+
/**
|
|
271
|
+
* Typed merchant content store: FAQ, Footer, Header, Announcement,
|
|
272
|
+
* Rich Text, and Page.
|
|
273
|
+
*
|
|
274
|
+
* Works in all three SDK modes (vibe-coded, storefront, admin):
|
|
275
|
+
* - **Public reads** (`get`, `list`, `getBySlug`): work in any mode.
|
|
276
|
+
* - **Write operations** (`create`, `update`, `publish`, `unpublish`,
|
|
277
|
+
* `remove`): admin mode only — they call `/api/v1/content/...` with
|
|
278
|
+
* the API key. Calling from storefront / vibe-coded mode throws.
|
|
279
|
+
*
|
|
280
|
+
* **Default key:** every type has `'main'` as its universal default key.
|
|
281
|
+
* Pass no argument to fetch the main entry; pass a custom key (e.g.
|
|
282
|
+
* `'shipping'`, `'holiday-2026'`) for topical entries.
|
|
283
|
+
*
|
|
284
|
+
* **404 contract:** public reads return `null` when no PUBLISHED row
|
|
285
|
+
* exists. Storefronts should render hard-coded fallbacks on `null` so
|
|
286
|
+
* the page never crashes when the merchant hasn't seeded yet.
|
|
287
|
+
*
|
|
288
|
+
* **Security:** RICH_TEXT, PAGE, and FAQ answers contain raw HTML.
|
|
289
|
+
* Always sanitize with isomorphic-dompurify (or equivalent) before
|
|
290
|
+
* rendering. The server does NOT pre-sanitize because some merchants
|
|
291
|
+
* embed iframes (e.g. YouTube videos).
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* // Storefront (public) — fetch the merchant's FAQ in Hebrew
|
|
296
|
+
* const faq = await client.content.faq.get('main', 'he');
|
|
297
|
+
* if (faq) {
|
|
298
|
+
* faq.data.items.forEach(({ question, answer }) => {
|
|
299
|
+
* // sanitize(answer) before injecting via dangerouslySetInnerHTML
|
|
300
|
+
* });
|
|
301
|
+
* }
|
|
302
|
+
*
|
|
303
|
+
* // Admin — create a shipping FAQ in DRAFT
|
|
304
|
+
* await client.content.faq.create({
|
|
305
|
+
* key: 'shipping',
|
|
306
|
+
* name: 'Shipping FAQ',
|
|
307
|
+
* data: { items: [{ question: '…', answer: '…' }] },
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
this.content = (() => {
|
|
312
|
+
const self = this;
|
|
313
|
+
const DEFAULT_KEY = "main";
|
|
314
|
+
function publicListPath(type) {
|
|
315
|
+
return `/content?type=${encodeURIComponent(type)}`;
|
|
316
|
+
}
|
|
317
|
+
function publicGetPath(type, key) {
|
|
318
|
+
return `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
|
|
319
|
+
}
|
|
320
|
+
function adminBase() {
|
|
321
|
+
return "/api/v1/content";
|
|
322
|
+
}
|
|
323
|
+
async function publicGet(type, key, locale) {
|
|
324
|
+
const query = locale ? { locale } : void 0;
|
|
325
|
+
const path = publicGetPath(type, key);
|
|
326
|
+
const onNotFound = (err) => {
|
|
327
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
328
|
+
throw err;
|
|
329
|
+
};
|
|
330
|
+
if (self.isVibeCodedMode()) {
|
|
331
|
+
return self.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
|
|
332
|
+
}
|
|
333
|
+
if (self.storeId && !self.apiKey) {
|
|
334
|
+
return self.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
|
|
335
|
+
}
|
|
336
|
+
throw new BrainerceError(
|
|
337
|
+
"content.<type>.get(key) is a public-read API. In admin mode, call client.content.list({ type }) and filter by key, or use client.content.findById(id).",
|
|
338
|
+
400
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
async function publicList(type, locale) {
|
|
342
|
+
const query = { type };
|
|
343
|
+
if (locale) query.locale = locale;
|
|
344
|
+
if (self.isVibeCodedMode()) {
|
|
345
|
+
return self.vibeCodedRequest("GET", "/content", void 0, query);
|
|
346
|
+
}
|
|
347
|
+
if (self.storeId && !self.apiKey) {
|
|
348
|
+
return self.storefrontRequest("GET", "/content", void 0, query);
|
|
349
|
+
}
|
|
350
|
+
return self.adminRequest(
|
|
351
|
+
"GET",
|
|
352
|
+
`${adminBase()}?type=${encodeURIComponent(type)}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
function requireAdmin(action) {
|
|
356
|
+
if (self.isVibeCodedMode() || self.storeId && !self.apiKey) {
|
|
357
|
+
throw new BrainerceError(
|
|
358
|
+
`client.content.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
|
|
359
|
+
403
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function createByType(type, input) {
|
|
364
|
+
requireAdmin("create");
|
|
365
|
+
return self.adminRequest("POST", adminBase(), { ...input, type });
|
|
366
|
+
}
|
|
367
|
+
async function updateById(id, input) {
|
|
368
|
+
requireAdmin("update");
|
|
369
|
+
return self.adminRequest(
|
|
370
|
+
"PATCH",
|
|
371
|
+
`${adminBase()}/${encodeURIComponent(id)}`,
|
|
372
|
+
input
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
async function publishById(id) {
|
|
376
|
+
requireAdmin("publish");
|
|
377
|
+
return self.adminRequest(
|
|
378
|
+
"POST",
|
|
379
|
+
`${adminBase()}/${encodeURIComponent(id)}/publish`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
async function unpublishById(id) {
|
|
383
|
+
requireAdmin("unpublish");
|
|
384
|
+
return self.adminRequest(
|
|
385
|
+
"POST",
|
|
386
|
+
`${adminBase()}/${encodeURIComponent(id)}/unpublish`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
async function removeById(id) {
|
|
390
|
+
requireAdmin("remove");
|
|
391
|
+
await self.adminRequest("DELETE", `${adminBase()}/${encodeURIComponent(id)}`);
|
|
392
|
+
}
|
|
393
|
+
function makeNamespace(type) {
|
|
394
|
+
return {
|
|
395
|
+
/**
|
|
396
|
+
* Fetch one PUBLISHED entry by key (defaults to `'main'`). Returns
|
|
397
|
+
* `null` on 404 — render a hard-coded fallback when the merchant
|
|
398
|
+
* hasn't seeded yet.
|
|
399
|
+
*/
|
|
400
|
+
get: (key = DEFAULT_KEY, locale) => publicGet(type, key, locale),
|
|
401
|
+
/** List all PUBLISHED entries of this type. */
|
|
402
|
+
list: (locale) => publicList(type, locale),
|
|
403
|
+
/** Create a new entry in DRAFT (admin mode). */
|
|
404
|
+
create: (input) => createByType(type, input)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
faq: makeNamespace("FAQ"),
|
|
409
|
+
footer: makeNamespace("FOOTER"),
|
|
410
|
+
header: makeNamespace("HEADER"),
|
|
411
|
+
announcement: makeNamespace("ANNOUNCEMENT"),
|
|
412
|
+
richText: makeNamespace("RICH_TEXT"),
|
|
413
|
+
page: {
|
|
414
|
+
...makeNamespace("PAGE"),
|
|
415
|
+
/**
|
|
416
|
+
* Fetch a PUBLISHED page by its `data.slug` (e.g. `'about'`).
|
|
417
|
+
* Returns `null` on 404. Use this from your `app/[slug]/page.tsx`
|
|
418
|
+
* catch-all route.
|
|
419
|
+
*/
|
|
420
|
+
getBySlug: async (slug, locale) => {
|
|
421
|
+
const query = locale ? { locale } : void 0;
|
|
422
|
+
const path = `/content/pages/by-slug/${encodeURIComponent(slug)}`;
|
|
423
|
+
const onNotFound = (err) => {
|
|
424
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
425
|
+
throw err;
|
|
426
|
+
};
|
|
427
|
+
if (self.isVibeCodedMode()) {
|
|
428
|
+
return self.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
|
|
429
|
+
}
|
|
430
|
+
if (self.storeId && !self.apiKey) {
|
|
431
|
+
return self.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
|
|
432
|
+
}
|
|
433
|
+
throw new BrainerceError(
|
|
434
|
+
"content.page.getBySlug() is a public-read API; not available in admin mode.",
|
|
435
|
+
400
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
// ---------- Admin operations (cross-type) ----------
|
|
440
|
+
/** Find a single row by its admin id (admin mode). */
|
|
441
|
+
findById: async (id) => {
|
|
442
|
+
requireAdmin("findById");
|
|
443
|
+
return self.adminRequest("GET", `${adminBase()}/${encodeURIComponent(id)}`);
|
|
444
|
+
},
|
|
445
|
+
/** List rows in admin mode with optional filters. */
|
|
446
|
+
listAdmin: async (filters) => {
|
|
447
|
+
requireAdmin("listAdmin");
|
|
448
|
+
const params = new URLSearchParams();
|
|
449
|
+
if (filters?.type) params.set("type", filters.type);
|
|
450
|
+
if (filters?.status) params.set("status", filters.status);
|
|
451
|
+
const qs = params.toString();
|
|
452
|
+
return self.adminRequest(
|
|
453
|
+
"GET",
|
|
454
|
+
qs ? `${adminBase()}?${qs}` : adminBase()
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
/** Replace `data` (and optional metadata) on an existing row. */
|
|
458
|
+
update: updateById,
|
|
459
|
+
/** Transition status DRAFT → PUBLISHED. */
|
|
460
|
+
publish: publishById,
|
|
461
|
+
/** Transition status PUBLISHED → DRAFT. */
|
|
462
|
+
unpublish: unpublishById,
|
|
463
|
+
/** Hard delete the row. Admin mode only. */
|
|
464
|
+
remove: removeById
|
|
465
|
+
};
|
|
466
|
+
})();
|
|
259
467
|
// -------------------- Local Cart (Client-Side for Guests) --------------------
|
|
260
468
|
// These methods store cart data in localStorage - NO API calls!
|
|
261
469
|
// Use for guest users in vibe-coded sites
|
|
@@ -667,6 +875,19 @@ var BrainerceClient = class {
|
|
|
667
875
|
}
|
|
668
876
|
return this.adminRequest("GET", "/api/v1/store");
|
|
669
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Resolve the script direction (`'ltr' | 'rtl'`) for a locale tag.
|
|
880
|
+
* Use this on `<html dir>` in your root layout — do not maintain a local
|
|
881
|
+
* RTL set in your storefront.
|
|
882
|
+
*
|
|
883
|
+
* @param locale - Optional BCP-47 tag. If omitted, falls back to the SDK
|
|
884
|
+
* client's configured locale (`new BrainerceClient({ locale: 'he' })`).
|
|
885
|
+
* @returns `'rtl'` for Arabic / Hebrew / Persian / Urdu / Yiddish family,
|
|
886
|
+
* `'ltr'` for everything else (including unknown locales).
|
|
887
|
+
*/
|
|
888
|
+
getStoreDirection(locale) {
|
|
889
|
+
return getDirectionForLocale(locale ?? this.locale);
|
|
890
|
+
}
|
|
670
891
|
// -------------------- Products --------------------
|
|
671
892
|
/**
|
|
672
893
|
* Get a list of products with pagination and filtering
|
|
@@ -780,10 +1001,11 @@ var BrainerceClient = class {
|
|
|
780
1001
|
*/
|
|
781
1002
|
async getProductBySlug(slug, options) {
|
|
782
1003
|
const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
|
|
1004
|
+
const encodedSlug = encodeURIComponent(slug);
|
|
783
1005
|
if (this.isVibeCodedMode()) {
|
|
784
1006
|
return this.vibeCodedRequest(
|
|
785
1007
|
"GET",
|
|
786
|
-
`/products/slug/${
|
|
1008
|
+
`/products/slug/${encodedSlug}`,
|
|
787
1009
|
void 0,
|
|
788
1010
|
void 0,
|
|
789
1011
|
headerOverrides
|
|
@@ -792,13 +1014,13 @@ var BrainerceClient = class {
|
|
|
792
1014
|
if (this.storeId && !this.apiKey) {
|
|
793
1015
|
return this.storefrontRequest(
|
|
794
1016
|
"GET",
|
|
795
|
-
`/products/slug/${
|
|
1017
|
+
`/products/slug/${encodedSlug}`,
|
|
796
1018
|
void 0,
|
|
797
1019
|
void 0,
|
|
798
1020
|
headerOverrides
|
|
799
1021
|
);
|
|
800
1022
|
}
|
|
801
|
-
return this.adminRequest("GET", `/api/v1/products/by-slug/${
|
|
1023
|
+
return this.adminRequest("GET", `/api/v1/products/by-slug/${encodedSlug}`);
|
|
802
1024
|
}
|
|
803
1025
|
/**
|
|
804
1026
|
* Get available categories for filtering products
|
|
@@ -7143,6 +7365,26 @@ function getDescriptionContent(product) {
|
|
|
7143
7365
|
}
|
|
7144
7366
|
return { text: product.description };
|
|
7145
7367
|
}
|
|
7368
|
+
function stripHtml(html) {
|
|
7369
|
+
if (!html) return "";
|
|
7370
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/'/gi, "'").replace(///gi, "/").replace(/\s+/g, " ").trim();
|
|
7371
|
+
}
|
|
7372
|
+
function deriveSeoDescription(product, options) {
|
|
7373
|
+
if (!product) return "";
|
|
7374
|
+
const maxLength = options?.maxLength ?? 160;
|
|
7375
|
+
const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
|
|
7376
|
+
if (authored) return authored;
|
|
7377
|
+
const plain = stripHtml(product.description);
|
|
7378
|
+
if (plain) {
|
|
7379
|
+
if (plain.length <= maxLength) return plain;
|
|
7380
|
+
const sliceLength = maxLength - 1;
|
|
7381
|
+
const slice = plain.slice(0, sliceLength);
|
|
7382
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
7383
|
+
const trimmed = lastSpace > maxLength * 0.5 ? slice.slice(0, lastSpace) : slice;
|
|
7384
|
+
return trimmed.replace(/[\s.,;:!?-]+$/u, "") + "\u2026";
|
|
7385
|
+
}
|
|
7386
|
+
return product.name ?? "";
|
|
7387
|
+
}
|
|
7146
7388
|
function getStockStatus(inventory, options) {
|
|
7147
7389
|
if (!inventory) {
|
|
7148
7390
|
return options?.outOfStockText ?? "Out of Stock";
|
|
@@ -7350,14 +7592,17 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
7350
7592
|
0 && (module.exports = {
|
|
7351
7593
|
BrainerceClient,
|
|
7352
7594
|
BrainerceError,
|
|
7595
|
+
RTL_LOCALES,
|
|
7353
7596
|
SDK_VERSION,
|
|
7354
7597
|
createWebhookHandler,
|
|
7598
|
+
deriveSeoDescription,
|
|
7355
7599
|
enableDevGuards,
|
|
7356
7600
|
formatPrice,
|
|
7357
7601
|
getCartItemImage,
|
|
7358
7602
|
getCartItemName,
|
|
7359
7603
|
getCartTotals,
|
|
7360
7604
|
getDescriptionContent,
|
|
7605
|
+
getDirectionForLocale,
|
|
7361
7606
|
getPriceDisplay,
|
|
7362
7607
|
getProductCustomizationFields,
|
|
7363
7608
|
getProductMetafield,
|
|
@@ -7375,5 +7620,6 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
7375
7620
|
isWebhookEventType,
|
|
7376
7621
|
parseWebhookEvent,
|
|
7377
7622
|
safePaymentRedirect,
|
|
7623
|
+
stripHtml,
|
|
7378
7624
|
verifyWebhook
|
|
7379
7625
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -120,6 +120,12 @@ var SDK_VERSION = "1.21.0";
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
122
122
|
var DEFAULT_TIMEOUT = 3e4;
|
|
123
|
+
var RTL_LOCALES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "yi"]);
|
|
124
|
+
function getDirectionForLocale(locale) {
|
|
125
|
+
if (!locale) return "ltr";
|
|
126
|
+
const primary = locale.split("-")[0].toLowerCase();
|
|
127
|
+
return RTL_LOCALES.has(primary) ? "rtl" : "ltr";
|
|
128
|
+
}
|
|
123
129
|
var BrainerceClient = class {
|
|
124
130
|
constructor(options) {
|
|
125
131
|
this.customerToken = null;
|
|
@@ -193,6 +199,204 @@ var BrainerceClient = class {
|
|
|
193
199
|
);
|
|
194
200
|
}
|
|
195
201
|
};
|
|
202
|
+
// -------------------- Content (typed merchant content) --------------------
|
|
203
|
+
/**
|
|
204
|
+
* Typed merchant content store: FAQ, Footer, Header, Announcement,
|
|
205
|
+
* Rich Text, and Page.
|
|
206
|
+
*
|
|
207
|
+
* Works in all three SDK modes (vibe-coded, storefront, admin):
|
|
208
|
+
* - **Public reads** (`get`, `list`, `getBySlug`): work in any mode.
|
|
209
|
+
* - **Write operations** (`create`, `update`, `publish`, `unpublish`,
|
|
210
|
+
* `remove`): admin mode only — they call `/api/v1/content/...` with
|
|
211
|
+
* the API key. Calling from storefront / vibe-coded mode throws.
|
|
212
|
+
*
|
|
213
|
+
* **Default key:** every type has `'main'` as its universal default key.
|
|
214
|
+
* Pass no argument to fetch the main entry; pass a custom key (e.g.
|
|
215
|
+
* `'shipping'`, `'holiday-2026'`) for topical entries.
|
|
216
|
+
*
|
|
217
|
+
* **404 contract:** public reads return `null` when no PUBLISHED row
|
|
218
|
+
* exists. Storefronts should render hard-coded fallbacks on `null` so
|
|
219
|
+
* the page never crashes when the merchant hasn't seeded yet.
|
|
220
|
+
*
|
|
221
|
+
* **Security:** RICH_TEXT, PAGE, and FAQ answers contain raw HTML.
|
|
222
|
+
* Always sanitize with isomorphic-dompurify (or equivalent) before
|
|
223
|
+
* rendering. The server does NOT pre-sanitize because some merchants
|
|
224
|
+
* embed iframes (e.g. YouTube videos).
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* // Storefront (public) — fetch the merchant's FAQ in Hebrew
|
|
229
|
+
* const faq = await client.content.faq.get('main', 'he');
|
|
230
|
+
* if (faq) {
|
|
231
|
+
* faq.data.items.forEach(({ question, answer }) => {
|
|
232
|
+
* // sanitize(answer) before injecting via dangerouslySetInnerHTML
|
|
233
|
+
* });
|
|
234
|
+
* }
|
|
235
|
+
*
|
|
236
|
+
* // Admin — create a shipping FAQ in DRAFT
|
|
237
|
+
* await client.content.faq.create({
|
|
238
|
+
* key: 'shipping',
|
|
239
|
+
* name: 'Shipping FAQ',
|
|
240
|
+
* data: { items: [{ question: '…', answer: '…' }] },
|
|
241
|
+
* });
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
this.content = (() => {
|
|
245
|
+
const self = this;
|
|
246
|
+
const DEFAULT_KEY = "main";
|
|
247
|
+
function publicListPath(type) {
|
|
248
|
+
return `/content?type=${encodeURIComponent(type)}`;
|
|
249
|
+
}
|
|
250
|
+
function publicGetPath(type, key) {
|
|
251
|
+
return `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
|
|
252
|
+
}
|
|
253
|
+
function adminBase() {
|
|
254
|
+
return "/api/v1/content";
|
|
255
|
+
}
|
|
256
|
+
async function publicGet(type, key, locale) {
|
|
257
|
+
const query = locale ? { locale } : void 0;
|
|
258
|
+
const path = publicGetPath(type, key);
|
|
259
|
+
const onNotFound = (err) => {
|
|
260
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
261
|
+
throw err;
|
|
262
|
+
};
|
|
263
|
+
if (self.isVibeCodedMode()) {
|
|
264
|
+
return self.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
|
|
265
|
+
}
|
|
266
|
+
if (self.storeId && !self.apiKey) {
|
|
267
|
+
return self.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
|
|
268
|
+
}
|
|
269
|
+
throw new BrainerceError(
|
|
270
|
+
"content.<type>.get(key) is a public-read API. In admin mode, call client.content.list({ type }) and filter by key, or use client.content.findById(id).",
|
|
271
|
+
400
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
async function publicList(type, locale) {
|
|
275
|
+
const query = { type };
|
|
276
|
+
if (locale) query.locale = locale;
|
|
277
|
+
if (self.isVibeCodedMode()) {
|
|
278
|
+
return self.vibeCodedRequest("GET", "/content", void 0, query);
|
|
279
|
+
}
|
|
280
|
+
if (self.storeId && !self.apiKey) {
|
|
281
|
+
return self.storefrontRequest("GET", "/content", void 0, query);
|
|
282
|
+
}
|
|
283
|
+
return self.adminRequest(
|
|
284
|
+
"GET",
|
|
285
|
+
`${adminBase()}?type=${encodeURIComponent(type)}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
function requireAdmin(action) {
|
|
289
|
+
if (self.isVibeCodedMode() || self.storeId && !self.apiKey) {
|
|
290
|
+
throw new BrainerceError(
|
|
291
|
+
`client.content.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
|
|
292
|
+
403
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function createByType(type, input) {
|
|
297
|
+
requireAdmin("create");
|
|
298
|
+
return self.adminRequest("POST", adminBase(), { ...input, type });
|
|
299
|
+
}
|
|
300
|
+
async function updateById(id, input) {
|
|
301
|
+
requireAdmin("update");
|
|
302
|
+
return self.adminRequest(
|
|
303
|
+
"PATCH",
|
|
304
|
+
`${adminBase()}/${encodeURIComponent(id)}`,
|
|
305
|
+
input
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
async function publishById(id) {
|
|
309
|
+
requireAdmin("publish");
|
|
310
|
+
return self.adminRequest(
|
|
311
|
+
"POST",
|
|
312
|
+
`${adminBase()}/${encodeURIComponent(id)}/publish`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async function unpublishById(id) {
|
|
316
|
+
requireAdmin("unpublish");
|
|
317
|
+
return self.adminRequest(
|
|
318
|
+
"POST",
|
|
319
|
+
`${adminBase()}/${encodeURIComponent(id)}/unpublish`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
async function removeById(id) {
|
|
323
|
+
requireAdmin("remove");
|
|
324
|
+
await self.adminRequest("DELETE", `${adminBase()}/${encodeURIComponent(id)}`);
|
|
325
|
+
}
|
|
326
|
+
function makeNamespace(type) {
|
|
327
|
+
return {
|
|
328
|
+
/**
|
|
329
|
+
* Fetch one PUBLISHED entry by key (defaults to `'main'`). Returns
|
|
330
|
+
* `null` on 404 — render a hard-coded fallback when the merchant
|
|
331
|
+
* hasn't seeded yet.
|
|
332
|
+
*/
|
|
333
|
+
get: (key = DEFAULT_KEY, locale) => publicGet(type, key, locale),
|
|
334
|
+
/** List all PUBLISHED entries of this type. */
|
|
335
|
+
list: (locale) => publicList(type, locale),
|
|
336
|
+
/** Create a new entry in DRAFT (admin mode). */
|
|
337
|
+
create: (input) => createByType(type, input)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
faq: makeNamespace("FAQ"),
|
|
342
|
+
footer: makeNamespace("FOOTER"),
|
|
343
|
+
header: makeNamespace("HEADER"),
|
|
344
|
+
announcement: makeNamespace("ANNOUNCEMENT"),
|
|
345
|
+
richText: makeNamespace("RICH_TEXT"),
|
|
346
|
+
page: {
|
|
347
|
+
...makeNamespace("PAGE"),
|
|
348
|
+
/**
|
|
349
|
+
* Fetch a PUBLISHED page by its `data.slug` (e.g. `'about'`).
|
|
350
|
+
* Returns `null` on 404. Use this from your `app/[slug]/page.tsx`
|
|
351
|
+
* catch-all route.
|
|
352
|
+
*/
|
|
353
|
+
getBySlug: async (slug, locale) => {
|
|
354
|
+
const query = locale ? { locale } : void 0;
|
|
355
|
+
const path = `/content/pages/by-slug/${encodeURIComponent(slug)}`;
|
|
356
|
+
const onNotFound = (err) => {
|
|
357
|
+
if (err instanceof BrainerceError && err.statusCode === 404) return null;
|
|
358
|
+
throw err;
|
|
359
|
+
};
|
|
360
|
+
if (self.isVibeCodedMode()) {
|
|
361
|
+
return self.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
|
|
362
|
+
}
|
|
363
|
+
if (self.storeId && !self.apiKey) {
|
|
364
|
+
return self.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
|
|
365
|
+
}
|
|
366
|
+
throw new BrainerceError(
|
|
367
|
+
"content.page.getBySlug() is a public-read API; not available in admin mode.",
|
|
368
|
+
400
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
// ---------- Admin operations (cross-type) ----------
|
|
373
|
+
/** Find a single row by its admin id (admin mode). */
|
|
374
|
+
findById: async (id) => {
|
|
375
|
+
requireAdmin("findById");
|
|
376
|
+
return self.adminRequest("GET", `${adminBase()}/${encodeURIComponent(id)}`);
|
|
377
|
+
},
|
|
378
|
+
/** List rows in admin mode with optional filters. */
|
|
379
|
+
listAdmin: async (filters) => {
|
|
380
|
+
requireAdmin("listAdmin");
|
|
381
|
+
const params = new URLSearchParams();
|
|
382
|
+
if (filters?.type) params.set("type", filters.type);
|
|
383
|
+
if (filters?.status) params.set("status", filters.status);
|
|
384
|
+
const qs = params.toString();
|
|
385
|
+
return self.adminRequest(
|
|
386
|
+
"GET",
|
|
387
|
+
qs ? `${adminBase()}?${qs}` : adminBase()
|
|
388
|
+
);
|
|
389
|
+
},
|
|
390
|
+
/** Replace `data` (and optional metadata) on an existing row. */
|
|
391
|
+
update: updateById,
|
|
392
|
+
/** Transition status DRAFT → PUBLISHED. */
|
|
393
|
+
publish: publishById,
|
|
394
|
+
/** Transition status PUBLISHED → DRAFT. */
|
|
395
|
+
unpublish: unpublishById,
|
|
396
|
+
/** Hard delete the row. Admin mode only. */
|
|
397
|
+
remove: removeById
|
|
398
|
+
};
|
|
399
|
+
})();
|
|
196
400
|
// -------------------- Local Cart (Client-Side for Guests) --------------------
|
|
197
401
|
// These methods store cart data in localStorage - NO API calls!
|
|
198
402
|
// Use for guest users in vibe-coded sites
|
|
@@ -604,6 +808,19 @@ var BrainerceClient = class {
|
|
|
604
808
|
}
|
|
605
809
|
return this.adminRequest("GET", "/api/v1/store");
|
|
606
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Resolve the script direction (`'ltr' | 'rtl'`) for a locale tag.
|
|
813
|
+
* Use this on `<html dir>` in your root layout — do not maintain a local
|
|
814
|
+
* RTL set in your storefront.
|
|
815
|
+
*
|
|
816
|
+
* @param locale - Optional BCP-47 tag. If omitted, falls back to the SDK
|
|
817
|
+
* client's configured locale (`new BrainerceClient({ locale: 'he' })`).
|
|
818
|
+
* @returns `'rtl'` for Arabic / Hebrew / Persian / Urdu / Yiddish family,
|
|
819
|
+
* `'ltr'` for everything else (including unknown locales).
|
|
820
|
+
*/
|
|
821
|
+
getStoreDirection(locale) {
|
|
822
|
+
return getDirectionForLocale(locale ?? this.locale);
|
|
823
|
+
}
|
|
607
824
|
// -------------------- Products --------------------
|
|
608
825
|
/**
|
|
609
826
|
* Get a list of products with pagination and filtering
|
|
@@ -717,10 +934,11 @@ var BrainerceClient = class {
|
|
|
717
934
|
*/
|
|
718
935
|
async getProductBySlug(slug, options) {
|
|
719
936
|
const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
|
|
937
|
+
const encodedSlug = encodeURIComponent(slug);
|
|
720
938
|
if (this.isVibeCodedMode()) {
|
|
721
939
|
return this.vibeCodedRequest(
|
|
722
940
|
"GET",
|
|
723
|
-
`/products/slug/${
|
|
941
|
+
`/products/slug/${encodedSlug}`,
|
|
724
942
|
void 0,
|
|
725
943
|
void 0,
|
|
726
944
|
headerOverrides
|
|
@@ -729,13 +947,13 @@ var BrainerceClient = class {
|
|
|
729
947
|
if (this.storeId && !this.apiKey) {
|
|
730
948
|
return this.storefrontRequest(
|
|
731
949
|
"GET",
|
|
732
|
-
`/products/slug/${
|
|
950
|
+
`/products/slug/${encodedSlug}`,
|
|
733
951
|
void 0,
|
|
734
952
|
void 0,
|
|
735
953
|
headerOverrides
|
|
736
954
|
);
|
|
737
955
|
}
|
|
738
|
-
return this.adminRequest("GET", `/api/v1/products/by-slug/${
|
|
956
|
+
return this.adminRequest("GET", `/api/v1/products/by-slug/${encodedSlug}`);
|
|
739
957
|
}
|
|
740
958
|
/**
|
|
741
959
|
* Get available categories for filtering products
|
|
@@ -7080,6 +7298,26 @@ function getDescriptionContent(product) {
|
|
|
7080
7298
|
}
|
|
7081
7299
|
return { text: product.description };
|
|
7082
7300
|
}
|
|
7301
|
+
function stripHtml(html) {
|
|
7302
|
+
if (!html) return "";
|
|
7303
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/'/gi, "'").replace(///gi, "/").replace(/\s+/g, " ").trim();
|
|
7304
|
+
}
|
|
7305
|
+
function deriveSeoDescription(product, options) {
|
|
7306
|
+
if (!product) return "";
|
|
7307
|
+
const maxLength = options?.maxLength ?? 160;
|
|
7308
|
+
const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
|
|
7309
|
+
if (authored) return authored;
|
|
7310
|
+
const plain = stripHtml(product.description);
|
|
7311
|
+
if (plain) {
|
|
7312
|
+
if (plain.length <= maxLength) return plain;
|
|
7313
|
+
const sliceLength = maxLength - 1;
|
|
7314
|
+
const slice = plain.slice(0, sliceLength);
|
|
7315
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
7316
|
+
const trimmed = lastSpace > maxLength * 0.5 ? slice.slice(0, lastSpace) : slice;
|
|
7317
|
+
return trimmed.replace(/[\s.,;:!?-]+$/u, "") + "\u2026";
|
|
7318
|
+
}
|
|
7319
|
+
return product.name ?? "";
|
|
7320
|
+
}
|
|
7083
7321
|
function getStockStatus(inventory, options) {
|
|
7084
7322
|
if (!inventory) {
|
|
7085
7323
|
return options?.outOfStockText ?? "Out of Stock";
|
|
@@ -7286,14 +7524,17 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
7286
7524
|
export {
|
|
7287
7525
|
BrainerceClient,
|
|
7288
7526
|
BrainerceError,
|
|
7527
|
+
RTL_LOCALES,
|
|
7289
7528
|
SDK_VERSION,
|
|
7290
7529
|
createWebhookHandler,
|
|
7530
|
+
deriveSeoDescription,
|
|
7291
7531
|
enableDevGuards,
|
|
7292
7532
|
formatPrice,
|
|
7293
7533
|
getCartItemImage,
|
|
7294
7534
|
getCartItemName,
|
|
7295
7535
|
getCartTotals,
|
|
7296
7536
|
getDescriptionContent,
|
|
7537
|
+
getDirectionForLocale,
|
|
7297
7538
|
formatPrice as getPriceDisplay,
|
|
7298
7539
|
getProductCustomizationFields,
|
|
7299
7540
|
getProductMetafield,
|
|
@@ -7311,5 +7552,6 @@ export {
|
|
|
7311
7552
|
isWebhookEventType,
|
|
7312
7553
|
parseWebhookEvent,
|
|
7313
7554
|
safePaymentRedirect,
|
|
7555
|
+
stripHtml,
|
|
7314
7556
|
verifyWebhook
|
|
7315
7557
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|