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/dist/index.js CHANGED
@@ -204,7 +204,7 @@ function isDevGuardsEnabled() {
204
204
  }
205
205
 
206
206
  // src/version.ts
207
- var SDK_VERSION = "2.4.0";
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.** A "10% off your first order" popup needs a
385
- * coupon from the dashboard create one with the `customer_first_order`
386
- * condition and show the code after a successful call. Subscribing does not
387
- * mint a code on its own.
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`): work in any mode.
452
- * - **Write operations** (`create`, `update`, `publish`, `unpublish`,
453
- * `remove`): admin mode only they call `/api/content/...` with
454
- * the API key. Calling from storefront / vibe-coded mode throws.
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
- * key: 'shipping',
482
- * name: 'Shipping FAQ',
483
- * data: { items: [{ question: '…', answer: '…' }] },
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.list({ type }) and filter by key, or use client.content.findById(id).",
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
- return this.adminRequest(
519
- "GET",
520
- `${adminBase()}?type=${encodeURIComponent(type)}`
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 createByType = async (type, input) => {
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("POST", adminBase(), { ...input, type });
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("POST", `${adminBase()}/${encodeURIComponent(id)}/publish`);
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("DELETE", `${adminBase()}/${encodeURIComponent(id)}`);
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
- /** List all PUBLISHED entries of this type. */
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
- /** Create a new entry in DRAFT (admin mode). */
569
- create: (input) => createByType(type, input)
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
- /** Find a single row by its admin id (admin mode). */
610
- findById: async (id) => {
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
- qs ? `${adminBase()}?${qs}` : adminBase()
829
+ `${adminBase()}/${encodeURIComponent(id)}`,
830
+ void 0,
831
+ requireStoreId("findById", storeId)
624
832
  );
625
833
  },
626
- /** Replace `data` (and optional metadata) on an existing row. */
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
- /** Transition status DRAFT → PUBLISHED. */
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
- /** Transition status PUBLISHED → DRAFT. */
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
- /** Hard delete the row. Admin mode only. */
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 draft = await brainerce.blog.create({ title: 'Hello World' });
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 published posts. Works in all modes.
672
- * Filters: `category`, `tag`, `page`, `limit`.
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
- query
981
+ {
982
+ ...query,
983
+ storeId: requireStoreId("getPosts", storeId)
984
+ }
697
985
  );
698
986
  },
699
987
  /**
700
- * Fetch one published post by its slug. Returns `null` on 404.
701
- * Works in all modes.
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
- return this.adminRequest("GET", path).catch(onNotFound);
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
- /** Create a blog post in DRAFT status. Admin mode only. */
714
- create: (input) => {
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
- /** Update a blog post by ID. Admin mode only. */
719
- update: (id, input) => {
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
- /** Transition status → PUBLISHED (sets publishedAt = now if unset). Admin mode only. */
728
- publish: (id) => {
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
- /** Transition status PUBLISHED → DRAFT. Admin mode only. */
736
- unpublish: (id) => {
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
- /** Hard-delete a blog post. Admin mode only. */
744
- remove: async (id) => {
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("DELETE", `${adminBase}/${encodeURIComponent(id)}`);
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) - DEPRECATED --------------------
10049
- // Account-level team methods. These are the ONLY team endpoints reachable with
10050
- // a `brainerce_*` API key. The store-level equivalents (`getStoreTeam`,
10051
- // `inviteStoreMember`, `updateStoreMember`, ...) sit behind
10052
- // `DashboardOnlyGuard`, which rejects api_key principals by design so they
10053
- // return 403 from the SDK, not 404, and "fixing" their path would not help.
10054
- // Do not migrate SDK code onto them.
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 there is no API-key replacement yet: `getStoreTeam`
10057
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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.adminRequest("GET", "/api/v1/team/members");
10517
+ return this.teamIsDashboardOnly("getTeamMembers");
10061
10518
  }
10062
10519
  /**
10063
- * @deprecated Retiring, but there is no API-key replacement yet: `getStoreTeam`
10064
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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.adminRequest("GET", "/api/v1/team/invitations");
10524
+ return this.teamIsDashboardOnly("getTeamInvitations");
10068
10525
  }
10069
10526
  /**
10070
- * @deprecated Retiring, but there is no API-key replacement yet: `inviteStoreMember`
10071
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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(data) {
10074
- return this.adminRequest("POST", "/api/v1/team/invitations", data);
10530
+ async inviteTeamMember(_data) {
10531
+ return this.teamIsDashboardOnly("inviteTeamMember");
10075
10532
  }
10076
10533
  /**
10077
- * @deprecated Retiring, but there is no API-key replacement yet: `resendStoreInvitation`
10078
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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(invitationId) {
10081
- return this.adminRequest(
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 there is no API-key replacement yet: `revokeStoreInvitation`
10088
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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(invitationId) {
10091
- await this.adminRequest(
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 there is no API-key replacement yet: `updateStoreMember`
10098
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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(memberId, data) {
10101
- return this.adminRequest(
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 there is no API-key replacement yet: `removeStoreMember`
10109
- * is dashboard-only (403 for api_key). Keep using this until one ships.
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(memberId) {
10112
- await this.adminRequest("DELETE", `/api/v1/team/members/${encodePathSegment(memberId)}`);
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.