brainerce 2.4.0 → 2.7.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 +8361 -8090
- package/dist/index.d.mts +746 -94
- package/dist/index.d.ts +746 -94
- package/dist/index.js +563 -116
- package/dist/index.mjs +563 -116
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -204,7 +204,7 @@ function isDevGuardsEnabled() {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// src/version.ts
|
|
207
|
-
var SDK_VERSION = "2.
|
|
207
|
+
var SDK_VERSION = "2.7.0";
|
|
208
208
|
|
|
209
209
|
// src/client.ts
|
|
210
210
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -381,10 +381,14 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
381
381
|
* Storefront (public) and vibe-coded modes only. Rate-limited server-side to
|
|
382
382
|
* 3 requests / 60s per IP, plus one confirmation email per address per 24h.
|
|
383
383
|
*
|
|
384
|
-
* **Where the discount goes.**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
384
|
+
* **Where the discount goes.** Configure the newsletter welcome offer and the
|
|
385
|
+
* platform issues the coupon itself: read it with `marketing.getBenefit()`,
|
|
386
|
+
* show those terms beside the field, and stop there.
|
|
387
|
+
*
|
|
388
|
+
* ⛔ DO NOT SHOW A CODE AFTER THIS CALL RESOLVES. No coupon exists yet. It is
|
|
389
|
+
* minted when the recipient clicks the confirmation link, and it is mailed to
|
|
390
|
+
* them at that moment — a code rendered here is a code that was never issued.
|
|
391
|
+
* Say "check your email", the same as for the subscription itself.
|
|
388
392
|
*
|
|
389
393
|
* @example
|
|
390
394
|
* ```typescript
|
|
@@ -412,6 +416,163 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
412
416
|
"/marketing/subscribe",
|
|
413
417
|
input
|
|
414
418
|
);
|
|
419
|
+
},
|
|
420
|
+
/**
|
|
421
|
+
* The welcome offer to render beside the signup field, or `null` when this
|
|
422
|
+
* store offers none.
|
|
423
|
+
*
|
|
424
|
+
* Show the discount, how long the coupon lasts, any minimum order, whether
|
|
425
|
+
* it is first-order only, and the merchant's own headline and terms. Then
|
|
426
|
+
* post to `marketing.subscribe()` and tell the shopper to check their
|
|
427
|
+
* inbox.
|
|
428
|
+
*
|
|
429
|
+
* ⛔ THE COUPON DOES NOT EXIST YET at any point in that sequence. It is
|
|
430
|
+
* created when the recipient clicks the confirmation link in their email,
|
|
431
|
+
* and it is mailed to them there. Rendering a code on this screen renders a
|
|
432
|
+
* code nobody was issued.
|
|
433
|
+
*
|
|
434
|
+
* ⛔ Takes no email address and returns nothing about any individual, on
|
|
435
|
+
* purpose. There is no "has this person already claimed" call, because an
|
|
436
|
+
* unauthenticated one would be an oracle for who shops here. If you need to
|
|
437
|
+
* discourage a repeat signup, say the offer is one per address; do not try
|
|
438
|
+
* to detect it.
|
|
439
|
+
*
|
|
440
|
+
* `null` is the common case on a store that never set this up, so handle it
|
|
441
|
+
* rather than assuming the object. Cache it per page load: it belongs to
|
|
442
|
+
* the store, not to the visitor.
|
|
443
|
+
*
|
|
444
|
+
* Storefront (public) and vibe-coded modes.
|
|
445
|
+
*
|
|
446
|
+
* @param locale - Storefront locale, e.g. `"he"`. Picks the language of the
|
|
447
|
+
* headline and terms; falls back to the store language when omitted.
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* ```typescript
|
|
451
|
+
* const offer = await brainerce.marketing.getBenefit('he');
|
|
452
|
+
* if (offer) {
|
|
453
|
+
* // "10% הנחה על ההזמנה הראשונה"
|
|
454
|
+
* render(offer.headline ?? defaultHeadline(offer), offer.terms);
|
|
455
|
+
* }
|
|
456
|
+
* await brainerce.marketing.subscribe({ email, locale: 'he', honeypot });
|
|
457
|
+
* // → "בדקו את המייל שלכם" — never a coupon code
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
getBenefit: async (locale) => {
|
|
461
|
+
const query = locale ? { locale } : void 0;
|
|
462
|
+
if (this.isVibeCodedMode()) {
|
|
463
|
+
return this.vibeCodedRequest(
|
|
464
|
+
"GET",
|
|
465
|
+
"/newsletter-benefit",
|
|
466
|
+
void 0,
|
|
467
|
+
query
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return this.storefrontRequest(
|
|
471
|
+
"GET",
|
|
472
|
+
"/newsletter-benefit",
|
|
473
|
+
void 0,
|
|
474
|
+
query
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
// -------------------- Newsletter signup benefit (Admin) --------------------
|
|
479
|
+
/**
|
|
480
|
+
* Manage the newsletter welcome offer: the terms merchants configure, and the
|
|
481
|
+
* benefits that offer has produced.
|
|
482
|
+
*
|
|
483
|
+
* Admin mode (`apiKey`) only, on the `coupons:read` / `coupons:write` scopes.
|
|
484
|
+
* The benefit IS a coupon feature — it mints a Coupon row and the coupon
|
|
485
|
+
* machinery enforces it — so it carries no scope of its own.
|
|
486
|
+
*
|
|
487
|
+
* ⛔ THERE IS NO "ISSUE A BENEFIT TO THIS ADDRESS" CALL, and there will not
|
|
488
|
+
* be one. A benefit exists because someone submitted the signup form AND
|
|
489
|
+
* clicked the confirmation link; handing one out directly would skip the
|
|
490
|
+
* consent the double opt-in exists to collect and break the one-per-address
|
|
491
|
+
* guarantee that the grant's unique constraint provides. `resend` re-sends a
|
|
492
|
+
* code that already exists; it never creates one.
|
|
493
|
+
*/
|
|
494
|
+
this.newsletterBenefit = {
|
|
495
|
+
/**
|
|
496
|
+
* The store's configuration, or `null` when none was ever saved.
|
|
497
|
+
*
|
|
498
|
+
* `null` and `{ enabled: false }` are different: never configured, versus
|
|
499
|
+
* configured and switched off. Both mean "offer nothing" to a storefront.
|
|
500
|
+
*/
|
|
501
|
+
getSettings: async () => {
|
|
502
|
+
return this.adminRequest(
|
|
503
|
+
"GET",
|
|
504
|
+
"/api/v1/newsletter-benefit/settings"
|
|
505
|
+
);
|
|
506
|
+
},
|
|
507
|
+
/**
|
|
508
|
+
* Create or replace the offer.
|
|
509
|
+
*
|
|
510
|
+
* ⛔ A FULL REPLACEMENT, not a patch. Every field is written, so a field you
|
|
511
|
+
* omit is cleared rather than kept.
|
|
512
|
+
*
|
|
513
|
+
* Saving never rewrites a promise already made: signups still waiting for a
|
|
514
|
+
* confirmation click keep the terms they were shown, and coupons already
|
|
515
|
+
* issued are untouched. Switching `enabled` off stops new offers and leaves
|
|
516
|
+
* every issued coupon working until it expires.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```typescript
|
|
520
|
+
* await brainerce.newsletterBenefit.updateSettings({
|
|
521
|
+
* enabled: true,
|
|
522
|
+
* discountType: 'PERCENTAGE',
|
|
523
|
+
* discountValue: 10,
|
|
524
|
+
* minimumOrderAmount: 200,
|
|
525
|
+
* combinesWithOther: false,
|
|
526
|
+
* validityDays: 7,
|
|
527
|
+
* eligibilityTtlHours: 168,
|
|
528
|
+
* firstOrderOnly: true,
|
|
529
|
+
* content: { he: { headline: '10% הנחה על ההזמנה הראשונה' } },
|
|
530
|
+
* });
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
updateSettings: async (input) => {
|
|
534
|
+
return this.adminRequest(
|
|
535
|
+
"PUT",
|
|
536
|
+
"/api/v1/newsletter-benefit/settings",
|
|
537
|
+
input
|
|
538
|
+
);
|
|
539
|
+
},
|
|
540
|
+
/**
|
|
541
|
+
* Issued benefits, newest first, as `{ data, meta }`.
|
|
542
|
+
*
|
|
543
|
+
* ⛔ NO EMAIL FILTER — the API refuses the parameter. Filter the page you
|
|
544
|
+
* get back rather than asking the server about one address.
|
|
545
|
+
*/
|
|
546
|
+
listGrants: async (params = {}) => {
|
|
547
|
+
return this.adminRequest(
|
|
548
|
+
"GET",
|
|
549
|
+
"/api/v1/newsletter-benefit/grants",
|
|
550
|
+
void 0,
|
|
551
|
+
{
|
|
552
|
+
page: params.page,
|
|
553
|
+
limit: params.limit,
|
|
554
|
+
status: params.status,
|
|
555
|
+
from: params.from,
|
|
556
|
+
to: params.to
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* Re-send one benefit that went astray.
|
|
562
|
+
*
|
|
563
|
+
* ⛔ SENDS THE SAME CODE. It never mints a second coupon, so a support
|
|
564
|
+
* ticket cannot become two discounts. For a benefit whose issuance failed
|
|
565
|
+
* before any coupon existed, this retries the issuance and mails the result.
|
|
566
|
+
*
|
|
567
|
+
* Rejects a signup that has not been confirmed and one that lapsed before a
|
|
568
|
+
* coupon was minted: there is nothing to re-send in either case, and
|
|
569
|
+
* nothing that may be created.
|
|
570
|
+
*/
|
|
571
|
+
resend: async (grantId) => {
|
|
572
|
+
return this.adminRequest(
|
|
573
|
+
"POST",
|
|
574
|
+
`/api/v1/newsletter-benefit/grants/${encodePathSegment(grantId)}/resend`
|
|
575
|
+
);
|
|
415
576
|
}
|
|
416
577
|
};
|
|
417
578
|
// -------------------- Stock alerts --------------------
|
|
@@ -448,10 +609,20 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
448
609
|
* Rich Text, and Page.
|
|
449
610
|
*
|
|
450
611
|
* Works in all three SDK modes (vibe-coded, storefront, admin):
|
|
451
|
-
* - **Public reads** (`get`, `list`, `getBySlug`):
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
612
|
+
* - **Public reads** (`get`, `list`, `getBySlug`): storefront and
|
|
613
|
+
* vibe-coded mode. There is no admin equivalent of a by-key/by-slug
|
|
614
|
+
* read — in admin mode they throw and point you at `listAdmin()` /
|
|
615
|
+
* `findById()`.
|
|
616
|
+
* - **Admin reads** (`listAdmin`, `findById`) and **writes** (`create`,
|
|
617
|
+
* `update`, `publish`, `unpublish`, `remove`): admin mode only — they
|
|
618
|
+
* call `/api/content/...` with the API key. Calling from storefront /
|
|
619
|
+
* vibe-coded mode throws.
|
|
620
|
+
*
|
|
621
|
+
* **⚠️ Every admin method takes an explicit `storeId`.** Admin mode has no
|
|
622
|
+
* ambient store (`storeId` is only set in storefront mode), and the routes
|
|
623
|
+
* are store-scoped: omitting it is rejected fail-closed by the store scope
|
|
624
|
+
* guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your API key
|
|
625
|
+
* is bound to — naming any other store is rejected as cross-tenant.
|
|
455
626
|
*
|
|
456
627
|
* **Default key:** every type has `'main'` as its universal default key.
|
|
457
628
|
* Pass no argument to fetch the main entry; pass a custom key (e.g.
|
|
@@ -476,12 +647,15 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
476
647
|
* });
|
|
477
648
|
* }
|
|
478
649
|
*
|
|
479
|
-
* // Admin — create a shipping FAQ in DRAFT
|
|
480
|
-
* await client.content.faq.create(
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* }
|
|
650
|
+
* // Admin — create a shipping FAQ in DRAFT (storeId is required)
|
|
651
|
+
* await client.content.faq.create(
|
|
652
|
+
* {
|
|
653
|
+
* key: 'shipping',
|
|
654
|
+
* name: 'Shipping FAQ',
|
|
655
|
+
* data: { items: [{ question: '…', answer: '…' }] },
|
|
656
|
+
* },
|
|
657
|
+
* 'store_123'
|
|
658
|
+
* );
|
|
485
659
|
* ```
|
|
486
660
|
*/
|
|
487
661
|
this.content = (() => {
|
|
@@ -502,7 +676,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
502
676
|
return this.storefrontRequest("GET", path, void 0, query).catch(onNotFound);
|
|
503
677
|
}
|
|
504
678
|
throw new BrainerceError(
|
|
505
|
-
"content.<type>.get(key) is a public-read API. In admin mode, call client.content.
|
|
679
|
+
"content.<type>.get(key) is a public-read API. In admin mode, call client.content.listAdmin({ storeId, type: '" + type + "' }) and filter by key, or client.content.findById(id, storeId) if you already hold the id.",
|
|
506
680
|
400
|
|
507
681
|
);
|
|
508
682
|
};
|
|
@@ -515,9 +689,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
515
689
|
if (this.storeId && !this.apiKey) {
|
|
516
690
|
return this.storefrontRequest("GET", "/content", void 0, query);
|
|
517
691
|
}
|
|
518
|
-
|
|
519
|
-
"
|
|
520
|
-
|
|
692
|
+
throw new BrainerceError(
|
|
693
|
+
"content.<type>.list() is a public-read API. In admin mode, call client.content.listAdmin({ storeId, type: '" + type + "' }).",
|
|
694
|
+
400
|
|
521
695
|
);
|
|
522
696
|
};
|
|
523
697
|
const requireAdmin = (action) => {
|
|
@@ -528,32 +702,59 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
528
702
|
);
|
|
529
703
|
}
|
|
530
704
|
};
|
|
531
|
-
const
|
|
705
|
+
const requireStoreId = (action, storeId) => {
|
|
706
|
+
if (!storeId) {
|
|
707
|
+
throw new BrainerceError(
|
|
708
|
+
`client.content.${action}() requires a storeId. Admin mode has no ambient store \u2014 pass the id of the store your API key is bound to.`,
|
|
709
|
+
400
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
return { storeId };
|
|
713
|
+
};
|
|
714
|
+
const createByType = async (type, input, storeId) => {
|
|
532
715
|
requireAdmin("create");
|
|
533
|
-
return this.adminRequest(
|
|
716
|
+
return this.adminRequest(
|
|
717
|
+
"POST",
|
|
718
|
+
adminBase(),
|
|
719
|
+
{ ...input, type },
|
|
720
|
+
requireStoreId("create", storeId)
|
|
721
|
+
);
|
|
534
722
|
};
|
|
535
|
-
const updateById = async (id, input) => {
|
|
723
|
+
const updateById = async (id, input, storeId) => {
|
|
536
724
|
requireAdmin("update");
|
|
537
725
|
return this.adminRequest(
|
|
538
726
|
"PATCH",
|
|
539
727
|
`${adminBase()}/${encodeURIComponent(id)}`,
|
|
540
|
-
input
|
|
728
|
+
input,
|
|
729
|
+
requireStoreId("update", storeId)
|
|
541
730
|
);
|
|
542
731
|
};
|
|
543
|
-
const publishById = async (id) => {
|
|
732
|
+
const publishById = async (id, storeId) => {
|
|
544
733
|
requireAdmin("publish");
|
|
545
|
-
return this.adminRequest(
|
|
734
|
+
return this.adminRequest(
|
|
735
|
+
"POST",
|
|
736
|
+
`${adminBase()}/${encodeURIComponent(id)}/publish`,
|
|
737
|
+
void 0,
|
|
738
|
+
requireStoreId("publish", storeId)
|
|
739
|
+
);
|
|
546
740
|
};
|
|
547
|
-
const unpublishById = async (id) => {
|
|
741
|
+
const unpublishById = async (id, storeId) => {
|
|
548
742
|
requireAdmin("unpublish");
|
|
549
743
|
return this.adminRequest(
|
|
550
744
|
"POST",
|
|
551
|
-
`${adminBase()}/${encodeURIComponent(id)}/unpublish
|
|
745
|
+
`${adminBase()}/${encodeURIComponent(id)}/unpublish`,
|
|
746
|
+
void 0,
|
|
747
|
+
requireStoreId("unpublish", storeId)
|
|
552
748
|
);
|
|
553
749
|
};
|
|
554
|
-
const removeById = async (id) => {
|
|
750
|
+
const removeById = async (id, storeId) => {
|
|
555
751
|
requireAdmin("remove");
|
|
556
|
-
await this.adminRequest(
|
|
752
|
+
await this.adminRequest(
|
|
753
|
+
"DELETE",
|
|
754
|
+
`${adminBase()}/${encodeURIComponent(id)}`,
|
|
755
|
+
void 0,
|
|
756
|
+
requireStoreId("remove", storeId)
|
|
757
|
+
);
|
|
557
758
|
};
|
|
558
759
|
function makeNamespace(type) {
|
|
559
760
|
return {
|
|
@@ -563,10 +764,17 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
563
764
|
* hasn't seeded yet.
|
|
564
765
|
*/
|
|
565
766
|
get: (key = DEFAULT_KEY, locale) => publicGet(type, key, locale),
|
|
566
|
-
/**
|
|
767
|
+
/**
|
|
768
|
+
* List all PUBLISHED entries of this type (storefront / vibe-coded
|
|
769
|
+
* mode). In admin mode this throws — use
|
|
770
|
+
* `client.content.listAdmin({ storeId, type })`.
|
|
771
|
+
*/
|
|
567
772
|
list: (locale) => publicList(type, locale),
|
|
568
|
-
/**
|
|
569
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Create a new entry in DRAFT (admin mode).
|
|
775
|
+
* `storeId` is required — see the namespace docs above.
|
|
776
|
+
*/
|
|
777
|
+
create: (input, storeId) => createByType(type, input, storeId)
|
|
570
778
|
};
|
|
571
779
|
}
|
|
572
780
|
return {
|
|
@@ -606,30 +814,78 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
606
814
|
}
|
|
607
815
|
},
|
|
608
816
|
// ---------- Admin operations (cross-type) ----------
|
|
609
|
-
/**
|
|
610
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Find a single row by its admin id (admin mode).
|
|
819
|
+
*
|
|
820
|
+
* @example
|
|
821
|
+
* ```typescript
|
|
822
|
+
* const row = await client.content.findById('cnt_123', 'store_123');
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
findById: async (id, storeId) => {
|
|
611
826
|
requireAdmin("findById");
|
|
612
|
-
return this.adminRequest("GET", `${adminBase()}/${encodeURIComponent(id)}`);
|
|
613
|
-
},
|
|
614
|
-
/** List rows in admin mode with optional filters. */
|
|
615
|
-
listAdmin: async (filters) => {
|
|
616
|
-
requireAdmin("listAdmin");
|
|
617
|
-
const params = new URLSearchParams();
|
|
618
|
-
if (filters?.type) params.set("type", filters.type);
|
|
619
|
-
if (filters?.status) params.set("status", filters.status);
|
|
620
|
-
const qs = params.toString();
|
|
621
827
|
return this.adminRequest(
|
|
622
828
|
"GET",
|
|
623
|
-
|
|
829
|
+
`${adminBase()}/${encodeURIComponent(id)}`,
|
|
830
|
+
void 0,
|
|
831
|
+
requireStoreId("findById", storeId)
|
|
624
832
|
);
|
|
625
833
|
},
|
|
626
|
-
/**
|
|
834
|
+
/**
|
|
835
|
+
* List rows in admin mode. `storeId` is required; `type` and `status`
|
|
836
|
+
* are optional filters.
|
|
837
|
+
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* const faqs = await client.content.listAdmin({
|
|
841
|
+
* storeId: 'store_123',
|
|
842
|
+
* type: 'FAQ',
|
|
843
|
+
* status: 'DRAFT',
|
|
844
|
+
* });
|
|
845
|
+
* ```
|
|
846
|
+
*/
|
|
847
|
+
listAdmin: async (filters) => {
|
|
848
|
+
requireAdmin("listAdmin");
|
|
849
|
+
const query = requireStoreId("listAdmin", filters?.storeId);
|
|
850
|
+
if (filters.type) query.type = filters.type;
|
|
851
|
+
if (filters.status) query.status = filters.status;
|
|
852
|
+
return this.adminRequest("GET", adminBase(), void 0, query);
|
|
853
|
+
},
|
|
854
|
+
/**
|
|
855
|
+
* Replace `data` (and optional metadata) on an existing row.
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ```typescript
|
|
859
|
+
* await client.content.update('cnt_123', { name: 'Shipping FAQ' }, 'store_123');
|
|
860
|
+
* ```
|
|
861
|
+
*/
|
|
627
862
|
update: updateById,
|
|
628
|
-
/**
|
|
863
|
+
/**
|
|
864
|
+
* Transition status DRAFT → PUBLISHED.
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* ```typescript
|
|
868
|
+
* await client.content.publish('cnt_123', 'store_123');
|
|
869
|
+
* ```
|
|
870
|
+
*/
|
|
629
871
|
publish: publishById,
|
|
630
|
-
/**
|
|
872
|
+
/**
|
|
873
|
+
* Transition status PUBLISHED → DRAFT.
|
|
874
|
+
*
|
|
875
|
+
* @example
|
|
876
|
+
* ```typescript
|
|
877
|
+
* await client.content.unpublish('cnt_123', 'store_123');
|
|
878
|
+
* ```
|
|
879
|
+
*/
|
|
631
880
|
unpublish: unpublishById,
|
|
632
|
-
/**
|
|
881
|
+
/**
|
|
882
|
+
* Hard delete the row. Admin mode only.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```typescript
|
|
886
|
+
* await client.content.remove('cnt_123', 'store_123');
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
633
889
|
remove: removeById
|
|
634
890
|
};
|
|
635
891
|
})();
|
|
@@ -637,15 +893,26 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
637
893
|
/**
|
|
638
894
|
* Read and manage blog posts.
|
|
639
895
|
*
|
|
896
|
+
* **⚠️ Every admin call takes an explicit `storeId`.** Admin mode has no
|
|
897
|
+
* ambient store (`storeId` is only set in storefront mode) and the admin
|
|
898
|
+
* routes are store-scoped: omitting it is rejected fail-closed by the store
|
|
899
|
+
* scope guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your
|
|
900
|
+
* API key is bound to — naming any other store is rejected as cross-tenant.
|
|
901
|
+
*
|
|
902
|
+
* Admin lookups are **by id**, not by slug (`getPost(slug)` is a public read
|
|
903
|
+
* and throws in admin mode — use `findById(id, storeId)`).
|
|
904
|
+
*
|
|
640
905
|
* ```typescript
|
|
641
906
|
* // Storefront / vibe-coded: list published posts
|
|
642
907
|
* const { data: posts } = await brainerce.blog.getPosts({ category: 'news' });
|
|
643
908
|
*
|
|
644
|
-
* // Fetch one by slug
|
|
909
|
+
* // Fetch one by slug (storefront / vibe-coded)
|
|
645
910
|
* const post = await brainerce.blog.getPost('my-first-post');
|
|
646
911
|
*
|
|
647
|
-
* // Admin: create a draft
|
|
648
|
-
* const
|
|
912
|
+
* // Admin: list, read one, and create a draft
|
|
913
|
+
* const all = await brainerce.blog.getPosts({}, 'store_123');
|
|
914
|
+
* const one = await brainerce.blog.findById('post_123', 'store_123');
|
|
915
|
+
* const draft = await brainerce.blog.create({ title: 'Hello World' }, 'store_123');
|
|
649
916
|
* ```
|
|
650
917
|
*/
|
|
651
918
|
this.blog = /* @__PURE__ */ (() => {
|
|
@@ -666,12 +933,30 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
666
933
|
const buildQuery = (params) => Object.fromEntries(
|
|
667
934
|
Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
|
|
668
935
|
);
|
|
936
|
+
const requireStoreId = (action, storeId) => {
|
|
937
|
+
if (!storeId) {
|
|
938
|
+
throw new BrainerceError(
|
|
939
|
+
`client.blog.${action}() requires a storeId in admin mode. Admin mode has no ambient store \u2014 pass the id of the store your API key is bound to.`,
|
|
940
|
+
400
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return storeId;
|
|
944
|
+
};
|
|
669
945
|
return {
|
|
670
946
|
/**
|
|
671
|
-
* List
|
|
672
|
-
*
|
|
947
|
+
* List posts. Filters: `category`, `tag`, `page`, `limit`.
|
|
948
|
+
*
|
|
949
|
+
* Storefront / vibe-coded mode lists PUBLISHED posts and ignores
|
|
950
|
+
* `storeId` (the store is already in the base URL). Admin mode lists
|
|
951
|
+
* drafts too and REQUIRES `storeId`.
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const { data } = await client.blog.getPosts({ category: 'news' }); // storefront
|
|
956
|
+
* const { data } = await client.blog.getPosts({}, 'store_123'); // admin
|
|
957
|
+
* ```
|
|
673
958
|
*/
|
|
674
|
-
getPosts: (params = {}) => {
|
|
959
|
+
getPosts: (params = {}, storeId) => {
|
|
675
960
|
const query = buildQuery(params);
|
|
676
961
|
if (this.isVibeCodedMode()) {
|
|
677
962
|
return this.vibeCodedRequest(
|
|
@@ -693,12 +978,23 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
693
978
|
"GET",
|
|
694
979
|
adminBase,
|
|
695
980
|
void 0,
|
|
696
|
-
|
|
981
|
+
{
|
|
982
|
+
...query,
|
|
983
|
+
storeId: requireStoreId("getPosts", storeId)
|
|
984
|
+
}
|
|
697
985
|
);
|
|
698
986
|
},
|
|
699
987
|
/**
|
|
700
|
-
* Fetch one
|
|
701
|
-
*
|
|
988
|
+
* Fetch one PUBLISHED post by its slug. Returns `null` on 404.
|
|
989
|
+
*
|
|
990
|
+
* Storefront / vibe-coded mode only — the admin API has no by-slug
|
|
991
|
+
* lookup (`GET /api/blog/posts/:id` is by id), so this throws in admin
|
|
992
|
+
* mode rather than issuing a request that can only 404.
|
|
993
|
+
*
|
|
994
|
+
* @example
|
|
995
|
+
* ```typescript
|
|
996
|
+
* const post = await client.blog.getPost('my-first-post');
|
|
997
|
+
* ```
|
|
702
998
|
*/
|
|
703
999
|
getPost: (slug) => {
|
|
704
1000
|
const path = `${publicBase}/${encodeURIComponent(slug)}`;
|
|
@@ -708,42 +1004,111 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
708
1004
|
if (this.storeId && !this.apiKey) {
|
|
709
1005
|
return this.storefrontRequest("GET", path).catch(onNotFound);
|
|
710
1006
|
}
|
|
711
|
-
|
|
1007
|
+
throw new BrainerceError(
|
|
1008
|
+
"client.blog.getPost(slug) is a public-read API. In admin mode, look a post up by id with client.blog.findById(id, storeId), or find it with client.blog.getPosts({}, storeId).",
|
|
1009
|
+
400
|
|
1010
|
+
);
|
|
1011
|
+
},
|
|
1012
|
+
/**
|
|
1013
|
+
* Fetch one post by its admin id — drafts included. Admin mode only.
|
|
1014
|
+
* Returns `null` on 404.
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* ```typescript
|
|
1018
|
+
* const post = await client.blog.findById('post_123', 'store_123');
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
findById: (id, storeId) => {
|
|
1022
|
+
requireAdmin("findById");
|
|
1023
|
+
return this.adminRequest(
|
|
1024
|
+
"GET",
|
|
1025
|
+
`${adminBase}/${encodeURIComponent(id)}`,
|
|
1026
|
+
void 0,
|
|
1027
|
+
{ storeId: requireStoreId("findById", storeId) }
|
|
1028
|
+
).catch(onNotFound);
|
|
712
1029
|
},
|
|
713
|
-
/**
|
|
714
|
-
|
|
1030
|
+
/**
|
|
1031
|
+
* Create a blog post in DRAFT status. Admin mode only.
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```typescript
|
|
1035
|
+
* const draft = await client.blog.create({ title: 'Hello World' }, 'store_123');
|
|
1036
|
+
* ```
|
|
1037
|
+
*/
|
|
1038
|
+
create: (input, storeId) => {
|
|
715
1039
|
requireAdmin("create");
|
|
716
|
-
return this.adminRequest("POST", adminBase, input
|
|
1040
|
+
return this.adminRequest("POST", adminBase, input, {
|
|
1041
|
+
storeId: requireStoreId("create", storeId)
|
|
1042
|
+
});
|
|
717
1043
|
},
|
|
718
|
-
/**
|
|
719
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* Update a blog post by ID. Admin mode only.
|
|
1046
|
+
*
|
|
1047
|
+
* @example
|
|
1048
|
+
* ```typescript
|
|
1049
|
+
* await client.blog.update('post_123', { title: 'Renamed' }, 'store_123');
|
|
1050
|
+
* ```
|
|
1051
|
+
*/
|
|
1052
|
+
update: (id, input, storeId) => {
|
|
720
1053
|
requireAdmin("update");
|
|
721
1054
|
return this.adminRequest(
|
|
722
1055
|
"PATCH",
|
|
723
1056
|
`${adminBase}/${encodeURIComponent(id)}`,
|
|
724
|
-
input
|
|
1057
|
+
input,
|
|
1058
|
+
{ storeId: requireStoreId("update", storeId) }
|
|
725
1059
|
);
|
|
726
1060
|
},
|
|
727
|
-
/**
|
|
728
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* Transition status → PUBLISHED (sets publishedAt = now if unset).
|
|
1063
|
+
* Admin mode only.
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
1066
|
+
* ```typescript
|
|
1067
|
+
* await client.blog.publish('post_123', 'store_123');
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
publish: (id, storeId) => {
|
|
729
1071
|
requireAdmin("publish");
|
|
730
1072
|
return this.adminRequest(
|
|
731
1073
|
"POST",
|
|
732
|
-
`${adminBase}/${encodeURIComponent(id)}/publish
|
|
1074
|
+
`${adminBase}/${encodeURIComponent(id)}/publish`,
|
|
1075
|
+
void 0,
|
|
1076
|
+
{ storeId: requireStoreId("publish", storeId) }
|
|
733
1077
|
);
|
|
734
1078
|
},
|
|
735
|
-
/**
|
|
736
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* Transition status PUBLISHED → DRAFT. Admin mode only.
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* ```typescript
|
|
1084
|
+
* await client.blog.unpublish('post_123', 'store_123');
|
|
1085
|
+
* ```
|
|
1086
|
+
*/
|
|
1087
|
+
unpublish: (id, storeId) => {
|
|
737
1088
|
requireAdmin("unpublish");
|
|
738
1089
|
return this.adminRequest(
|
|
739
1090
|
"POST",
|
|
740
|
-
`${adminBase}/${encodeURIComponent(id)}/unpublish
|
|
1091
|
+
`${adminBase}/${encodeURIComponent(id)}/unpublish`,
|
|
1092
|
+
void 0,
|
|
1093
|
+
{ storeId: requireStoreId("unpublish", storeId) }
|
|
741
1094
|
);
|
|
742
1095
|
},
|
|
743
|
-
/**
|
|
744
|
-
|
|
1096
|
+
/**
|
|
1097
|
+
* Hard-delete a blog post. Admin mode only.
|
|
1098
|
+
*
|
|
1099
|
+
* @example
|
|
1100
|
+
* ```typescript
|
|
1101
|
+
* await client.blog.remove('post_123', 'store_123');
|
|
1102
|
+
* ```
|
|
1103
|
+
*/
|
|
1104
|
+
remove: async (id, storeId) => {
|
|
745
1105
|
requireAdmin("remove");
|
|
746
|
-
await this.adminRequest(
|
|
1106
|
+
await this.adminRequest(
|
|
1107
|
+
"DELETE",
|
|
1108
|
+
`${adminBase}/${encodeURIComponent(id)}`,
|
|
1109
|
+
void 0,
|
|
1110
|
+
{ storeId: requireStoreId("remove", storeId) }
|
|
1111
|
+
);
|
|
747
1112
|
}
|
|
748
1113
|
};
|
|
749
1114
|
})();
|
|
@@ -2270,6 +2635,64 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2270
2635
|
* console.log('Product type:', product.type); // 'VARIABLE'
|
|
2271
2636
|
* ```
|
|
2272
2637
|
*/
|
|
2638
|
+
/**
|
|
2639
|
+
* Read a kit's contents, with its resolved price and how many can be sold.
|
|
2640
|
+
*
|
|
2641
|
+
* A KIT is one purchasable product assembled from other catalog products. It
|
|
2642
|
+
* holds no inventory of its own: `available` is whichever component runs out
|
|
2643
|
+
* first. Outside `FIXED` pricing the product row's `basePrice` is a
|
|
2644
|
+
* placeholder, so use the `price` returned here.
|
|
2645
|
+
*
|
|
2646
|
+
* Safe to call for any product — a non-KIT returns an empty, unsellable
|
|
2647
|
+
* shape rather than throwing, so callers need not branch on product type.
|
|
2648
|
+
*
|
|
2649
|
+
* @example
|
|
2650
|
+
* ```typescript
|
|
2651
|
+
* const kit = await client.getKitComponents('prod_123');
|
|
2652
|
+
* console.log(kit.price, kit.available, kit.components.length);
|
|
2653
|
+
* ```
|
|
2654
|
+
*/
|
|
2655
|
+
async getKitComponents(productId) {
|
|
2656
|
+
return this.request(
|
|
2657
|
+
"GET",
|
|
2658
|
+
`/api/v1/products/${encodePathSegment(productId)}/kit-components`
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Replace a kit's contents, and optionally how it is priced.
|
|
2663
|
+
*
|
|
2664
|
+
* A full replace, not a patch: send the list you want the kit to end up with.
|
|
2665
|
+
* Omit `pricingMode` to leave the kit's current mode untouched.
|
|
2666
|
+
*
|
|
2667
|
+
* Rejected: a product that is not a KIT, a component from another store, a
|
|
2668
|
+
* component that is itself a KIT, a VARIABLE component with no variant
|
|
2669
|
+
* pinned, a variant that does not belong to its product, the same slot
|
|
2670
|
+
* listed twice, and a component whose product or pinned variant is not
|
|
2671
|
+
* published.
|
|
2672
|
+
*
|
|
2673
|
+
* That last one is checked over the WHOLE list you send, not just the rows
|
|
2674
|
+
* you changed. Once a product already inside a kit is unpublished, no edit
|
|
2675
|
+
* to that kit saves until you publish it again or drop it from the list.
|
|
2676
|
+
*
|
|
2677
|
+
* @example
|
|
2678
|
+
* ```typescript
|
|
2679
|
+
* await client.setKitComponents('prod_123', {
|
|
2680
|
+
* components: [
|
|
2681
|
+
* { componentProductId: 'prod_bottle', quantity: 1 },
|
|
2682
|
+
* { componentProductId: 'prod_glass', quantity: 2 },
|
|
2683
|
+
* ],
|
|
2684
|
+
* pricingMode: 'SUM_MINUS_PERCENT',
|
|
2685
|
+
* discountValue: 10,
|
|
2686
|
+
* });
|
|
2687
|
+
* ```
|
|
2688
|
+
*/
|
|
2689
|
+
async setKitComponents(productId, body) {
|
|
2690
|
+
return this.request(
|
|
2691
|
+
"PUT",
|
|
2692
|
+
`/api/v1/products/${encodePathSegment(productId)}/kit-components`,
|
|
2693
|
+
body
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2273
2696
|
async convertToVariable(productId) {
|
|
2274
2697
|
return this.request(
|
|
2275
2698
|
"PATCH",
|
|
@@ -2854,6 +3277,13 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
2854
3277
|
* exist and 404'd silently. The live route is product-scoped:
|
|
2855
3278
|
* `GET /api/v1/products/:id/inventory`. A product with no inventory row
|
|
2856
3279
|
* reads back as all zeroes rather than 404ing.
|
|
3280
|
+
*
|
|
3281
|
+
* The response carries the whole {@link ProductInventoryResponse} — the
|
|
3282
|
+
* `InventoryItem` id, `trackingMode`, `backorderMode`, `backorderLimit`,
|
|
3283
|
+
* `lowStockThreshold`, `lastInventorySyncAt` and `updatedAt` alongside the
|
|
3284
|
+
* three counters. It always did; only the three counters were declared.
|
|
3285
|
+
* On the all-zeroes no-row branch everything but the counters is absent,
|
|
3286
|
+
* so test `id` rather than expecting a 404.
|
|
2857
3287
|
*/
|
|
2858
3288
|
async getInventory(productId) {
|
|
2859
3289
|
return this.adminRequest(
|
|
@@ -10045,71 +10475,88 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
10045
10475
|
}
|
|
10046
10476
|
throw new Error("uploadReviewPhoto requires storefront or vibe-coded mode");
|
|
10047
10477
|
}
|
|
10048
|
-
// -------------------- Team Management (Admin) -
|
|
10049
|
-
//
|
|
10050
|
-
// a `brainerce_*` API key.
|
|
10051
|
-
//
|
|
10052
|
-
//
|
|
10053
|
-
//
|
|
10054
|
-
//
|
|
10478
|
+
// -------------------- Team Management (Admin) - NOT REACHABLE BY API KEY --------------------
|
|
10479
|
+
// ⛔ These seven used to be documented as "the ONLY team endpoints reachable
|
|
10480
|
+
// with a `brainerce_*` API key". That was wrong, and every one of them 403s.
|
|
10481
|
+
//
|
|
10482
|
+
// The routes exist and the guards admit an api_key, but admission is not the
|
|
10483
|
+
// same as success. `external-api.controller.ts` hands `TeamService` a synthetic
|
|
10484
|
+
// principal — `SYSTEM_USER_ID = 'api-key-user'` (`:171`, used at `:4572`,
|
|
10485
|
+
// `:4586`, `:4607`, ...) — and every TeamService method opens with
|
|
10486
|
+
// `verifyAccountAccess` / `verifyAccountOwner`, which is an
|
|
10487
|
+
// `accountUser.findFirst({ where: { userId, ... } })`. No `AccountUser` row has
|
|
10488
|
+
// `userId = 'api-key-user'` anywhere; the sentinel is row-less by construction.
|
|
10489
|
+
// So the lookup misses and every call throws `Access denied to this account`.
|
|
10490
|
+
//
|
|
10491
|
+
// The store-level equivalents (`getStoreTeam`, `inviteStoreMember`, ...) sit
|
|
10492
|
+
// behind `DashboardOnlyGuard`, which rejects api_key principals by design. So
|
|
10493
|
+
// BOTH families are dashboard-only and there is no API-key path to team
|
|
10494
|
+
// management at all.
|
|
10495
|
+
//
|
|
10496
|
+
// ⛔ Do NOT "fix" this by letting api_key through. Team membership is
|
|
10497
|
+
// ACCOUNT-scoped and `inviteMember` grants `Role.ACCOUNT_OWNER`, i.e. ownership
|
|
10498
|
+
// of every store on the account. An API key is bound to exactly ONE store, so
|
|
10499
|
+
// admitting it here converts a single-store credential into account ownership.
|
|
10500
|
+
// That is a privilege-escalation primitive, not a missing feature.
|
|
10501
|
+
//
|
|
10502
|
+
// These throw rather than return a 403 from the wire, so the failure names its
|
|
10503
|
+
// own cause instead of arriving as a bare status code. Same treatment as
|
|
10504
|
+
// `createDonation` above. Agents: use the dashboard, or the `team:*` tools on
|
|
10505
|
+
// the admin MCP server, which are OAuth-authenticated and store-scoped.
|
|
10506
|
+
/** The message every team method throws. One string so they cannot drift. */
|
|
10507
|
+
teamIsDashboardOnly(method) {
|
|
10508
|
+
throw new Error(
|
|
10509
|
+
`${method} is not available with an API key. Team management is account-scoped and every route rejects the api_key principal, so this call can only fail. Manage team members in the dashboard, or use the store-scoped team tools on the admin MCP server (OAuth), which are bound to a single store.`
|
|
10510
|
+
);
|
|
10511
|
+
}
|
|
10055
10512
|
/**
|
|
10056
|
-
* @deprecated Retiring, but
|
|
10057
|
-
* is dashboard-only
|
|
10513
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10514
|
+
* api_key principal and `getStoreTeam` is dashboard-only. Use the dashboard.
|
|
10058
10515
|
*/
|
|
10059
10516
|
async getTeamMembers() {
|
|
10060
|
-
return this.
|
|
10517
|
+
return this.teamIsDashboardOnly("getTeamMembers");
|
|
10061
10518
|
}
|
|
10062
10519
|
/**
|
|
10063
|
-
* @deprecated Retiring, but
|
|
10064
|
-
* is dashboard-only
|
|
10520
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10521
|
+
* api_key principal and `getStoreTeam` is dashboard-only. Use the dashboard.
|
|
10065
10522
|
*/
|
|
10066
10523
|
async getTeamInvitations() {
|
|
10067
|
-
return this.
|
|
10524
|
+
return this.teamIsDashboardOnly("getTeamInvitations");
|
|
10068
10525
|
}
|
|
10069
10526
|
/**
|
|
10070
|
-
* @deprecated Retiring, but
|
|
10071
|
-
*
|
|
10527
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10528
|
+
* api_key principal and the store-level route is dashboard-only. Use the dashboard.
|
|
10072
10529
|
*/
|
|
10073
|
-
async inviteTeamMember(
|
|
10074
|
-
return this.
|
|
10530
|
+
async inviteTeamMember(_data) {
|
|
10531
|
+
return this.teamIsDashboardOnly("inviteTeamMember");
|
|
10075
10532
|
}
|
|
10076
10533
|
/**
|
|
10077
|
-
* @deprecated Retiring, but
|
|
10078
|
-
*
|
|
10534
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10535
|
+
* api_key principal and the store-level route is dashboard-only. Use the dashboard.
|
|
10079
10536
|
*/
|
|
10080
|
-
async resendTeamInvitation(
|
|
10081
|
-
return this.
|
|
10082
|
-
"POST",
|
|
10083
|
-
`/api/v1/team/invitations/${encodePathSegment(invitationId)}/resend`
|
|
10084
|
-
);
|
|
10537
|
+
async resendTeamInvitation(_invitationId) {
|
|
10538
|
+
return this.teamIsDashboardOnly("resendTeamInvitation");
|
|
10085
10539
|
}
|
|
10086
10540
|
/**
|
|
10087
|
-
* @deprecated Retiring, but
|
|
10088
|
-
*
|
|
10541
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10542
|
+
* api_key principal and the store-level route is dashboard-only. Use the dashboard.
|
|
10089
10543
|
*/
|
|
10090
|
-
async revokeTeamInvitation(
|
|
10091
|
-
|
|
10092
|
-
"DELETE",
|
|
10093
|
-
`/api/v1/team/invitations/${encodePathSegment(invitationId)}`
|
|
10094
|
-
);
|
|
10544
|
+
async revokeTeamInvitation(_invitationId) {
|
|
10545
|
+
return this.teamIsDashboardOnly("revokeTeamInvitation");
|
|
10095
10546
|
}
|
|
10096
10547
|
/**
|
|
10097
|
-
* @deprecated Retiring, but
|
|
10098
|
-
*
|
|
10548
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10549
|
+
* api_key principal and the store-level route is dashboard-only. Use the dashboard.
|
|
10099
10550
|
*/
|
|
10100
|
-
async updateTeamMemberRole(
|
|
10101
|
-
return this.
|
|
10102
|
-
"PATCH",
|
|
10103
|
-
`/api/v1/team/members/${encodePathSegment(memberId)}/role`,
|
|
10104
|
-
data
|
|
10105
|
-
);
|
|
10551
|
+
async updateTeamMemberRole(_memberId, _data) {
|
|
10552
|
+
return this.teamIsDashboardOnly("updateTeamMemberRole");
|
|
10106
10553
|
}
|
|
10107
10554
|
/**
|
|
10108
|
-
* @deprecated Retiring, but
|
|
10109
|
-
*
|
|
10555
|
+
* @deprecated Retiring, but this ALWAYS throws: `/v1/team/*` rejects the
|
|
10556
|
+
* api_key principal and the store-level route is dashboard-only. Use the dashboard.
|
|
10110
10557
|
*/
|
|
10111
|
-
async removeTeamMember(
|
|
10112
|
-
|
|
10558
|
+
async removeTeamMember(_memberId) {
|
|
10559
|
+
return this.teamIsDashboardOnly("removeTeamMember");
|
|
10113
10560
|
}
|
|
10114
10561
|
// -------------------- Store Team Management (Admin) --------------------
|
|
10115
10562
|
// Store-level team management. Each store has its own team with roles and permissions.
|