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/README.md +39 -3
- package/dist/index.d.mts +387 -7
- package/dist/index.d.ts +387 -7
- package/dist/index.js +242 -3
- package/dist/index.mjs +238 -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,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/${
|
|
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/${
|
|
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/${
|
|
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(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/'/gi, "'").replace(///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/${
|
|
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/${
|
|
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/${
|
|
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(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/'/gi, "'").replace(///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.
|
|
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",
|