brainerce 2.3.0 → 2.5.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 CHANGED
@@ -596,6 +596,26 @@ if (!page) notFound();
596
596
 
597
597
  All `get` / `getBySlug` return `null` on 404. Render a hard-coded fallback so the page never crashes when the merchant hasn't seeded yet.
598
598
 
599
+ **Admin mode (API key) — every call needs an explicit `storeId`.** The reads above (`get`, `list`, `getBySlug`) are storefront APIs; in admin mode they throw and point you at the admin pair below. Admin mode has no ambient store — `storeId` is only set in storefront mode — and the admin content routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant.
600
+
601
+ ```typescript
602
+ // Read (drafts included)
603
+ const rows = await client.content.listAdmin({ storeId, type: 'FAQ', status: 'DRAFT' });
604
+ const row = await client.content.findById('cnt_123', storeId);
605
+
606
+ // Write
607
+ const created = await client.content.faq.create(
608
+ { key: 'shipping', name: 'Shipping FAQ', data: { items: [{ question: '…', answer: '…' }] } },
609
+ storeId
610
+ );
611
+ await client.content.update('cnt_123', { name: 'Shipping FAQ' }, storeId);
612
+ await client.content.publish('cnt_123', storeId);
613
+ await client.content.unpublish('cnt_123', storeId);
614
+ await client.content.remove('cnt_123', storeId);
615
+ ```
616
+
617
+ Your API key needs the `content:read` scope for the reads and `content:write` for the writes.
618
+
599
619
  **Security**: `FAQ.items[i].answer`, `RICH_TEXT.html`, `PAGE.html`, and `Product.description` are **merchant-authored HTML**. The server does NOT pre-sanitize FAQ/RICH_TEXT/PAGE (merchants may embed iframes); `Product.description` is server-sanitized on write but you still sanitize on render. `Product.description` may contain `<video>` and host-locked YouTube/Vimeo `<iframe>` embeds, so allow those tags (iframe restricted to `www.youtube.com` / `www.youtube-nocookie.com` / `player.vimeo.com`) and add those hosts to your CSP `frame-src`. ALWAYS sanitize before injecting:
600
620
 
601
621
  ```typescript
@@ -612,18 +632,35 @@ Full guide: [Core Integration "Content"](https://brainerce.com/docs/integration/
612
632
  Merchants publish blog posts in **Content → Blog**. Storefronts choose their own URL scheme: render posts at `/blog/[slug]`, `/articles/[slug]`, or whatever fits the brand.
613
633
 
614
634
  ```typescript
615
- // List published posts (any SDK mode)
635
+ // List published posts (storefront / vibe-coded mode)
616
636
  const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });
617
637
 
618
638
  // Filter by category or tag
619
639
  const { data: news } = await client.blog.getPosts({ category: 'news' });
620
640
  const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });
621
641
 
622
- // Fetch one by slug — returns null on 404
642
+ // Fetch one by slug — returns null on 404 (storefront / vibe-coded mode)
623
643
  const post = await client.blog.getPost(params.slug);
624
644
  if (!post) notFound();
625
645
  ```
626
646
 
647
+ **Admin mode (API key) — every call needs an explicit `storeId`.** Admin mode has no ambient store (`storeId` is only set in storefront mode) and the admin blog routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant. Admin lookups are **by id, not by slug** — `getPost(slug)` is a storefront read and throws in admin mode.
648
+
649
+ ```typescript
650
+ // Read (drafts included)
651
+ const { data, meta } = await client.blog.getPosts({ page: 1, limit: 10 }, storeId);
652
+ const post = await client.blog.findById('post_123', storeId); // null on 404
653
+
654
+ // Write
655
+ const draft = await client.blog.create({ title: 'Hello World' }, storeId);
656
+ await client.blog.update('post_123', { title: 'Renamed' }, storeId);
657
+ await client.blog.publish('post_123', storeId);
658
+ await client.blog.unpublish('post_123', storeId);
659
+ await client.blog.remove('post_123', storeId);
660
+ ```
661
+
662
+ Your API key needs the `blog:read` scope for the reads and `blog:write` for the writes.
663
+
627
664
  **Security**: `post.content` is merchant-authored HTML. Always sanitize before rendering:
628
665
 
629
666
  ```tsx
@@ -638,7 +675,7 @@ return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;
638
675
 
639
676
  ### SEO: JSON-LD builders, sitemap helpers, IndexNow key, llms.txt + agents.md
640
677
 
641
- The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on `reviewCount > 0` with explicit `bestRating`/`worstRating`, AggregateOffer with `offerCount` for VARIABLE products, XSS-safe serialization, and the full availability mapping: `InStock` from the backend's pre-computed `inventory.inStock`, `BackOrder` for purchasable-while-out-of-stock products, `OutOfStock` otherwise). `buildProductJsonLd`'s Offer also always includes `itemCondition` (hardcoded `NewCondition`, for a first-party new-goods catalog), `priceValidUntil` when the product has an active sale-price window (`salePriceEndsAt`), and `shippingDetails` when you pass `shipping` (real flat-rate/free zones from `storeInfo.shipping`, omitted entirely and never fabricated if you don't pass it). A `'KIT'` product is emitted with **no `offers` block at all** a kit prices from its components, so publishing its placeholder `basePrice` would advertise a price it will not be sold at, and a price mismatch against the landing page is a Merchant Center violation. Prefer these builders over hand-rolled JSON-LD:
678
+ The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on `reviewCount > 0` with explicit `bestRating`/`worstRating`, AggregateOffer with `offerCount` for VARIABLE products, XSS-safe serialization, and the full availability mapping: `InStock` from the backend's pre-computed `inventory.inStock`, `BackOrder` for purchasable-while-out-of-stock products, `OutOfStock` otherwise). `buildProductJsonLd`'s Offer also always includes `itemCondition` (hardcoded `NewCondition`, for a first-party new-goods catalog), `priceValidUntil` when the product has an active sale-price window (`salePriceEndsAt`), and `shippingDetails` when you pass `shipping` (real flat-rate/free zones from `storeInfo.shipping`, omitted entirely and never fabricated if you don't pass it). A `'KIT'` product is emitted with **no `offers` block at all**, which is deliberate: kits are not published to Google or Meta shopping feeds, so the builder does not advertise a price for one. Do not hand-roll an Offer to add it. Prefer these builders over hand-rolled JSON-LD:
642
679
 
643
680
  ```tsx
644
681
  import {
@@ -1740,6 +1777,39 @@ const { data } = await client.getProducts({
1740
1777
 
1741
1778
  ### Products
1742
1779
 
1780
+ #### Kits (`type: 'KIT'`)
1781
+
1782
+ A kit is one purchasable product assembled from other catalog products. It is
1783
+ bought as a SINGLE cart line: pass the kit's own `productId` to `addToCart` and
1784
+ the server reserves each component behind that line. Adding components
1785
+ separately charges twice and reserves twice.
1786
+
1787
+ A kit has **no `inventory` block**. Read `kitAvailable` on the product (`null` =
1788
+ unlimited, `0` = not sellable), or `available` from `getKitComponents`. Outside
1789
+ `FIXED` pricing the product row's `basePrice` is a placeholder; use the resolved
1790
+ price.
1791
+
1792
+ ```typescript
1793
+ // Read what's in a kit (admin API key required)
1794
+ const kit = await client.getKitComponents('prod_123');
1795
+ console.log(kit.price, kit.available, kit.pricingMode);
1796
+ kit.components.forEach((c) => console.log(c.name, 'x' + c.quantity));
1797
+
1798
+ // Replace its contents, and how it's priced
1799
+ await client.setKitComponents('prod_123', {
1800
+ components: [
1801
+ { componentProductId: 'prod_bottle', quantity: 1 },
1802
+ // A VARIABLE component must have one variant pinned
1803
+ { componentProductId: 'prod_glass', componentVariantId: 'pv_300ml', quantity: 2 },
1804
+ ],
1805
+ pricingMode: 'SUM_MINUS_PERCENT', // or 'FIXED' | 'SUM'
1806
+ discountValue: 10,
1807
+ });
1808
+ ```
1809
+
1810
+ On the storefront, `getProductBySlug` returns `kitComponents` for display so a
1811
+ shopper can see what is in the box. Those rows are display only.
1812
+
1743
1813
  #### Get Products (with pagination)
1744
1814
 
1745
1815
  ```typescript
@@ -1888,10 +1958,35 @@ interface Product {
1888
1958
  displayCurrency?: string; // ISO 4217 of the display* fields
1889
1959
  status: string; // e.g. "active" | "draft"
1890
1960
  type: 'SIMPLE' | 'VARIABLE' | 'KIT';
1961
+ // ── KIT only ──────────────────────────────────────────────────────────────
1962
+ // What the kit contains. Returned on the single-product (by slug) read ONLY,
1963
+ // never on list responses. DISPLAY ONLY — render it so the shopper sees what
1964
+ // is in the box; never turn these rows into cart lines.
1965
+ kitComponents?: Array<{
1966
+ productId: string;
1967
+ variantId: string | null;
1968
+ name: string;
1969
+ sku: string | null;
1970
+ quantity: number;
1971
+ image: string | null;
1972
+ }>;
1973
+ // How many kits can be sold: whichever component runs out first decides.
1974
+ // null = unlimited, 0 = NOT SELLABLE (including a kit with no components).
1975
+ // A kit has NO `inventory` block, so this is the ONLY stock signal. Reading
1976
+ // a missing `inventory` as "in stock" renders a sold-out kit as buyable.
1977
+ kitAvailable?: number | null;
1978
+ // How the kit is priced. FIXED (default) keeps the merchant's own basePrice.
1979
+ // SUM = exactly what the components cost, recomputed on every read.
1980
+ // SUM_MINUS_PERCENT = that sum less kitDiscountValue percent (0-100).
1981
+ // Outside FIXED the stored basePrice is a PLACEHOLDER — but the storefront
1982
+ // reads overlay `basePrice` with the resolved price, so render basePrice.
1983
+ kitPricingMode?: 'FIXED' | 'SUM' | 'SUM_MINUS_PERCENT';
1984
+ kitDiscountValue?: number;
1985
+ // ──────────────────────────────────────────────────────────────────────────
1891
1986
  isDownloadable?: boolean;
1892
1987
  downloads?: DownloadFile[] | null; // when isDownloadable
1893
1988
  images?: ProductImage[];
1894
- inventory?: InventoryInfo | null;
1989
+ inventory?: InventoryInfo | null; // null/absent on a KIT — use kitAvailable
1895
1990
  variants?: ProductVariant[];
1896
1991
  categories?: Array<{ id: string; name: string; slug?: string | null }>; // NOT string[] — use slug to link to /category/{slug}
1897
1992
  brands?: Array<{ id: string; name: string }>; // objects, not string[]
@@ -2222,19 +2317,41 @@ function ProductPrice({ product }: { product: Product }) {
2222
2317
 
2223
2318
  **When the product is a `'KIT'`:**
2224
2319
 
2225
- A kit is a bundle sold as one line, priced from real component products. It is
2226
- not a Bundle offer and not a discount rule it is a product with its own page,
2227
- slug and SEO.
2228
-
2229
- - Show `basePrice` / `salePrice` as you would for a `'SIMPLE'` product. The
2230
- backend has already resolved the kit's price from its components, so do NOT
2231
- try to sum them yourself.
2232
- - A kit carries **no `inventory` of its own**. Availability is derived from what
2233
- its components have in stock, so never render "out of stock" from a missing
2234
- `inventory` object on a kit.
2235
- - Add it to the cart by `productId` with no `variantId`, like a `'SIMPLE'`
2236
- product. Kits take no variants and no modifier selections; sending
2237
- `selections` is rejected.
2320
+ A kit is one purchasable product assembled from other catalog products, sold as
2321
+ one line. It is not a Bundle offer and not a discount rule: it is a product with
2322
+ its own page, slug and SEO.
2323
+
2324
+ - Show `basePrice` as you would for a `'SIMPLE'` product. The storefront reads
2325
+ overlay it with the **resolved** kit price, so do NOT sum the components
2326
+ yourself.
2327
+ - **`salePrice` is only meaningful on a `FIXED` kit.** On `SUM` and
2328
+ `SUM_MINUS_PERCENT` the discount is already expressed by the pricing mode and
2329
+ the kit's own sale price is ignored by the engine, so rendering a strikethrough
2330
+ from it shows the shopper a discount that is not being applied. Both reads
2331
+ behave the same way: outside `FIXED` they null the kit's `salePrice`, and on
2332
+ `FIXED` they leave `basePrice`/`salePrice` untouched so the normal was/now
2333
+ logic works. Gate any strikethrough on `kitPricingMode === 'FIXED'`.
2334
+ - **Do not tell the shopper the price is fixed.** There are three pricing modes
2335
+ and two of them move on their own. `kitPricingMode: 'FIXED'` (the default) is
2336
+ the number the merchant typed and stays put. `'SUM'` makes the kit cost
2337
+ exactly what its contents cost, **recomputed on every read**, so a component
2338
+ going on sale lowers the kit price by itself. `'SUM_MINUS_PERCENT'` is that
2339
+ sum less `kitDiscountValue` percent. Outside `FIXED` the stored `basePrice`
2340
+ on the raw product row is a **placeholder** — never cache it, and never
2341
+ compute from a value you fetched earlier.
2342
+ - A kit carries **no `inventory` of its own**. Read **`kitAvailable`**:
2343
+ `null` = unlimited, `0` = **not sellable** (including a kit with no
2344
+ components), any other number = how many kits can be sold. "Don't render
2345
+ out-of-stock just because `inventory` is missing" is only half the rule:
2346
+ without `kitAvailable` a sold-out kit renders as buyable, which is worse.
2347
+ - Render **`kitComponents`** so the shopper can see what is in the box. It comes
2348
+ back on the single-product (by slug) read only, never on list responses, as
2349
+ `{ productId, variantId, name, sku, quantity, image }` rows. They are
2350
+ **display only**.
2351
+ - Add it to the cart by the kit's own `productId` with **no `variantId`** — a
2352
+ kit has no variants and sending one is rejected. It takes no modifier
2353
+ `selections` either (HTTP 400). ⛔ **Never add its components as separate
2354
+ lines**: that charges the customer twice and reserves the stock twice.
2238
2355
 
2239
2356
  #### Rendering Product Descriptions
2240
2357
 
@@ -3196,9 +3313,15 @@ interface Checkout {
3196
3313
  discountAmount: string;
3197
3314
  shippingAmount: string;
3198
3315
  taxAmount: string; // "0" in inclusive (VAT) mode — see taxBreakdown.totalTax
3199
- taxBreakdown?: TaxBreakdown | null; // { totalTax, pricesIncludeTax, breakdown[] }
3316
+ taxBreakdown?: TaxBreakdown | null; // { subtotal, shippingNet, totalTax, total, pricesIncludeTax, breakdown[] }
3200
3317
  // breakdown[] is ONE ROW PER TAX and is often more than one row (Canada charges
3201
3318
  // GST + PST/QST). Loop it; never read breakdown[0].
3319
+ // taxBreakdown.subtotal is NOT the `subtotal` above: it is the net of every
3320
+ // taxed line and it INCLUDES the shipping net, so subtotal + shipping + tax
3321
+ // counts shipping twice. Use taxBreakdown.subtotal - taxBreakdown.shippingNet
3322
+ // for a goods-only row, and subtract shippingNet rather than the gross
3323
+ // shippingAmount — they are equal only while shipping is untaxed. The
3324
+ // top-level `subtotal` is goods-only and needs no adjustment.
3202
3325
  total: string;
3203
3326
  couponCode?: string | null;
3204
3327
  // Gift cards held against this checkout, oldest first. Read these to re-render
@@ -5083,6 +5206,12 @@ const product: Product = await client.createProduct({
5083
5206
  costPrice: 12.0, // Optional, internal — never synced to platforms
5084
5207
  status: 'active', // 'active' | 'draft' (default: 'active')
5085
5208
  type: 'SIMPLE', // 'SIMPLE' | 'VARIABLE' | 'KIT' (default: 'SIMPLE')
5209
+ // KIT only. FIXED (default) means you set basePrice and it stays put. SUM
5210
+ // means the kit costs exactly what its components cost, recomputed on every
5211
+ // read, so a component going on sale lowers the kit price on its own.
5212
+ // SUM_MINUS_PERCENT is that sum less kitDiscountValue percent.
5213
+ // kitPricingMode: 'SUM_MINUS_PERCENT',
5214
+ // kitDiscountValue: 10, // 0-100, required with SUM_MINUS_PERCENT
5086
5215
  categories: ['cat_id'], // Existing category IDs
5087
5216
  categoryNames: ['Apparel'], // Or assign/auto-create by name — merged with `categories`
5088
5217
  brands: ['brand_id'],
@@ -5155,6 +5284,69 @@ await client.bulkSaveVariants(variableProduct.id, {
5155
5284
 
5156
5285
  **GTIN vs MPN:** these are two different identifiers, not interchangeable: GTIN (EAN/UPC/ISBN) is a universal barcode; MPN is manufacturer-specific and only meaningful paired with a brand. Provide GTIN when the product has one; otherwise brand + MPN. A product typically needs one or the other, not both.
5157
5286
 
5287
+ ### Kit Management (`type: 'KIT'`)
5288
+
5289
+ ⛔ **`createProduct({ type: 'KIT' })` gives you an EMPTY kit, and an empty kit
5290
+ cannot be bought.** A kit is defined by its component list, so creating the
5291
+ product is only half the job: until you call `setKitComponents`, the kit has no
5292
+ price, `kitAvailable` is `0`, and `addToCart` refuses it as unavailable. It is
5293
+ not an error state you will see in the response — the create succeeds and the
5294
+ product looks fine. Create, then set components, in that order.
5295
+
5296
+ ```typescript
5297
+ import type { KitDetail, KitComponentWriteInput } from 'brainerce';
5298
+
5299
+ // 1. Create the kit product (empty and unbuyable at this point)
5300
+ const kit = await client.createProduct({
5301
+ name: 'Wine Gift Box',
5302
+ basePrice: 199, // required to save; only charged on FIXED pricing
5303
+ type: 'KIT',
5304
+ kitPricingMode: 'SUM_MINUS_PERCENT', // also settable on updateProduct
5305
+ kitDiscountValue: 10, // 0-100, required with SUM_MINUS_PERCENT
5306
+ });
5307
+
5308
+ // 2. Give it contents — this is what makes it purchasable
5309
+ const detail: KitDetail = await client.setKitComponents(kit.id, {
5310
+ components: [
5311
+ { componentProductId: 'prod_bottle', quantity: 1 },
5312
+ // A VARIABLE component MUST have exactly one variant pinned
5313
+ { componentProductId: 'prod_glass', componentVariantId: 'pv_300ml', quantity: 2 },
5314
+ ],
5315
+ pricingMode: 'SUM_MINUS_PERCENT', // optional — omit to leave the mode alone
5316
+ discountValue: 10,
5317
+ });
5318
+
5319
+ // 3. Read it back any time
5320
+ const read: KitDetail = await client.getKitComponents(kit.id);
5321
+ console.log(read.price, read.available, read.pricingMode, read.discountValue);
5322
+ read.components.forEach((c) => console.log(c.name, 'x' + c.quantity, c.unitPrice, c.available));
5323
+ ```
5324
+
5325
+ `setKitComponents` is a **full replace**, not a patch: send the list the kit
5326
+ should end up with. An empty `components` array clears the kit and makes it
5327
+ unbuyable again. Max **30** components; `quantity` is 1-2000 per slot.
5328
+
5329
+ `getKitComponents` is safe to call on any product id — a non-KIT returns an
5330
+ empty, unsellable shape rather than throwing, so callers need not branch on
5331
+ product type first.
5332
+
5333
+ **Pricing modes:** `FIXED` (default) charges the kit's own `basePrice` /
5334
+ `salePrice`. `SUM` charges exactly what the components cost, **recomputed on
5335
+ every read**, so a component going on sale lowers the kit price on its own.
5336
+ `SUM_MINUS_PERCENT` is that sum less `kitDiscountValue` percent. Outside `FIXED`
5337
+ the stored `basePrice` is a **placeholder** and the kit's own `salePrice` is
5338
+ ignored. Use `KitDetail.price`, or the `basePrice` on a storefront read, which
5339
+ outside `FIXED` is already overlaid with the resolved figure (on `FIXED` the
5340
+ stored prices are returned as they are, sale included).
5341
+
5342
+ **Refused on write:** a product that is not a `KIT`; a component from another
5343
+ store; a component that is itself a `KIT` (kits do not nest); a `VARIABLE`
5344
+ component with no `componentVariantId`; a variant that does not belong to its
5345
+ product; the same slot listed twice; and the kit itself. You also cannot delete
5346
+ a product while it sits inside a kit — remove it from the kit first. `SIMPLE` ↔
5347
+ `KIT` conversion is allowed (`KIT` → `SIMPLE` only once the kit is empty);
5348
+ `VARIABLE` ↔ `KIT` is refused outright.
5349
+
5158
5350
  ### Bulk Product Creation (catalog import)
5159
5351
 
5160
5352
  Importing a catalog (a supplier feed, a CSV/Excel export, a store migration)
@@ -5897,8 +6089,10 @@ Each store has its own team with roles (`OWNER`, `MANAGER`, `STAFF`, `VIEWER`) a
5897
6089
  granular permissions, including per-sales-channel scoping. **Managing it is a dashboard
5898
6090
  operation. There is no SDK path to it, and this is deliberate.**
5899
6091
 
5900
- This section previously showed store-level team calls. **Since SDK 2.1.1 they throw a
5901
- `BrainerceError` that says so, instead of returning a bare 404.** Ten methods are
6092
+ This section previously showed store-level team calls. **Since SDK 2.2.0 they throw a
6093
+ `BrainerceError` that says so, instead of returning a bare 404.** (The change is dated
6094
+ 2.1.1 in the changelog, but that version was never published: npm went 2.1.0 straight to
6095
+ 2.2.0, so 2.2.0 is the first release you can actually install it from.) Ten methods are
5902
6096
  affected: `getStoreTeam`, `inviteStoreMember`, `updateStoreMember`,
5903
6097
  `updateStoreMemberSalesChannels`, `removeStoreMember`, `resendStoreInvitation`,
5904
6098
  `revokeStoreInvitation`, `acceptStoreInvitation`, `getMyStores` and
@@ -5919,17 +6113,22 @@ route is `@Public()`, and it was only ever failing because the SDK asked for
5919
6113
  Use it to render an invitation-acceptance page; the acceptance itself must happen in the
5920
6114
  dashboard, because it is matched against the invited user's own email address.
5921
6115
 
5922
- > **The older account-level methods are not a substitute for this.** `getTeamMembers`,
5923
- > `getTeamInvitations`, `inviteTeamMember`, `resendTeamInvitation`, `revokeTeamInvitation`,
5924
- > `updateTeamMemberRole` and `removeTeamMember` do still reach `/api/v1/team/…`, but they
5925
- > manage the **account** team, not a store's. They will not invite anyone to a store or
5926
- > scope a member to a sales channel; only the dashboard does that.
6116
+ > **The older account-level methods are not a substitute, and they do not work
6117
+ > either.** `getTeamMembers`, `getTeamInvitations`, `inviteTeamMember`,
6118
+ > `resendTeamInvitation`, `revokeTeamInvitation`, `updateTeamMemberRole` and
6119
+ > `removeTeamMember` now **throw** instead of calling `/api/v1/team/…`. An earlier
6120
+ > version of this page said they remained the supported call for the account team.
6121
+ > That was wrong: those routes hand the team service a synthetic `api-key-user`
6122
+ > principal that has no account membership row, so every one of them answered
6123
+ > `Access denied to this account`. The SDK raises that locally now, naming the cause,
6124
+ > rather than sending a request that cannot succeed.
5927
6125
  >
5928
- > **For the account team, they remain the supported call.** All seven are tagged
5929
- > `@deprecated`, which records an intent to retire them, not a migration you can perform
5930
- > today. There is no API-key replacement: the store-level methods named above are
5931
- > dashboard-only. Keep using these until an API-key route ships, and expect the tag to
5932
- > outlive this note.
6126
+ > **There is no API-key path to team management, at either level, and there should
6127
+ > not be.** Team membership is account-scoped and an invite can grant ownership of
6128
+ > every store on the account, while an API key is bound to exactly one store.
6129
+ > Admitting the key would turn a single-store credential into account ownership.
6130
+ > Manage members in the dashboard, or use the store-scoped `team:*` tools on the
6131
+ > admin MCP server, which are OAuth-authenticated and bound to one store.
5933
6132
 
5934
6133
  ### Email Settings & Templates
5935
6134