brainerce 1.58.1 → 1.63.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 +534 -125
- package/dist/index.d.mts +623 -43
- package/dist/index.d.ts +623 -43
- package/dist/index.js +359 -41
- package/dist/index.mjs +358 -41
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// src/version.ts
|
|
118
|
-
var SDK_VERSION = "1.
|
|
118
|
+
var SDK_VERSION = "1.60.0";
|
|
119
119
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -272,6 +272,87 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
274
|
};
|
|
275
|
+
// -------------------- Marketing signup (newsletter) --------------------
|
|
276
|
+
/**
|
|
277
|
+
* Email marketing signup for a storefront — a newsletter popup, a footer
|
|
278
|
+
* capture bar, an exit-intent modal.
|
|
279
|
+
*
|
|
280
|
+
* **Confirmed opt-in, always.** `subscribe()` creates the contact and mails
|
|
281
|
+
* them a confirmation link. The address is NOT subscribed and CANNOT receive
|
|
282
|
+
* a campaign until the recipient clicks that link. This is not a setting:
|
|
283
|
+
* consent has to come from the mailbox, or anyone could subscribe anyone.
|
|
284
|
+
*
|
|
285
|
+
* So do not render "You're subscribed!" on success — render "Check your
|
|
286
|
+
* email to confirm." The one is a lie until the click lands.
|
|
287
|
+
*
|
|
288
|
+
* The response is identical for a brand-new address, one that is already
|
|
289
|
+
* subscribed, and one suppressed after a bounce, so the form can't be used to
|
|
290
|
+
* probe who shops here. Show the same message for every success.
|
|
291
|
+
*
|
|
292
|
+
* Storefront (public) and vibe-coded modes only. Rate-limited server-side to
|
|
293
|
+
* 3 requests / 60s per IP, plus one confirmation email per address per 24h.
|
|
294
|
+
*
|
|
295
|
+
* **Where the discount goes.** A "10% off your first order" popup needs a
|
|
296
|
+
* coupon from the dashboard — create one with the `customer_first_order`
|
|
297
|
+
* condition and show the code after a successful call. Subscribing does not
|
|
298
|
+
* mint a code on its own.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* // Newsletter popup — hidden honeypot input, Hebrew storefront
|
|
303
|
+
* await brainerce.marketing.subscribe({
|
|
304
|
+
* email: 'jane@example.com',
|
|
305
|
+
* locale: 'he',
|
|
306
|
+
* source: 'popup',
|
|
307
|
+
* honeypot: hiddenFieldValue,
|
|
308
|
+
* });
|
|
309
|
+
* // → show "בדקו את המייל שלכם כדי לאשר" — NOT "נרשמת בהצלחה"
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
this.marketing = {
|
|
313
|
+
subscribe: async (input) => {
|
|
314
|
+
if (this.isVibeCodedMode()) {
|
|
315
|
+
return this.vibeCodedRequest(
|
|
316
|
+
"POST",
|
|
317
|
+
"/marketing/subscribe",
|
|
318
|
+
input
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return this.storefrontRequest(
|
|
322
|
+
"POST",
|
|
323
|
+
"/marketing/subscribe",
|
|
324
|
+
input
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
// -------------------- Stock alerts --------------------
|
|
329
|
+
/**
|
|
330
|
+
* "Email me when this is back."
|
|
331
|
+
*
|
|
332
|
+
* ⛔ Not a newsletter signup, and must not be worded as one. It grants no
|
|
333
|
+
* marketing consent, creates no customer account, and the person is never
|
|
334
|
+
* mailed anything else as a result — exactly one message, about this item,
|
|
335
|
+
* with a link that stops it. Someone who unsubscribed from marketing can
|
|
336
|
+
* still use this, so do not gate it on consent.
|
|
337
|
+
*
|
|
338
|
+
* Show the affordance only on an item that is out of stock AND cannot be
|
|
339
|
+
* backordered. Every other case is silently ignored server-side — the
|
|
340
|
+
* response is uniform on purpose, so it cannot be used to read stock levels
|
|
341
|
+
* or test who is a customer — which means a button on an in-stock item looks
|
|
342
|
+
* like it worked and does nothing.
|
|
343
|
+
*
|
|
344
|
+
* Pass `variantId` on any product with variants. Without it the alert waits
|
|
345
|
+
* on the product as a whole, and a shopper who wanted the medium hears when
|
|
346
|
+
* the small comes back.
|
|
347
|
+
*/
|
|
348
|
+
this.stockAlerts = {
|
|
349
|
+
subscribe: async (input) => {
|
|
350
|
+
if (this.isVibeCodedMode()) {
|
|
351
|
+
return this.vibeCodedRequest("POST", "/stock-alerts", input);
|
|
352
|
+
}
|
|
353
|
+
return this.storefrontRequest("POST", "/stock-alerts", input);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
275
356
|
// -------------------- Content (typed merchant content) --------------------
|
|
276
357
|
/**
|
|
277
358
|
* Typed merchant content store: FAQ, Footer, Header, Announcement,
|
|
@@ -3046,6 +3127,11 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3046
3127
|
* Register a new customer with password (creates account)
|
|
3047
3128
|
* Works in vibe-coded, storefront, and admin mode
|
|
3048
3129
|
*
|
|
3130
|
+
* `birthMonth`/`birthDay` are optional and must be sent together. When
|
|
3131
|
+
* `getStoreInfo().requireBirthday` is true the merchant made the birthday
|
|
3132
|
+
* mandatory on that sales channel, and a call without both fields is
|
|
3133
|
+
* rejected with HTTP 400.
|
|
3134
|
+
*
|
|
3049
3135
|
* @example
|
|
3050
3136
|
* ```typescript
|
|
3051
3137
|
* const auth = await client.registerCustomer({
|
|
@@ -3053,6 +3139,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
3053
3139
|
* password: 'securepassword123',
|
|
3054
3140
|
* firstName: 'Jane',
|
|
3055
3141
|
* lastName: 'Doe',
|
|
3142
|
+
* birthMonth: 4, // optional, unless the channel requires a birthday
|
|
3143
|
+
* birthDay: 17,
|
|
3056
3144
|
* });
|
|
3057
3145
|
* ```
|
|
3058
3146
|
*/
|
|
@@ -4117,7 +4205,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4117
4205
|
* try {
|
|
4118
4206
|
* await client.createCheckout(cartId);
|
|
4119
4207
|
* } catch (err) {
|
|
4120
|
-
*
|
|
4208
|
+
* // BrainerceError.details is the whole response body — the code lives
|
|
4209
|
+
* // there, NOT on the error object itself.
|
|
4210
|
+
* if ((err as BrainerceError).details?.code === 'PRICE_DRIFT') {
|
|
4121
4211
|
* // ask user to confirm new prices, then:
|
|
4122
4212
|
* await client.refreshCartSnapshots(cartId);
|
|
4123
4213
|
* await client.createCheckout(cartId);
|
|
@@ -4329,16 +4419,27 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4329
4419
|
* List visible reviews for a product (storefront / sales-channel modes).
|
|
4330
4420
|
* Reviews that the merchant has hidden are excluded.
|
|
4331
4421
|
*
|
|
4422
|
+
* Each review carries `images` — the photos its author attached, already
|
|
4423
|
+
* filtered to the ones shoppers are allowed to see. Always an array.
|
|
4424
|
+
*
|
|
4425
|
+
* Ordering defaults to `photos_first`: reviews carrying photos lead, newest-first
|
|
4426
|
+
* within each group. Pass `sort: 'newest'` for plain chronological order. On a
|
|
4427
|
+
* store with no review photos the two are identical.
|
|
4428
|
+
*
|
|
4332
4429
|
* @example
|
|
4333
4430
|
* ```typescript
|
|
4334
4431
|
* const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
|
|
4335
|
-
* data.forEach(r =>
|
|
4432
|
+
* data.forEach(r => {
|
|
4433
|
+
* console.log(r.rating, r.body, r.verifiedPurchase);
|
|
4434
|
+
* r.images.forEach(img => console.log(img.thumbnailUrl ?? img.url));
|
|
4435
|
+
* });
|
|
4336
4436
|
* ```
|
|
4337
4437
|
*/
|
|
4338
4438
|
async listProductReviews(productId, params) {
|
|
4339
4439
|
const queryParams = {};
|
|
4340
4440
|
if (params?.page) queryParams.page = params.page;
|
|
4341
4441
|
if (params?.limit) queryParams.limit = params.limit;
|
|
4442
|
+
if (params?.sort) queryParams.sort = params.sort;
|
|
4342
4443
|
if (this.isVibeCodedMode()) {
|
|
4343
4444
|
return this.vibeCodedRequest(
|
|
4344
4445
|
"GET",
|
|
@@ -4496,6 +4597,37 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
4496
4597
|
storeId ? { storeId } : void 0
|
|
4497
4598
|
);
|
|
4498
4599
|
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Admin: hide ONE photo on a review, leaving the review and its other photos
|
|
4602
|
+
* visible. Requires an API key with `reviews:write`.
|
|
4603
|
+
*/
|
|
4604
|
+
async hideProductReviewImage(imageId, storeId) {
|
|
4605
|
+
if (!this.apiKey) {
|
|
4606
|
+
throw new BrainerceError("hideProductReviewImage() requires admin (API key) mode", 400);
|
|
4607
|
+
}
|
|
4608
|
+
return this.adminRequest(
|
|
4609
|
+
"PATCH",
|
|
4610
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/hide`,
|
|
4611
|
+
void 0,
|
|
4612
|
+
storeId ? { storeId } : void 0
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Admin: show one review photo. This is also the approve action — a photo that
|
|
4617
|
+
* has never been approved carries no `approvedAt`, and showing it stamps one, so
|
|
4618
|
+
* stores using review-photo approval need no separate verb.
|
|
4619
|
+
*/
|
|
4620
|
+
async showProductReviewImage(imageId, storeId) {
|
|
4621
|
+
if (!this.apiKey) {
|
|
4622
|
+
throw new BrainerceError("showProductReviewImage() requires admin (API key) mode", 400);
|
|
4623
|
+
}
|
|
4624
|
+
return this.adminRequest(
|
|
4625
|
+
"PATCH",
|
|
4626
|
+
`/api/v1/review-images/${encodePathSegment(imageId)}/show`,
|
|
4627
|
+
void 0,
|
|
4628
|
+
storeId ? { storeId } : void 0
|
|
4629
|
+
);
|
|
4630
|
+
}
|
|
4499
4631
|
/** Admin: unhide a previously hidden review. */
|
|
4500
4632
|
async showProductReview(reviewId, storeId) {
|
|
4501
4633
|
if (!this.apiKey) {
|
|
@@ -5955,6 +6087,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5955
6087
|
* Get applicable custom field definitions for a checkout.
|
|
5956
6088
|
* Returns fields filtered by visibility conditions (delivery type, products in cart).
|
|
5957
6089
|
* Use these to render dynamic input fields in the checkout flow.
|
|
6090
|
+
*
|
|
6091
|
+
* **Vibe-coded or storefront mode only.** There is no checkout custom-field
|
|
6092
|
+
* route on the API-key `/v1` surface; in admin mode this throws.
|
|
5958
6093
|
*/
|
|
5959
6094
|
async getCheckoutCustomFields(checkoutId) {
|
|
5960
6095
|
if (this.isVibeCodedMode()) {
|
|
@@ -5969,15 +6104,18 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5969
6104
|
`/checkout/${encodePathSegment(checkoutId)}/custom-fields`
|
|
5970
6105
|
);
|
|
5971
6106
|
}
|
|
5972
|
-
|
|
5973
|
-
"
|
|
5974
|
-
|
|
6107
|
+
throw new BrainerceError(
|
|
6108
|
+
"getCheckoutCustomFields is only available in vibe-coded or storefront mode",
|
|
6109
|
+
400
|
|
5975
6110
|
);
|
|
5976
6111
|
}
|
|
5977
6112
|
/**
|
|
5978
6113
|
* Set checkout custom field values and recalculate surcharges.
|
|
5979
6114
|
* The checkout total is automatically updated to include surcharges.
|
|
5980
6115
|
*
|
|
6116
|
+
* **Vibe-coded or storefront mode only.** There is no checkout custom-field
|
|
6117
|
+
* route on the API-key `/v1` surface; in admin mode this throws.
|
|
6118
|
+
*
|
|
5981
6119
|
* @example
|
|
5982
6120
|
* ```typescript
|
|
5983
6121
|
* const checkout = await client.setCheckoutCustomFields(checkoutId, {
|
|
@@ -6005,10 +6143,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
6005
6143
|
data
|
|
6006
6144
|
);
|
|
6007
6145
|
}
|
|
6008
|
-
|
|
6009
|
-
"
|
|
6010
|
-
|
|
6011
|
-
data
|
|
6146
|
+
throw new BrainerceError(
|
|
6147
|
+
"setCheckoutCustomFields is only available in vibe-coded or storefront mode",
|
|
6148
|
+
400
|
|
6012
6149
|
);
|
|
6013
6150
|
}
|
|
6014
6151
|
/**
|
|
@@ -7146,11 +7283,20 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
7146
7283
|
);
|
|
7147
7284
|
}
|
|
7148
7285
|
/**
|
|
7149
|
-
* Update the current customer's profile (requires customerToken)
|
|
7150
|
-
* Only available in storefront mode
|
|
7286
|
+
* Update the current customer's profile (requires customerToken).
|
|
7287
|
+
* Only available in storefront and vibe-coded mode.
|
|
7151
7288
|
*
|
|
7152
|
-
* `birthMonth`/`birthDay` (1-12 / 1-31
|
|
7153
|
-
*
|
|
7289
|
+
* `birthMonth`/`birthDay` (1-12 / 1-31) power the loyalty birthday gift.
|
|
7290
|
+
* Month and day only, never a year, for privacy. Send both or neither, and
|
|
7291
|
+
* the day has to exist in the month: anything else is rejected with HTTP 400.
|
|
7292
|
+
* Send `null` for both to REMOVE a stored birthday. Leaving the keys out is a
|
|
7293
|
+
* different request and keeps the stored value, so a profile form whose
|
|
7294
|
+
* fields the shopper cleared has to send nulls, not omit them.
|
|
7295
|
+
*
|
|
7296
|
+
* The saved values now come back on the returned `CustomerProfile`, on
|
|
7297
|
+
* `getMyProfile()` and on every customer read type, so a profile form can
|
|
7298
|
+
* re-render the birthday it just saved. Before this, no API returned the two
|
|
7299
|
+
* fields at all and the form always came back blank.
|
|
7154
7300
|
*/
|
|
7155
7301
|
async updateMyProfile(data) {
|
|
7156
7302
|
if (!this.customerToken && !this.proxyMode) {
|
|
@@ -8274,8 +8420,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
8274
8420
|
// /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
|
|
8275
8421
|
//
|
|
8276
8422
|
// Server-side validation failures arrive as a structured 400 envelope:
|
|
8277
|
-
// { code: 'MODIFIER_VALIDATION_FAILED', errors:
|
|
8278
|
-
//
|
|
8423
|
+
// { code: 'MODIFIER_VALIDATION_FAILED', message, details: { errors: [...] } }
|
|
8424
|
+
// BrainerceError.details holds the WHOLE body, so the issue list is
|
|
8425
|
+
// `err.details.details.errors` — see ModifierValidationFailedError.
|
|
8279
8426
|
/**
|
|
8280
8427
|
* List modifier groups in a store, paginated.
|
|
8281
8428
|
* Requires Admin mode (apiKey).
|
|
@@ -9326,28 +9473,99 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9326
9473
|
}
|
|
9327
9474
|
throw new Error("uploadCustomizationFile requires storefront or vibe-coded mode");
|
|
9328
9475
|
}
|
|
9476
|
+
/**
|
|
9477
|
+
* Upload one photo to attach to a product review.
|
|
9478
|
+
*
|
|
9479
|
+
* Available in storefront and vibe-coded modes, and — unlike
|
|
9480
|
+
* `uploadCustomizationFile` — it REQUIRES a logged-in customer who has actually
|
|
9481
|
+
* bought the product. Call `setCustomerToken(...)` first. That is the same bar as
|
|
9482
|
+
* writing the review itself, checked here so an ineligible shopper is told before
|
|
9483
|
+
* they wait for the upload rather than after.
|
|
9484
|
+
*
|
|
9485
|
+
* Returns a storage `key`. Collect the keys and pass them as `imageKeys` when you
|
|
9486
|
+
* submit or update the review — the `url` is for a local preview only, and sending
|
|
9487
|
+
* it back instead of the key will be rejected.
|
|
9488
|
+
*
|
|
9489
|
+
* Server rules:
|
|
9490
|
+
* - `image/jpeg|png|webp|gif` only, cross-checked against the file's real bytes.
|
|
9491
|
+
* - Max 5 MB and 40 megapixels per file.
|
|
9492
|
+
* - Throttled to 10 uploads / minute → HTTP 429.
|
|
9493
|
+
* - 403 when the store has review photos turned off, or the customer has not
|
|
9494
|
+
* bought the product; 400 once the review is already at its photo cap.
|
|
9495
|
+
* - EXIF is stripped (so GPS coordinates never reach the storefront) while the
|
|
9496
|
+
* orientation tag is applied first, so phone photos stay upright.
|
|
9497
|
+
* - A photo uploaded but never attached to a submitted review is reclaimed after
|
|
9498
|
+
* 7 days.
|
|
9499
|
+
*
|
|
9500
|
+
* Read `photos` from `getMyProductReview()` for the store's live limits rather
|
|
9501
|
+
* than hard-coding them.
|
|
9502
|
+
*
|
|
9503
|
+
* @example
|
|
9504
|
+
* ```ts
|
|
9505
|
+
* const { photos } = await client.getMyProductReview(productId);
|
|
9506
|
+
* if (photos.enabled) {
|
|
9507
|
+
* const uploads = await Promise.all(
|
|
9508
|
+
* [...fileInput.files].slice(0, photos.maxPerReview)
|
|
9509
|
+
* .map(f => client.uploadReviewPhoto(productId, f))
|
|
9510
|
+
* );
|
|
9511
|
+
* await client.submitProductReview(productId, {
|
|
9512
|
+
* rating: 5,
|
|
9513
|
+
* body: 'Arrived beautifully wrapped.',
|
|
9514
|
+
* imageKeys: uploads.map(u => u.key),
|
|
9515
|
+
* });
|
|
9516
|
+
* }
|
|
9517
|
+
* ```
|
|
9518
|
+
*/
|
|
9519
|
+
async uploadReviewPhoto(productId, file) {
|
|
9520
|
+
const formData = new FormData();
|
|
9521
|
+
formData.append("file", file);
|
|
9522
|
+
if (this.isVibeCodedMode()) {
|
|
9523
|
+
return this.vibeCodedRequest(
|
|
9524
|
+
"POST",
|
|
9525
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9526
|
+
formData
|
|
9527
|
+
);
|
|
9528
|
+
}
|
|
9529
|
+
if (this.storeId && !this.apiKey) {
|
|
9530
|
+
return this.storefrontRequest(
|
|
9531
|
+
"POST",
|
|
9532
|
+
`/products/${encodePathSegment(productId)}/review-photo`,
|
|
9533
|
+
formData
|
|
9534
|
+
);
|
|
9535
|
+
}
|
|
9536
|
+
throw new Error("uploadReviewPhoto requires storefront or vibe-coded mode");
|
|
9537
|
+
}
|
|
9329
9538
|
// -------------------- Team Management (Admin) - DEPRECATED --------------------
|
|
9330
|
-
//
|
|
9539
|
+
// Account-level team methods. These are the ONLY team endpoints reachable with
|
|
9540
|
+
// a `brainerce_*` API key. The store-level equivalents (`getStoreTeam`,
|
|
9541
|
+
// `inviteStoreMember`, `updateStoreMember`, ...) sit behind
|
|
9542
|
+
// `DashboardOnlyGuard`, which rejects api_key principals by design — so they
|
|
9543
|
+
// return 403 from the SDK, not 404, and "fixing" their path would not help.
|
|
9544
|
+
// Do not migrate SDK code onto them.
|
|
9331
9545
|
/**
|
|
9332
|
-
* @deprecated
|
|
9546
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `getStoreTeam`
|
|
9547
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9333
9548
|
*/
|
|
9334
9549
|
async getTeamMembers() {
|
|
9335
9550
|
return this.adminRequest("GET", "/api/v1/team/members");
|
|
9336
9551
|
}
|
|
9337
9552
|
/**
|
|
9338
|
-
* @deprecated
|
|
9553
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `getStoreTeam`
|
|
9554
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9339
9555
|
*/
|
|
9340
9556
|
async getTeamInvitations() {
|
|
9341
9557
|
return this.adminRequest("GET", "/api/v1/team/invitations");
|
|
9342
9558
|
}
|
|
9343
9559
|
/**
|
|
9344
|
-
* @deprecated
|
|
9560
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `inviteStoreMember`
|
|
9561
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9345
9562
|
*/
|
|
9346
9563
|
async inviteTeamMember(data) {
|
|
9347
9564
|
return this.adminRequest("POST", "/api/v1/team/invitations", data);
|
|
9348
9565
|
}
|
|
9349
9566
|
/**
|
|
9350
|
-
* @deprecated
|
|
9567
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `resendStoreInvitation`
|
|
9568
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9351
9569
|
*/
|
|
9352
9570
|
async resendTeamInvitation(invitationId) {
|
|
9353
9571
|
return this.adminRequest(
|
|
@@ -9356,7 +9574,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9356
9574
|
);
|
|
9357
9575
|
}
|
|
9358
9576
|
/**
|
|
9359
|
-
* @deprecated
|
|
9577
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `revokeStoreInvitation`
|
|
9578
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9360
9579
|
*/
|
|
9361
9580
|
async revokeTeamInvitation(invitationId) {
|
|
9362
9581
|
await this.adminRequest(
|
|
@@ -9365,7 +9584,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9365
9584
|
);
|
|
9366
9585
|
}
|
|
9367
9586
|
/**
|
|
9368
|
-
* @deprecated
|
|
9587
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `updateStoreMember`
|
|
9588
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9369
9589
|
*/
|
|
9370
9590
|
async updateTeamMemberRole(memberId, data) {
|
|
9371
9591
|
return this.adminRequest(
|
|
@@ -9375,7 +9595,8 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
9375
9595
|
);
|
|
9376
9596
|
}
|
|
9377
9597
|
/**
|
|
9378
|
-
* @deprecated
|
|
9598
|
+
* @deprecated Retiring, but there is no API-key replacement yet: `removeStoreMember`
|
|
9599
|
+
* is dashboard-only (403 for api_key). Keep using this until one ships.
|
|
9379
9600
|
*/
|
|
9380
9601
|
async removeTeamMember(memberId) {
|
|
9381
9602
|
await this.adminRequest("DELETE", `/api/v1/team/members/${encodePathSegment(memberId)}`);
|
|
@@ -10001,7 +10222,7 @@ var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\
|
|
|
10001
10222
|
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
10002
10223
|
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
10003
10224
|
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
10004
|
-
function validateDateAvailabilityConfig(config, fieldType) {
|
|
10225
|
+
function validateDateAvailabilityConfig(config, fieldType, surface = "checkout") {
|
|
10005
10226
|
const errors = [];
|
|
10006
10227
|
if (!config) return errors;
|
|
10007
10228
|
if (config.minDate !== void 0 && !DATE_RE.test(config.minDate)) {
|
|
@@ -10013,6 +10234,29 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10013
10234
|
if (config.minDate && config.maxDate && DATE_RE.test(config.minDate) && DATE_RE.test(config.maxDate) && config.minDate > config.maxDate) {
|
|
10014
10235
|
errors.push("minDate must be on or before maxDate");
|
|
10015
10236
|
}
|
|
10237
|
+
if (config.leadTimeMinutes !== void 0) {
|
|
10238
|
+
if (!Number.isInteger(config.leadTimeMinutes) || config.leadTimeMinutes < 0) {
|
|
10239
|
+
errors.push("leadTimeMinutes must be a non-negative integer");
|
|
10240
|
+
}
|
|
10241
|
+
}
|
|
10242
|
+
if (config.maxDaysAhead !== void 0) {
|
|
10243
|
+
if (!Number.isInteger(config.maxDaysAhead) || config.maxDaysAhead < 1) {
|
|
10244
|
+
errors.push("maxDaysAhead must be a positive integer");
|
|
10245
|
+
}
|
|
10246
|
+
}
|
|
10247
|
+
if (config.cutoffTime !== void 0 && !TIME_RE.test(config.cutoffTime)) {
|
|
10248
|
+
errors.push("cutoffTime must be in HH:mm format");
|
|
10249
|
+
}
|
|
10250
|
+
if (surface !== "checkout") {
|
|
10251
|
+
const relativeKeys = ["leadTimeMinutes", "cutoffTime", "maxDaysAhead"].filter(
|
|
10252
|
+
(k) => config[k] !== void 0
|
|
10253
|
+
);
|
|
10254
|
+
if (relativeKeys.length > 0) {
|
|
10255
|
+
errors.push(
|
|
10256
|
+
`${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`
|
|
10257
|
+
);
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10016
10260
|
if (config.blockedWeekdays) {
|
|
10017
10261
|
for (const d of config.blockedWeekdays) {
|
|
10018
10262
|
if (!Number.isInteger(d) || d < 0 || d > 6) {
|
|
@@ -10030,22 +10274,37 @@ function validateDateAvailabilityConfig(config, fieldType) {
|
|
|
10030
10274
|
}
|
|
10031
10275
|
}
|
|
10032
10276
|
if (config.businessHours) {
|
|
10033
|
-
const
|
|
10277
|
+
const windowsByWeekday = /* @__PURE__ */ new Map();
|
|
10034
10278
|
for (const w of config.businessHours) {
|
|
10035
10279
|
if (!Number.isInteger(w.weekday) || w.weekday < 0 || w.weekday > 6) {
|
|
10036
10280
|
errors.push(`businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`);
|
|
10037
10281
|
continue;
|
|
10038
10282
|
}
|
|
10039
|
-
if (seenWeekdays.has(w.weekday)) {
|
|
10040
|
-
errors.push(`businessHours has more than one window for weekday ${w.weekday}`);
|
|
10041
|
-
}
|
|
10042
|
-
seenWeekdays.add(w.weekday);
|
|
10043
10283
|
const openValid = TIME_RE.test(w.open);
|
|
10044
10284
|
const closeValid = TIME_RE.test(w.close);
|
|
10045
10285
|
if (!openValid) errors.push(`businessHours.open must be HH:mm, got "${w.open}"`);
|
|
10046
10286
|
if (!closeValid) errors.push(`businessHours.close must be HH:mm, got "${w.close}"`);
|
|
10047
|
-
if (openValid
|
|
10287
|
+
if (!openValid || !closeValid) continue;
|
|
10288
|
+
if (w.open >= w.close) {
|
|
10048
10289
|
errors.push(`businessHours for weekday ${w.weekday}: open must be before close`);
|
|
10290
|
+
continue;
|
|
10291
|
+
}
|
|
10292
|
+
const forDay = windowsByWeekday.get(w.weekday);
|
|
10293
|
+
if (forDay) forDay.push(w);
|
|
10294
|
+
else windowsByWeekday.set(w.weekday, [w]);
|
|
10295
|
+
}
|
|
10296
|
+
const weekdaysWithWindows = Array.from(windowsByWeekday.keys()).sort((a, b) => a - b);
|
|
10297
|
+
for (const weekday of weekdaysWithWindows) {
|
|
10298
|
+
const sorted = [...windowsByWeekday.get(weekday) ?? []].sort(
|
|
10299
|
+
(a, b) => a.open < b.open ? -1 : a.open > b.open ? 1 : 0
|
|
10300
|
+
);
|
|
10301
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
10302
|
+
if (sorted[i].open < sorted[i - 1].close) {
|
|
10303
|
+
errors.push(
|
|
10304
|
+
`businessHours for weekday ${weekday}: ${sorted[i - 1].open}-${sorted[i - 1].close} overlaps ${sorted[i].open}-${sorted[i].close}`
|
|
10305
|
+
);
|
|
10306
|
+
break;
|
|
10307
|
+
}
|
|
10049
10308
|
}
|
|
10050
10309
|
}
|
|
10051
10310
|
}
|
|
@@ -10101,6 +10360,32 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
10101
10360
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
10102
10361
|
};
|
|
10103
10362
|
}
|
|
10363
|
+
function resolveRelativeBounds(config, clock) {
|
|
10364
|
+
if (!config || !clock) return {};
|
|
10365
|
+
const leadMinutes = typeof config.leadTimeMinutes === "number" && Number.isFinite(config.leadTimeMinutes) ? Math.max(0, Math.floor(config.leadTimeMinutes)) : 0;
|
|
10366
|
+
const cutoff = typeof config.cutoffTime === "string" && TIME_RE.test(config.cutoffTime) ? config.cutoffTime : null;
|
|
10367
|
+
const daysAhead = typeof config.maxDaysAhead === "number" && Number.isFinite(config.maxDaysAhead) && config.maxDaysAhead >= 1 ? Math.floor(config.maxDaysAhead) : null;
|
|
10368
|
+
if (leadMinutes === 0 && !cutoff && daysAhead === null) return {};
|
|
10369
|
+
const now = clock.now ?? /* @__PURE__ */ new Date();
|
|
10370
|
+
const nowLocal = resolveStoreLocalParts(now, clock.timezone);
|
|
10371
|
+
const bounds = {};
|
|
10372
|
+
if (leadMinutes > 0 || cutoff) {
|
|
10373
|
+
const earliestInstant = new Date(now.getTime() + leadMinutes * 6e4);
|
|
10374
|
+
let earliestDate = resolveStoreLocalParts(earliestInstant, clock.timezone).dateYYYYMMDD;
|
|
10375
|
+
if (cutoff && nowLocal.hhmm >= cutoff) earliestDate = addCalendarDays(earliestDate, 1);
|
|
10376
|
+
bounds.earliestDate = earliestDate;
|
|
10377
|
+
if (leadMinutes > 0) bounds.earliestInstant = earliestInstant;
|
|
10378
|
+
}
|
|
10379
|
+
if (daysAhead !== null) {
|
|
10380
|
+
bounds.latestDate = addCalendarDays(nowLocal.dateYYYYMMDD, daysAhead);
|
|
10381
|
+
}
|
|
10382
|
+
return bounds;
|
|
10383
|
+
}
|
|
10384
|
+
function addCalendarDays(dateYYYYMMDD, days) {
|
|
10385
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
10386
|
+
const shifted = new Date(Date.UTC(year, month - 1, day + days));
|
|
10387
|
+
return `${shifted.getUTCFullYear()}-${pad2(shifted.getUTCMonth() + 1)}-${pad2(shifted.getUTCDate())}`;
|
|
10388
|
+
}
|
|
10104
10389
|
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
10105
10390
|
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
10106
10391
|
const expected = fieldType === "DATE" ? "expected a calendar date in YYYY-MM-DD format" : "expected an ISO-8601 date/time such as 2026-08-13T13:00:00+03:00";
|
|
@@ -10196,19 +10481,30 @@ function timezoneOffsetMs(utcMillis, timezone) {
|
|
|
10196
10481
|
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
10197
10482
|
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
10198
10483
|
}
|
|
10199
|
-
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
10484
|
+
function isCalendarDateAllowed(dateYYYYMMDD, config, clock) {
|
|
10200
10485
|
if (!config) return true;
|
|
10201
10486
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
10202
10487
|
if (config.maxDate && dateYYYYMMDD > config.maxDate) return false;
|
|
10488
|
+
if (relativeBoundFailure(dateYYYYMMDD, config, clock)) return false;
|
|
10203
10489
|
if (config.blockedDates?.includes(dateYYYYMMDD)) return false;
|
|
10204
10490
|
if (config.blockedWeekdays?.length) {
|
|
10205
10491
|
if (config.blockedWeekdays.includes(weekdayOfDateString(dateYYYYMMDD))) return false;
|
|
10206
10492
|
}
|
|
10207
10493
|
return true;
|
|
10208
10494
|
}
|
|
10209
|
-
function
|
|
10495
|
+
function relativeBoundFailure(dateYYYYMMDD, config, clock) {
|
|
10496
|
+
const bounds = resolveRelativeBounds(config, clock);
|
|
10497
|
+
if (bounds.earliestDate && dateYYYYMMDD < bounds.earliestDate) {
|
|
10498
|
+
return `the earliest available date is ${bounds.earliestDate}`;
|
|
10499
|
+
}
|
|
10500
|
+
if (bounds.latestDate && dateYYYYMMDD > bounds.latestDate) {
|
|
10501
|
+
return `the latest available date is ${bounds.latestDate}`;
|
|
10502
|
+
}
|
|
10503
|
+
return null;
|
|
10504
|
+
}
|
|
10505
|
+
function computeAvailableSlots(config, dateYYYYMMDD, clock) {
|
|
10210
10506
|
if (!config) return [];
|
|
10211
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10507
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10212
10508
|
if (!config.businessHours?.length || !config.slotDurationMinutes) return [];
|
|
10213
10509
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10214
10510
|
const windows = config.businessHours.filter((w) => w.weekday === weekday);
|
|
@@ -10221,27 +10517,47 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
10221
10517
|
slots.push(minutesToHHMM(t));
|
|
10222
10518
|
}
|
|
10223
10519
|
}
|
|
10224
|
-
|
|
10520
|
+
const ordered = Array.from(new Set(slots)).sort();
|
|
10521
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10522
|
+
if (!earliestInstant || !clock) return ordered;
|
|
10523
|
+
return ordered.filter((hhmm) => {
|
|
10524
|
+
const [hour, minute] = hhmm.split(":").map(Number);
|
|
10525
|
+
const start = instantFromStoreLocal(dateYYYYMMDD, hour, minute, 0, 0, clock.timezone);
|
|
10526
|
+
return start.getTime() >= earliestInstant.getTime();
|
|
10527
|
+
});
|
|
10225
10528
|
}
|
|
10226
|
-
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
10529
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD, clock) {
|
|
10227
10530
|
if (!config?.businessHours?.length) return [];
|
|
10228
|
-
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
10531
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) return [];
|
|
10229
10532
|
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
10230
10533
|
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
10231
10534
|
}
|
|
10232
|
-
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
10535
|
+
function isDateValueAllowed(instant, config, fieldType, timezone, now) {
|
|
10233
10536
|
if (!config) return { allowed: true };
|
|
10537
|
+
const clock = { timezone, now };
|
|
10234
10538
|
if (fieldType === "DATE") {
|
|
10235
10539
|
const dateYYYYMMDD = `${instant.getUTCFullYear()}-${pad2(instant.getUTCMonth() + 1)}-${pad2(instant.getUTCDate())}`;
|
|
10236
|
-
|
|
10540
|
+
const relative2 = relativeBoundFailure(dateYYYYMMDD, config, clock);
|
|
10541
|
+
if (relative2) return { allowed: false, reason: relative2 };
|
|
10542
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config, clock)) {
|
|
10237
10543
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10238
10544
|
}
|
|
10239
10545
|
return { allowed: true };
|
|
10240
10546
|
}
|
|
10241
10547
|
const local = resolveStoreLocalParts(instant, timezone);
|
|
10242
|
-
|
|
10548
|
+
const relative = relativeBoundFailure(local.dateYYYYMMDD, config, clock);
|
|
10549
|
+
if (relative) return { allowed: false, reason: relative };
|
|
10550
|
+
if (!isCalendarDateAllowed(local.dateYYYYMMDD, config, clock)) {
|
|
10243
10551
|
return { allowed: false, reason: "date is outside the allowed range" };
|
|
10244
10552
|
}
|
|
10553
|
+
const { earliestInstant } = resolveRelativeBounds(config, clock);
|
|
10554
|
+
if (earliestInstant && instant.getTime() < earliestInstant.getTime()) {
|
|
10555
|
+
const earliestLocal = resolveStoreLocalParts(earliestInstant, timezone);
|
|
10556
|
+
return {
|
|
10557
|
+
allowed: false,
|
|
10558
|
+
reason: `the earliest available time is ${earliestLocal.dateYYYYMMDD} ${earliestLocal.hhmm}`
|
|
10559
|
+
};
|
|
10560
|
+
}
|
|
10245
10561
|
if (!config.businessHours?.length) {
|
|
10246
10562
|
return { allowed: true };
|
|
10247
10563
|
}
|
|
@@ -10250,7 +10566,7 @@ function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
|
10250
10566
|
return { allowed: false, reason: "no business hours are configured for this day" };
|
|
10251
10567
|
}
|
|
10252
10568
|
if (config.slotDurationMinutes) {
|
|
10253
|
-
const slots = computeAvailableSlots(config, local.dateYYYYMMDD);
|
|
10569
|
+
const slots = computeAvailableSlots(config, local.dateYYYYMMDD, clock);
|
|
10254
10570
|
if (!slots.includes(local.hhmm)) {
|
|
10255
10571
|
return { allowed: false, reason: "time does not match an available slot" };
|
|
10256
10572
|
}
|
|
@@ -10880,6 +11196,7 @@ export {
|
|
|
10880
11196
|
jsonLdScriptProps,
|
|
10881
11197
|
parseDateFieldValue,
|
|
10882
11198
|
parseWebhookEvent,
|
|
11199
|
+
resolveRelativeBounds,
|
|
10883
11200
|
resolveStoreLocalParts,
|
|
10884
11201
|
safePaymentRedirect,
|
|
10885
11202
|
stripHtml2 as stripHtml,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.63.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",
|