brainerce 1.25.1 → 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/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/${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/${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/${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(/&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();
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/${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/${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/${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(/&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();
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.25.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",