brainerce 1.25.1 → 1.27.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/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,197 @@ 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 DEFAULT_KEY = "main";
313
+ const publicGetPath = (type, key) => `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
314
+ const adminBase = () => "/api/v1/content";
315
+ const publicGet = async (type, key, locale) => {
316
+ const query = locale ? { locale } : void 0;
317
+ const path = publicGetPath(type, key);
318
+ const onNotFound = (err) => {
319
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
320
+ throw err;
321
+ };
322
+ if (this.isVibeCodedMode()) {
323
+ return this.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
324
+ }
325
+ if (this.storeId && !this.apiKey) {
326
+ return this.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
327
+ }
328
+ throw new BrainerceError(
329
+ "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).",
330
+ 400
331
+ );
332
+ };
333
+ const publicList = async (type, locale) => {
334
+ const query = { type };
335
+ if (locale) query.locale = locale;
336
+ if (this.isVibeCodedMode()) {
337
+ return this.vibeCodedRequest("GET", "/content", void 0, query);
338
+ }
339
+ if (this.storeId && !this.apiKey) {
340
+ return this.storefrontRequest("GET", "/content", void 0, query);
341
+ }
342
+ return this.adminRequest(
343
+ "GET",
344
+ `${adminBase()}?type=${encodeURIComponent(type)}`
345
+ );
346
+ };
347
+ const requireAdmin = (action) => {
348
+ if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
349
+ throw new BrainerceError(
350
+ `client.content.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
351
+ 403
352
+ );
353
+ }
354
+ };
355
+ const createByType = async (type, input) => {
356
+ requireAdmin("create");
357
+ return this.adminRequest("POST", adminBase(), { ...input, type });
358
+ };
359
+ const updateById = async (id, input) => {
360
+ requireAdmin("update");
361
+ return this.adminRequest(
362
+ "PATCH",
363
+ `${adminBase()}/${encodeURIComponent(id)}`,
364
+ input
365
+ );
366
+ };
367
+ const publishById = async (id) => {
368
+ requireAdmin("publish");
369
+ return this.adminRequest("POST", `${adminBase()}/${encodeURIComponent(id)}/publish`);
370
+ };
371
+ const unpublishById = async (id) => {
372
+ requireAdmin("unpublish");
373
+ return this.adminRequest(
374
+ "POST",
375
+ `${adminBase()}/${encodeURIComponent(id)}/unpublish`
376
+ );
377
+ };
378
+ const removeById = async (id) => {
379
+ requireAdmin("remove");
380
+ await this.adminRequest("DELETE", `${adminBase()}/${encodeURIComponent(id)}`);
381
+ };
382
+ function makeNamespace(type) {
383
+ return {
384
+ /**
385
+ * Fetch one PUBLISHED entry by key (defaults to `'main'`). Returns
386
+ * `null` on 404 — render a hard-coded fallback when the merchant
387
+ * hasn't seeded yet.
388
+ */
389
+ get: (key = DEFAULT_KEY, locale) => publicGet(type, key, locale),
390
+ /** List all PUBLISHED entries of this type. */
391
+ list: (locale) => publicList(type, locale),
392
+ /** Create a new entry in DRAFT (admin mode). */
393
+ create: (input) => createByType(type, input)
394
+ };
395
+ }
396
+ return {
397
+ faq: makeNamespace("FAQ"),
398
+ footer: makeNamespace("FOOTER"),
399
+ header: makeNamespace("HEADER"),
400
+ announcement: makeNamespace("ANNOUNCEMENT"),
401
+ richText: makeNamespace("RICH_TEXT"),
402
+ page: {
403
+ ...makeNamespace("PAGE"),
404
+ /**
405
+ * Fetch a PUBLISHED page by its `data.slug` (e.g. `'about'`).
406
+ * Returns `null` on 404. Use this from your `app/[slug]/page.tsx`
407
+ * catch-all route.
408
+ */
409
+ getBySlug: async (slug, locale) => {
410
+ const query = locale ? { locale } : void 0;
411
+ const path = `/content/pages/by-slug/${encodeURIComponent(slug)}`;
412
+ const onNotFound = (err) => {
413
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
414
+ throw err;
415
+ };
416
+ if (this.isVibeCodedMode()) {
417
+ return this.vibeCodedRequest("GET", path, void 0, query).catch(
418
+ onNotFound
419
+ );
420
+ }
421
+ if (this.storeId && !this.apiKey) {
422
+ return this.storefrontRequest("GET", path, void 0, query).catch(
423
+ onNotFound
424
+ );
425
+ }
426
+ throw new BrainerceError(
427
+ "content.page.getBySlug() is a public-read API; not available in admin mode.",
428
+ 400
429
+ );
430
+ }
431
+ },
432
+ // ---------- Admin operations (cross-type) ----------
433
+ /** Find a single row by its admin id (admin mode). */
434
+ findById: async (id) => {
435
+ requireAdmin("findById");
436
+ return this.adminRequest("GET", `${adminBase()}/${encodeURIComponent(id)}`);
437
+ },
438
+ /** List rows in admin mode with optional filters. */
439
+ listAdmin: async (filters) => {
440
+ requireAdmin("listAdmin");
441
+ const params = new URLSearchParams();
442
+ if (filters?.type) params.set("type", filters.type);
443
+ if (filters?.status) params.set("status", filters.status);
444
+ const qs = params.toString();
445
+ return this.adminRequest(
446
+ "GET",
447
+ qs ? `${adminBase()}?${qs}` : adminBase()
448
+ );
449
+ },
450
+ /** Replace `data` (and optional metadata) on an existing row. */
451
+ update: updateById,
452
+ /** Transition status DRAFT → PUBLISHED. */
453
+ publish: publishById,
454
+ /** Transition status PUBLISHED → DRAFT. */
455
+ unpublish: unpublishById,
456
+ /** Hard delete the row. Admin mode only. */
457
+ remove: removeById
458
+ };
459
+ })();
259
460
  // -------------------- Local Cart (Client-Side for Guests) --------------------
260
461
  // These methods store cart data in localStorage - NO API calls!
261
462
  // Use for guest users in vibe-coded sites
@@ -667,6 +868,19 @@ var BrainerceClient = class {
667
868
  }
668
869
  return this.adminRequest("GET", "/api/v1/store");
669
870
  }
871
+ /**
872
+ * Resolve the script direction (`'ltr' | 'rtl'`) for a locale tag.
873
+ * Use this on `<html dir>` in your root layout — do not maintain a local
874
+ * RTL set in your storefront.
875
+ *
876
+ * @param locale - Optional BCP-47 tag. If omitted, falls back to the SDK
877
+ * client's configured locale (`new BrainerceClient({ locale: 'he' })`).
878
+ * @returns `'rtl'` for Arabic / Hebrew / Persian / Urdu / Yiddish family,
879
+ * `'ltr'` for everything else (including unknown locales).
880
+ */
881
+ getStoreDirection(locale) {
882
+ return getDirectionForLocale(locale ?? this.locale);
883
+ }
670
884
  // -------------------- Products --------------------
671
885
  /**
672
886
  * Get a list of products with pagination and filtering
@@ -780,10 +994,11 @@ var BrainerceClient = class {
780
994
  */
781
995
  async getProductBySlug(slug, options) {
782
996
  const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
997
+ const encodedSlug = encodeURIComponent(slug);
783
998
  if (this.isVibeCodedMode()) {
784
999
  return this.vibeCodedRequest(
785
1000
  "GET",
786
- `/products/slug/${slug}`,
1001
+ `/products/slug/${encodedSlug}`,
787
1002
  void 0,
788
1003
  void 0,
789
1004
  headerOverrides
@@ -792,13 +1007,13 @@ var BrainerceClient = class {
792
1007
  if (this.storeId && !this.apiKey) {
793
1008
  return this.storefrontRequest(
794
1009
  "GET",
795
- `/products/slug/${slug}`,
1010
+ `/products/slug/${encodedSlug}`,
796
1011
  void 0,
797
1012
  void 0,
798
1013
  headerOverrides
799
1014
  );
800
1015
  }
801
- return this.adminRequest("GET", `/api/v1/products/by-slug/${slug}`);
1016
+ return this.adminRequest("GET", `/api/v1/products/by-slug/${encodedSlug}`);
802
1017
  }
803
1018
  /**
804
1019
  * Get available categories for filtering products
@@ -7143,6 +7358,26 @@ function getDescriptionContent(product) {
7143
7358
  }
7144
7359
  return { text: product.description };
7145
7360
  }
7361
+ function stripHtml(html) {
7362
+ if (!html) return "";
7363
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&#x27;/gi, "'").replace(/&#x2F;/gi, "/").replace(/\s+/g, " ").trim();
7364
+ }
7365
+ function deriveSeoDescription(product, options) {
7366
+ if (!product) return "";
7367
+ const maxLength = options?.maxLength ?? 160;
7368
+ const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
7369
+ if (authored) return authored;
7370
+ const plain = stripHtml(product.description);
7371
+ if (plain) {
7372
+ if (plain.length <= maxLength) return plain;
7373
+ const sliceLength = maxLength - 1;
7374
+ const slice = plain.slice(0, sliceLength);
7375
+ const lastSpace = slice.lastIndexOf(" ");
7376
+ const trimmed = lastSpace > maxLength * 0.5 ? slice.slice(0, lastSpace) : slice;
7377
+ return trimmed.replace(/[\s.,;:!?-]+$/u, "") + "\u2026";
7378
+ }
7379
+ return product.name ?? "";
7380
+ }
7146
7381
  function getStockStatus(inventory, options) {
7147
7382
  if (!inventory) {
7148
7383
  return options?.outOfStockText ?? "Out of Stock";
@@ -7350,14 +7585,17 @@ function isCouponApplicableToProduct(coupon, productId) {
7350
7585
  0 && (module.exports = {
7351
7586
  BrainerceClient,
7352
7587
  BrainerceError,
7588
+ RTL_LOCALES,
7353
7589
  SDK_VERSION,
7354
7590
  createWebhookHandler,
7591
+ deriveSeoDescription,
7355
7592
  enableDevGuards,
7356
7593
  formatPrice,
7357
7594
  getCartItemImage,
7358
7595
  getCartItemName,
7359
7596
  getCartTotals,
7360
7597
  getDescriptionContent,
7598
+ getDirectionForLocale,
7361
7599
  getPriceDisplay,
7362
7600
  getProductCustomizationFields,
7363
7601
  getProductMetafield,
@@ -7375,5 +7613,6 @@ function isCouponApplicableToProduct(coupon, productId) {
7375
7613
  isWebhookEventType,
7376
7614
  parseWebhookEvent,
7377
7615
  safePaymentRedirect,
7616
+ stripHtml,
7378
7617
  verifyWebhook
7379
7618
  });
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,197 @@ 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 DEFAULT_KEY = "main";
246
+ const publicGetPath = (type, key) => `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
247
+ const adminBase = () => "/api/v1/content";
248
+ const publicGet = async (type, key, locale) => {
249
+ const query = locale ? { locale } : void 0;
250
+ const path = publicGetPath(type, key);
251
+ const onNotFound = (err) => {
252
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
253
+ throw err;
254
+ };
255
+ if (this.isVibeCodedMode()) {
256
+ return this.vibeCodedRequest("GET", path, void 0, query).catch(onNotFound);
257
+ }
258
+ if (this.storeId && !this.apiKey) {
259
+ return this.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
260
+ }
261
+ throw new BrainerceError(
262
+ "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).",
263
+ 400
264
+ );
265
+ };
266
+ const publicList = async (type, locale) => {
267
+ const query = { type };
268
+ if (locale) query.locale = locale;
269
+ if (this.isVibeCodedMode()) {
270
+ return this.vibeCodedRequest("GET", "/content", void 0, query);
271
+ }
272
+ if (this.storeId && !this.apiKey) {
273
+ return this.storefrontRequest("GET", "/content", void 0, query);
274
+ }
275
+ return this.adminRequest(
276
+ "GET",
277
+ `${adminBase()}?type=${encodeURIComponent(type)}`
278
+ );
279
+ };
280
+ const requireAdmin = (action) => {
281
+ if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
282
+ throw new BrainerceError(
283
+ `client.content.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
284
+ 403
285
+ );
286
+ }
287
+ };
288
+ const createByType = async (type, input) => {
289
+ requireAdmin("create");
290
+ return this.adminRequest("POST", adminBase(), { ...input, type });
291
+ };
292
+ const updateById = async (id, input) => {
293
+ requireAdmin("update");
294
+ return this.adminRequest(
295
+ "PATCH",
296
+ `${adminBase()}/${encodeURIComponent(id)}`,
297
+ input
298
+ );
299
+ };
300
+ const publishById = async (id) => {
301
+ requireAdmin("publish");
302
+ return this.adminRequest("POST", `${adminBase()}/${encodeURIComponent(id)}/publish`);
303
+ };
304
+ const unpublishById = async (id) => {
305
+ requireAdmin("unpublish");
306
+ return this.adminRequest(
307
+ "POST",
308
+ `${adminBase()}/${encodeURIComponent(id)}/unpublish`
309
+ );
310
+ };
311
+ const removeById = async (id) => {
312
+ requireAdmin("remove");
313
+ await this.adminRequest("DELETE", `${adminBase()}/${encodeURIComponent(id)}`);
314
+ };
315
+ function makeNamespace(type) {
316
+ return {
317
+ /**
318
+ * Fetch one PUBLISHED entry by key (defaults to `'main'`). Returns
319
+ * `null` on 404 — render a hard-coded fallback when the merchant
320
+ * hasn't seeded yet.
321
+ */
322
+ get: (key = DEFAULT_KEY, locale) => publicGet(type, key, locale),
323
+ /** List all PUBLISHED entries of this type. */
324
+ list: (locale) => publicList(type, locale),
325
+ /** Create a new entry in DRAFT (admin mode). */
326
+ create: (input) => createByType(type, input)
327
+ };
328
+ }
329
+ return {
330
+ faq: makeNamespace("FAQ"),
331
+ footer: makeNamespace("FOOTER"),
332
+ header: makeNamespace("HEADER"),
333
+ announcement: makeNamespace("ANNOUNCEMENT"),
334
+ richText: makeNamespace("RICH_TEXT"),
335
+ page: {
336
+ ...makeNamespace("PAGE"),
337
+ /**
338
+ * Fetch a PUBLISHED page by its `data.slug` (e.g. `'about'`).
339
+ * Returns `null` on 404. Use this from your `app/[slug]/page.tsx`
340
+ * catch-all route.
341
+ */
342
+ getBySlug: async (slug, locale) => {
343
+ const query = locale ? { locale } : void 0;
344
+ const path = `/content/pages/by-slug/${encodeURIComponent(slug)}`;
345
+ const onNotFound = (err) => {
346
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
347
+ throw err;
348
+ };
349
+ if (this.isVibeCodedMode()) {
350
+ return this.vibeCodedRequest("GET", path, void 0, query).catch(
351
+ onNotFound
352
+ );
353
+ }
354
+ if (this.storeId && !this.apiKey) {
355
+ return this.storefrontRequest("GET", path, void 0, query).catch(
356
+ onNotFound
357
+ );
358
+ }
359
+ throw new BrainerceError(
360
+ "content.page.getBySlug() is a public-read API; not available in admin mode.",
361
+ 400
362
+ );
363
+ }
364
+ },
365
+ // ---------- Admin operations (cross-type) ----------
366
+ /** Find a single row by its admin id (admin mode). */
367
+ findById: async (id) => {
368
+ requireAdmin("findById");
369
+ return this.adminRequest("GET", `${adminBase()}/${encodeURIComponent(id)}`);
370
+ },
371
+ /** List rows in admin mode with optional filters. */
372
+ listAdmin: async (filters) => {
373
+ requireAdmin("listAdmin");
374
+ const params = new URLSearchParams();
375
+ if (filters?.type) params.set("type", filters.type);
376
+ if (filters?.status) params.set("status", filters.status);
377
+ const qs = params.toString();
378
+ return this.adminRequest(
379
+ "GET",
380
+ qs ? `${adminBase()}?${qs}` : adminBase()
381
+ );
382
+ },
383
+ /** Replace `data` (and optional metadata) on an existing row. */
384
+ update: updateById,
385
+ /** Transition status DRAFT → PUBLISHED. */
386
+ publish: publishById,
387
+ /** Transition status PUBLISHED → DRAFT. */
388
+ unpublish: unpublishById,
389
+ /** Hard delete the row. Admin mode only. */
390
+ remove: removeById
391
+ };
392
+ })();
196
393
  // -------------------- Local Cart (Client-Side for Guests) --------------------
197
394
  // These methods store cart data in localStorage - NO API calls!
198
395
  // Use for guest users in vibe-coded sites
@@ -604,6 +801,19 @@ var BrainerceClient = class {
604
801
  }
605
802
  return this.adminRequest("GET", "/api/v1/store");
606
803
  }
804
+ /**
805
+ * Resolve the script direction (`'ltr' | 'rtl'`) for a locale tag.
806
+ * Use this on `<html dir>` in your root layout — do not maintain a local
807
+ * RTL set in your storefront.
808
+ *
809
+ * @param locale - Optional BCP-47 tag. If omitted, falls back to the SDK
810
+ * client's configured locale (`new BrainerceClient({ locale: 'he' })`).
811
+ * @returns `'rtl'` for Arabic / Hebrew / Persian / Urdu / Yiddish family,
812
+ * `'ltr'` for everything else (including unknown locales).
813
+ */
814
+ getStoreDirection(locale) {
815
+ return getDirectionForLocale(locale ?? this.locale);
816
+ }
607
817
  // -------------------- Products --------------------
608
818
  /**
609
819
  * Get a list of products with pagination and filtering
@@ -717,10 +927,11 @@ var BrainerceClient = class {
717
927
  */
718
928
  async getProductBySlug(slug, options) {
719
929
  const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
930
+ const encodedSlug = encodeURIComponent(slug);
720
931
  if (this.isVibeCodedMode()) {
721
932
  return this.vibeCodedRequest(
722
933
  "GET",
723
- `/products/slug/${slug}`,
934
+ `/products/slug/${encodedSlug}`,
724
935
  void 0,
725
936
  void 0,
726
937
  headerOverrides
@@ -729,13 +940,13 @@ var BrainerceClient = class {
729
940
  if (this.storeId && !this.apiKey) {
730
941
  return this.storefrontRequest(
731
942
  "GET",
732
- `/products/slug/${slug}`,
943
+ `/products/slug/${encodedSlug}`,
733
944
  void 0,
734
945
  void 0,
735
946
  headerOverrides
736
947
  );
737
948
  }
738
- return this.adminRequest("GET", `/api/v1/products/by-slug/${slug}`);
949
+ return this.adminRequest("GET", `/api/v1/products/by-slug/${encodedSlug}`);
739
950
  }
740
951
  /**
741
952
  * Get available categories for filtering products
@@ -7080,6 +7291,26 @@ function getDescriptionContent(product) {
7080
7291
  }
7081
7292
  return { text: product.description };
7082
7293
  }
7294
+ function stripHtml(html) {
7295
+ if (!html) return "";
7296
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&#x27;/gi, "'").replace(/&#x2F;/gi, "/").replace(/\s+/g, " ").trim();
7297
+ }
7298
+ function deriveSeoDescription(product, options) {
7299
+ if (!product) return "";
7300
+ const maxLength = options?.maxLength ?? 160;
7301
+ const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
7302
+ if (authored) return authored;
7303
+ const plain = stripHtml(product.description);
7304
+ if (plain) {
7305
+ if (plain.length <= maxLength) return plain;
7306
+ const sliceLength = maxLength - 1;
7307
+ const slice = plain.slice(0, sliceLength);
7308
+ const lastSpace = slice.lastIndexOf(" ");
7309
+ const trimmed = lastSpace > maxLength * 0.5 ? slice.slice(0, lastSpace) : slice;
7310
+ return trimmed.replace(/[\s.,;:!?-]+$/u, "") + "\u2026";
7311
+ }
7312
+ return product.name ?? "";
7313
+ }
7083
7314
  function getStockStatus(inventory, options) {
7084
7315
  if (!inventory) {
7085
7316
  return options?.outOfStockText ?? "Out of Stock";
@@ -7286,14 +7517,17 @@ function isCouponApplicableToProduct(coupon, productId) {
7286
7517
  export {
7287
7518
  BrainerceClient,
7288
7519
  BrainerceError,
7520
+ RTL_LOCALES,
7289
7521
  SDK_VERSION,
7290
7522
  createWebhookHandler,
7523
+ deriveSeoDescription,
7291
7524
  enableDevGuards,
7292
7525
  formatPrice,
7293
7526
  getCartItemImage,
7294
7527
  getCartItemName,
7295
7528
  getCartTotals,
7296
7529
  getDescriptionContent,
7530
+ getDirectionForLocale,
7297
7531
  formatPrice as getPriceDisplay,
7298
7532
  getProductCustomizationFields,
7299
7533
  getProductMetafield,
@@ -7311,5 +7545,6 @@ export {
7311
7545
  isWebhookEventType,
7312
7546
  parseWebhookEvent,
7313
7547
  safePaymentRedirect,
7548
+ stripHtml,
7314
7549
  verifyWebhook
7315
7550
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.25.1",
3
+ "version": "1.27.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",