brainerce 2.1.0 → 2.3.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.mjs CHANGED
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "2.0.3";
118
+ var SDK_VERSION = "2.3.0";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -361,7 +361,7 @@ var _BrainerceClient = class _BrainerceClient {
361
361
  * Works in all three SDK modes (vibe-coded, storefront, admin):
362
362
  * - **Public reads** (`get`, `list`, `getBySlug`): work in any mode.
363
363
  * - **Write operations** (`create`, `update`, `publish`, `unpublish`,
364
- * `remove`): admin mode only — they call `/api/v1/content/...` with
364
+ * `remove`): admin mode only — they call `/api/content/...` with
365
365
  * the API key. Calling from storefront / vibe-coded mode throws.
366
366
  *
367
367
  * **Default key:** every type has `'main'` as its universal default key.
@@ -398,7 +398,7 @@ var _BrainerceClient = class _BrainerceClient {
398
398
  this.content = (() => {
399
399
  const DEFAULT_KEY = "main";
400
400
  const publicGetPath = (type, key) => `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
401
- const adminBase = () => "/api/v1/content";
401
+ const adminBase = () => "/api/content";
402
402
  const publicGet = async (type, key, locale) => {
403
403
  const query = locale ? { locale } : void 0;
404
404
  const path = publicGetPath(type, key);
@@ -837,10 +837,19 @@ var _BrainerceClient = class _BrainerceClient {
837
837
  this.customerToken = null;
838
838
  }
839
839
  // -------------------- Private Methods --------------------
840
+ /**
841
+ * Turn an `idempotencyKey` option into the header the backend reads.
842
+ *
843
+ * Returns `undefined` when there is no key, so the header is absent rather
844
+ * than empty — the interceptor rejects a present-but-blank key with a 400.
845
+ */
846
+ idempotencyHeaders(options) {
847
+ return options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : void 0;
848
+ }
840
849
  /**
841
850
  * Make a request to the Admin API (requires apiKey)
842
851
  */
843
- async adminRequest(method, path, body, queryParams, responseType = "json") {
852
+ async adminRequest(method, path, body, queryParams, responseType = "json", extraHeaders) {
844
853
  if (!this.apiKey) {
845
854
  throw new BrainerceError(
846
855
  "This operation requires an API key. Initialize with apiKey instead of storeId.",
@@ -870,6 +879,11 @@ var _BrainerceClient = class _BrainerceClient {
870
879
  if (this.locale) {
871
880
  headers["Accept-Language"] = this.locale;
872
881
  }
882
+ if (extraHeaders) {
883
+ for (const [key, value] of Object.entries(extraHeaders)) {
884
+ if (value) headers[key] = value;
885
+ }
886
+ }
873
887
  for (let attempt = 0; attempt < 2; attempt++) {
874
888
  const controller = new AbortController();
875
889
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -2018,11 +2032,9 @@ var _BrainerceClient = class _BrainerceClient {
2018
2032
  queryParams
2019
2033
  );
2020
2034
  }
2021
- return this.adminRequest(
2022
- "GET",
2023
- "/api/v1/search/suggestions",
2024
- void 0,
2025
- queryParams
2035
+ throw new BrainerceError(
2036
+ "getSearchSuggestions is a storefront API. There is no admin suggestions endpoint \u2014 construct the client with `salesChannelId` or `storeId` to use it, or call getProducts({ search }) in admin mode.",
2037
+ 400
2026
2038
  );
2027
2039
  }
2028
2040
  /**
@@ -2196,21 +2208,25 @@ var _BrainerceClient = class _BrainerceClient {
2196
2208
  );
2197
2209
  }
2198
2210
  /**
2199
- * Publish a product to specific platforms
2211
+ * Publish a product to specific platforms.
2200
2212
  *
2201
- * @example
2202
- * ```typescript
2203
- * const result = await client.publishProduct('prod_123', ['SHOPIFY', 'WOOCOMMERCE']);
2204
- * console.log('Publish results:', result.results);
2205
- * ```
2213
+ * **Not callable.** The API-key `/v1` surface has no `products/:id/publish`
2214
+ * route. Platform publishing for products exists only on the dashboard
2215
+ * surface (`POST /api/products/:id/publish?storeId=`,
2216
+ * `products.controller.ts:556`), which resolves the acting user from a
2217
+ * dashboard session an API key does not carry. The generic trigger
2218
+ * `POST /v1/sync` answers `501 Not Implemented` and points at per-resource
2219
+ * publish endpoints — and products have none.
2220
+ *
2221
+ * {@link publishProductToSalesChannel} is a DIFFERENT operation: it controls
2222
+ * visibility on a vibe-coded storefront, not a push to an external platform.
2206
2223
  */
2207
2224
  async publishProduct(productId, platforms) {
2208
- return this.request(
2209
- "POST",
2210
- `/api/v1/products/${encodePathSegment(productId)}/publish`,
2211
- {
2212
- platforms
2213
- }
2225
+ void productId;
2226
+ void platforms;
2227
+ throw new BrainerceError(
2228
+ "publishProduct is not a route on the API-key /v1 surface. There is no products/:id/publish endpoint to call, so nothing was published; publish the product to a platform from the Brainerce dashboard. Note that publishProductToSalesChannel is a different operation \u2014 it controls vibe-coded storefront visibility, not an external-platform push.",
2229
+ 400
2214
2230
  );
2215
2231
  }
2216
2232
  // -------------------- Variants --------------------
@@ -4205,6 +4221,9 @@ var _BrainerceClient = class _BrainerceClient {
4205
4221
  * flags reflect the live state. Call this on cart load if you want to
4206
4222
  * surface drift to the customer before they reach checkout.
4207
4223
  *
4224
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
4225
+ * route for this, so an admin-mode client throws instead of 404ing.
4226
+ *
4208
4227
  * @example
4209
4228
  * ```typescript
4210
4229
  * const cart = await client.recalculateCart('cart_123');
@@ -4229,9 +4248,9 @@ var _BrainerceClient = class _BrainerceClient {
4229
4248
  "cart"
4230
4249
  );
4231
4250
  }
4232
- return this.withGuards(
4233
- this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/recalculate`),
4234
- "cart"
4251
+ throw new BrainerceError(
4252
+ "recalculateCart is not a route on the API-key /v1 surface. There is no cart/:cartId/recalculate endpoint to call, so nothing was recalculated. Cart recalculation is a storefront operation \u2014 call it from a client constructed with `salesChannelId` (vibe-coded) or `storeId` (public storefront), where the route exists.",
4253
+ 400
4235
4254
  );
4236
4255
  }
4237
4256
  /**
@@ -4240,6 +4259,9 @@ var _BrainerceClient = class _BrainerceClient {
4240
4259
  * subsequent `createCheckout` call will then succeed (it would otherwise
4241
4260
  * throw `PRICE_DRIFT`).
4242
4261
  *
4262
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
4263
+ * route for this, so an admin-mode client throws instead of 404ing.
4264
+ *
4243
4265
  * @example
4244
4266
  * ```typescript
4245
4267
  * try {
@@ -4274,12 +4296,9 @@ var _BrainerceClient = class _BrainerceClient {
4274
4296
  "cart"
4275
4297
  );
4276
4298
  }
4277
- return this.withGuards(
4278
- this.adminRequest(
4279
- "POST",
4280
- `/api/v1/cart/${encodePathSegment(cartId)}/refresh-snapshots`
4281
- ),
4282
- "cart"
4299
+ throw new BrainerceError(
4300
+ "refreshCartSnapshots is not a route on the API-key /v1 surface. There is no cart/:cartId/refresh-snapshots endpoint to call, so no price snapshots were refreshed. Snapshot refresh is a storefront operation \u2014 call it from a client constructed with `salesChannelId` (vibe-coded) or `storeId` (public storefront), where the route exists.",
4301
+ 400
4283
4302
  );
4284
4303
  }
4285
4304
  /**
@@ -5723,6 +5742,202 @@ var _BrainerceClient = class _BrainerceClient {
5723
5742
  return this.adminRequest("POST", "/api/v1/gift-cards/balance", { code });
5724
5743
  }
5725
5744
  // ==========================================================================
5745
+ // Gift cards — administration (API key only)
5746
+ //
5747
+ // Full programmatic management, unlike Shopify, which requires you to ask
5748
+ // their support for the equivalent scope. The gate here is the SCOPE the
5749
+ // merchant granted your key, so the separation matters: `gift_cards:issue`
5750
+ // mints stored value and `gift_cards:adjust` rewrites a balance, and neither
5751
+ // is implied by `gift_cards:read`. Ask for the least your integration needs.
5752
+ //
5753
+ // Every call reaches the same service the dashboard uses, so the rules hold
5754
+ // identically: a note is mandatory on anything that moves value, a debit
5755
+ // cannot cross what live checkout holds have reserved, and there is no delete
5756
+ // anywhere — the ledger is append-only.
5757
+ // ==========================================================================
5758
+ /**
5759
+ * List gift cards.
5760
+ *
5761
+ * `search` matches the LAST FOUR of a code or part of a recipient email. It
5762
+ * cannot match a full code: only an HMAC is stored, so there is nothing to
5763
+ * search against.
5764
+ *
5765
+ * Requires `gift_cards:read`.
5766
+ */
5767
+ async listGiftCards(params) {
5768
+ const q = new URLSearchParams();
5769
+ if (params?.page) q.set("page", String(params.page));
5770
+ if (params?.limit) q.set("limit", String(params.limit));
5771
+ if (params?.filter) q.set("filter", params.filter);
5772
+ if (params?.search) q.set("search", params.search);
5773
+ const qs = q.toString();
5774
+ return this.adminRequest(
5775
+ "GET",
5776
+ `/api/v1/gift-cards${qs ? `?${qs}` : ""}`
5777
+ );
5778
+ }
5779
+ /**
5780
+ * Outstanding gift card liability, per currency.
5781
+ *
5782
+ * This is the month-end number. Read `byCurrency` if the store sells in more
5783
+ * than one — currencies are never summed together.
5784
+ *
5785
+ * Requires `gift_cards:read`.
5786
+ */
5787
+ async getGiftCardLiability() {
5788
+ return this.adminRequest("GET", "/api/v1/gift-cards/liability");
5789
+ }
5790
+ /**
5791
+ * One gift card with its full ledger.
5792
+ *
5793
+ * Requires `gift_cards:read`.
5794
+ */
5795
+ async getGiftCard(giftCardId) {
5796
+ return this.adminRequest(
5797
+ "GET",
5798
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}`
5799
+ );
5800
+ }
5801
+ /**
5802
+ * Issue a gift card.
5803
+ *
5804
+ * ⚠️ **The code comes back exactly once.** It is stored only as an HMAC, so
5805
+ * this response is the only time it exists in readable form anywhere. Persist
5806
+ * it or deliver it before you discard the response — no later call, dashboard
5807
+ * screen or database query can recover it.
5808
+ *
5809
+ * A `note` is required. Refused when gift cards are switched off for the
5810
+ * store.
5811
+ *
5812
+ * ⚠️ **Pass an `idempotencyKey`.** It is the ONLY recovery that exists here:
5813
+ * re-sending the identical request with the same key inside 24 hours replays
5814
+ * the cached response, code included. Without one, a retried timeout mints a
5815
+ * SECOND card and a second real liability. The key is sent as the
5816
+ * `Idempotency-Key` header; reuse the same value across retries of the same
5817
+ * logical issue, never a fresh random one.
5818
+ *
5819
+ * Requires `gift_cards:issue`.
5820
+ *
5821
+ * @example
5822
+ * ```typescript
5823
+ * const card = await client.issueGiftCard(
5824
+ * {
5825
+ * amount: '200.00',
5826
+ * note: 'Compensation for order #1042',
5827
+ * recipientEmail: 'dana@example.com',
5828
+ * },
5829
+ * { idempotencyKey: 'compensation-order-1042' }
5830
+ * );
5831
+ * await sendToCustomer(card.plaintextCode); // your only chance
5832
+ * ```
5833
+ */
5834
+ async issueGiftCard(data, options) {
5835
+ return this.adminRequest(
5836
+ "POST",
5837
+ "/api/v1/gift-cards",
5838
+ data,
5839
+ void 0,
5840
+ "json",
5841
+ this.idempotencyHeaders(options)
5842
+ );
5843
+ }
5844
+ /**
5845
+ * Re-issue a gift card onto a new code.
5846
+ *
5847
+ * The answer to a customer losing their code. Mints a new code, moves the
5848
+ * WHOLE balance to it, and REVOKES the old card.
5849
+ *
5850
+ * **This is not a resend.** The old code stops working the moment this
5851
+ * returns — if the customer still holds a printed card, it dies. Refused
5852
+ * while a checkout holds value on the card. The original expiry carries
5853
+ * forward, so this cannot be used to restart an expiry clock.
5854
+ *
5855
+ * The new code is returned exactly once, under the same rules as issuance —
5856
+ * so pass an `idempotencyKey` here for the same reason, and with the same
5857
+ * force: a retried timeout without one revokes the replacement it just made
5858
+ * and mints another.
5859
+ *
5860
+ * Requires `gift_cards:issue`.
5861
+ */
5862
+ async reissueGiftCard(giftCardId, note, options) {
5863
+ return this.adminRequest(
5864
+ "POST",
5865
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/reissue`,
5866
+ { note },
5867
+ void 0,
5868
+ "json",
5869
+ this.idempotencyHeaders(options)
5870
+ );
5871
+ }
5872
+ /**
5873
+ * Adjust a gift card balance.
5874
+ *
5875
+ * `delta` is a SIGNED decimal string: `"25.00"` adds, `"-25.00"` takes away.
5876
+ * The `note` is required and is written to the ledger permanently — it is the
5877
+ * row a finance review reads a year from now.
5878
+ *
5879
+ * A debit cannot take the balance below what live checkout holds have already
5880
+ * reserved; that refusal names the held amount so you can act on it.
5881
+ *
5882
+ * Pass an `idempotencyKey`: an adjustment is a relative move, so a retried
5883
+ * timeout without one applies the delta twice.
5884
+ *
5885
+ * Requires `gift_cards:adjust`.
5886
+ */
5887
+ async adjustGiftCardBalance(giftCardId, delta, note, options) {
5888
+ return this.adminRequest(
5889
+ "PATCH",
5890
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/adjust`,
5891
+ { delta, note },
5892
+ void 0,
5893
+ "json",
5894
+ this.idempotencyHeaders(options)
5895
+ );
5896
+ }
5897
+ /**
5898
+ * Enable, disable or revoke one gift card.
5899
+ *
5900
+ * Deliberately does NOT touch live holds: a checkout that already reserved
5901
+ * value settles normally, because pulling it out from under a shopper
5902
+ * mid-payment would strand a provider charge already in flight. Disabling
5903
+ * stops NEW holds, which is what "off" actually means.
5904
+ *
5905
+ * Requires `gift_cards:write`.
5906
+ */
5907
+ async setGiftCardStatus(giftCardId, status, options) {
5908
+ return this.adminRequest(
5909
+ "PATCH",
5910
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/status`,
5911
+ { status },
5912
+ void 0,
5913
+ "json",
5914
+ this.idempotencyHeaders(options)
5915
+ );
5916
+ }
5917
+ /**
5918
+ * Disable or reactivate many gift cards at once.
5919
+ *
5920
+ * `REVOKED` is not accepted here — it belongs to re-issue, which moves the
5921
+ * balance to a replacement first. Revoking in bulk would strand balances with
5922
+ * nowhere to go. Cards already revoked are skipped, so the returned count is
5923
+ * the honest one and may be lower than the ids you sent.
5924
+ *
5925
+ * There is no bulk delete, here or anywhere: the ledger is append-only and a
5926
+ * card may carry a statutory retention life.
5927
+ *
5928
+ * Requires `gift_cards:write`.
5929
+ */
5930
+ async bulkSetGiftCardStatus(giftCardIds, status, options) {
5931
+ return this.adminRequest(
5932
+ "PATCH",
5933
+ "/api/v1/gift-cards/bulk/status",
5934
+ { giftCardIds, status },
5935
+ void 0,
5936
+ "json",
5937
+ this.idempotencyHeaders(options)
5938
+ );
5939
+ }
5940
+ // ==========================================================================
5726
5941
  // Donations
5727
5942
  // ==========================================================================
5728
5943
  /**
@@ -5865,10 +6080,7 @@ var _BrainerceClient = class _BrainerceClient {
5865
6080
  "/checkouts/shipping-destinations"
5866
6081
  );
5867
6082
  }
5868
- return this.adminRequest(
5869
- "GET",
5870
- "/api/v1/checkouts/shipping-destinations"
5871
- );
6083
+ return this.adminRequest("GET", "/api/checkouts/shipping-destinations");
5872
6084
  }
5873
6085
  /**
5874
6086
  * Set shipping address on checkout (includes customer email).
@@ -6127,11 +6339,14 @@ var _BrainerceClient = class _BrainerceClient {
6127
6339
  if (this.storeId && !this.apiKey) {
6128
6340
  return this.storefrontRequest("GET", "/pickup-locations");
6129
6341
  }
6130
- return this.adminRequest("GET", "/api/v1/checkouts/pickup-locations");
6342
+ return this.adminRequest("GET", "/api/checkouts/pickup-locations");
6131
6343
  }
6132
6344
  /**
6133
6345
  * Set delivery type on checkout (shipping or pickup).
6134
6346
  *
6347
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
6348
+ * route for this, so an admin-mode client throws instead of 404ing.
6349
+ *
6135
6350
  * @example
6136
6351
  * ```typescript
6137
6352
  * const checkout = await client.setDeliveryType('checkout_123', 'pickup');
@@ -6156,12 +6371,9 @@ var _BrainerceClient = class _BrainerceClient {
6156
6371
  }
6157
6372
  );
6158
6373
  }
6159
- return this.adminRequest(
6160
- "PATCH",
6161
- `/api/v1/checkout/${encodePathSegment(checkoutId)}/delivery-type`,
6162
- {
6163
- deliveryType
6164
- }
6374
+ throw new BrainerceError(
6375
+ "setDeliveryType is not a route on the API-key /v1 surface. There is no checkout/:checkoutId/delivery-type endpoint to call, so the delivery type was not changed. Set it from the storefront session that owns the checkout \u2014 a client constructed with `salesChannelId` (vibe-coded) or `storeId` (public storefront) \u2014 or from the Brainerce dashboard.",
6376
+ 400
6165
6377
  );
6166
6378
  }
6167
6379
  /**
@@ -6169,6 +6381,9 @@ var _BrainerceClient = class _BrainerceClient {
6169
6381
  * This sets the delivery type to "pickup", records customer info, and prepares for payment.
6170
6382
  * Equivalent to setShippingAddress + selectShippingMethod for delivery orders.
6171
6383
  *
6384
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
6385
+ * route for this, so an admin-mode client throws instead of 404ing.
6386
+ *
6172
6387
  * @example
6173
6388
  * ```typescript
6174
6389
  * const checkout = await client.selectPickupLocation('checkout_123', {
@@ -6195,10 +6410,9 @@ var _BrainerceClient = class _BrainerceClient {
6195
6410
  data
6196
6411
  );
6197
6412
  }
6198
- return this.adminRequest(
6199
- "PATCH",
6200
- `/api/v1/checkout/${encodePathSegment(checkoutId)}/pickup-location`,
6201
- data
6413
+ throw new BrainerceError(
6414
+ "selectPickupLocation is not a route on the API-key /v1 surface. There is no checkout/:checkoutId/pickup-location endpoint to call, so no pickup location was selected. Select it from the storefront session that owns the checkout \u2014 a client constructed with `salesChannelId` (vibe-coded) or `storeId` (public storefront) \u2014 or from the Brainerce dashboard.",
6415
+ 400
6202
6416
  );
6203
6417
  }
6204
6418
  /**
@@ -7523,13 +7737,17 @@ var _BrainerceClient = class _BrainerceClient {
7523
7737
  * lifetime earned, the program's display config, earned milestone `badges`,
7524
7738
  * and the `paidMembership` subscription state (null for free members).
7525
7739
  * Requires customerToken. Only available in storefront mode. `program` is
7526
- * null when the store has no loyalty program.
7740
+ * null when the store has no loyalty program. `pointsBalance` excludes points
7741
+ * still inside the return window - those are in `pendingPoints`, and a panel
7742
+ * that ignores them shows a shopper 0 the day they order.
7527
7743
  *
7528
7744
  * @example
7529
7745
  * ```typescript
7530
7746
  * client.setCustomerToken(auth.token);
7531
7747
  * const status = await client.getLoyaltyStatus();
7532
7748
  * if (status.enrolled) console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
7749
+ * // Points from an order just placed are in pendingPoints, NOT pointsBalance.
7750
+ * if (status.pendingPoints > 0) console.log(`+${status.pendingPoints} on ${status.pendingPointsConfirmAt}`);
7533
7751
  * status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
7534
7752
  * if (status.paidMembership?.status === 'ACTIVE') showPremiumPerks(status.paidMembership.plan);
7535
7753
  * ```
@@ -8376,11 +8594,28 @@ var _BrainerceClient = class _BrainerceClient {
8376
8594
  return this.adminRequest("POST", "/api/v1/tags", data);
8377
8595
  }
8378
8596
  /**
8379
- * Update an existing tag
8380
- * Requires Admin mode (apiKey)
8597
+ * Update an existing tag.
8598
+ *
8599
+ * **Not callable.** The API-key `/v1` surface serves `GET`, `POST` and
8600
+ * `DELETE` on tags (`external-api.controller.ts:3276`, `:3310`, `:3331`) but
8601
+ * no update verb — `PATCH /api/v1/tags/:id` 404'd silently. Tag editing lives
8602
+ * only on the dashboard surface (`PATCH /api/stores/:storeId/tags/:id`,
8603
+ * `tags.controller.ts:127`), which needs a `storeId` in the path that this
8604
+ * signature does not carry, and resolves the acting user from a dashboard
8605
+ * session an API key does not have.
8606
+ *
8607
+ * Edit the tag in the Brainerce dashboard. {@link deleteTag} +
8608
+ * {@link createTag} is NOT an equivalent workaround: it drops the tag's
8609
+ * product assignments, and `CreateTagDto` has no `translations` field, so any
8610
+ * per-locale names are lost too.
8381
8611
  */
8382
8612
  async updateTag(tagId, data) {
8383
- return this.adminRequest("PATCH", `/api/v1/tags/${encodePathSegment(tagId)}`, data);
8613
+ void tagId;
8614
+ void data;
8615
+ throw new BrainerceError(
8616
+ "updateTag is not a route on the API-key /v1 surface. Tags are served there for read, create and delete only, so the tag was not changed; edit it in the Brainerce dashboard. Deleting and recreating the tag is not equivalent \u2014 it drops the product assignments and the translations.",
8617
+ 400
8618
+ );
8384
8619
  }
8385
8620
  /**
8386
8621
  * Delete a tag
@@ -8478,24 +8713,41 @@ var _BrainerceClient = class _BrainerceClient {
8478
8713
  );
8479
8714
  }
8480
8715
  /**
8481
- * Update an attribute option
8482
- * Requires Admin mode (apiKey)
8716
+ * Update an attribute option.
8717
+ *
8718
+ * **Not callable.** The API-key `/v1` surface serves attribute options for
8719
+ * list and create only (`external-api.controller.ts:3517`, `:3543`); there is
8720
+ * no per-option route, so this 404'd silently. Editing an option lives on the
8721
+ * dashboard surface (`PUT /api/stores/:storeId/attributes/:id/options/:optionId`,
8722
+ * `attributes.controller.ts:132` — note it is `PUT` there, not `PATCH`), which
8723
+ * needs a path `storeId` this signature does not carry.
8724
+ *
8725
+ * Edit the option in the Brainerce dashboard.
8483
8726
  */
8484
8727
  async updateAttributeOption(attributeId, optionId, data) {
8485
- return this.adminRequest(
8486
- "PATCH",
8487
- `/api/v1/attributes/${encodePathSegment(attributeId)}/options/${encodePathSegment(optionId)}`,
8488
- data
8728
+ void attributeId;
8729
+ void optionId;
8730
+ void data;
8731
+ throw new BrainerceError(
8732
+ "updateAttributeOption is not a route on the API-key /v1 surface. Attribute options are served there for list and create only, so the option was not changed; edit it in the Brainerce dashboard.",
8733
+ 400
8489
8734
  );
8490
8735
  }
8491
8736
  /**
8492
- * Delete an attribute option
8493
- * Requires Admin mode (apiKey)
8737
+ * Delete an attribute option.
8738
+ *
8739
+ * **Not callable.** Same gap as {@link updateAttributeOption}: the `/v1`
8740
+ * surface has no per-option route. Deleting an option lives on the dashboard
8741
+ * surface (`DELETE /api/stores/:storeId/attributes/:id/options/:optionId`,
8742
+ * `attributes.controller.ts:145`), which needs a path `storeId` this signature
8743
+ * does not carry.
8494
8744
  */
8495
8745
  async deleteAttributeOption(attributeId, optionId) {
8496
- await this.adminRequest(
8497
- "DELETE",
8498
- `/api/v1/attributes/${encodePathSegment(attributeId)}/options/${encodePathSegment(optionId)}`
8746
+ void attributeId;
8747
+ void optionId;
8748
+ throw new BrainerceError(
8749
+ "deleteAttributeOption is not a route on the API-key /v1 surface. Attribute options are served there for list and create only, so nothing was deleted; delete the option in the Brainerce dashboard.",
8750
+ 400
8499
8751
  );
8500
8752
  }
8501
8753
  // -------------------- Modifier Groups (Admin) --------------------
@@ -9101,10 +9353,7 @@ var _BrainerceClient = class _BrainerceClient {
9101
9353
  * ```
9102
9354
  */
9103
9355
  async applyTaxPreset(presetKey) {
9104
- return this.adminRequest(
9105
- "POST",
9106
- `/api/v1/tax/presets/${encodePathSegment(presetKey)}/apply`
9107
- );
9356
+ return this.adminRequest("POST", `/api/v1/tax/presets/${encodePathSegment(presetKey)}/apply`);
9108
9357
  }
9109
9358
  /**
9110
9359
  * Get a single tax rate by ID
@@ -9321,25 +9570,48 @@ var _BrainerceClient = class _BrainerceClient {
9321
9570
  // Products/Coupons. When no publishes exist for an entity, it remains
9322
9571
  // visible to all vibe-coded sites of the store (legacy default).
9323
9572
  /**
9324
- * Publish a metafield definition to a vibe-coded site (admin mode).
9325
- * @example
9326
- * ```typescript
9327
- * await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
9328
- * ```
9573
+ * Publish a metafield definition to a sales channel (admin mode).
9574
+ *
9575
+ * **Not callable.** This asked for `publish-vibe-coded`, which is a
9576
+ * deprecated backend alias, not the canonical route. The canonical spelling
9577
+ * is `publish-sales-channel`, and the `/v1` surface serves it for products
9578
+ * (`external-api.controller.ts:862`), coupons (`:947`), customers (`:1548`),
9579
+ * categories (`:3039`), brands (`:3211`) and tags (`:3349`) — but NOT for
9580
+ * metafield definitions. Both spellings 404 there.
9581
+ *
9582
+ * The operation exists only on the dashboard surface
9583
+ * (`POST /api/stores/:storeId/metafield-definitions/:id/publish-sales-channel`,
9584
+ * `metafields.controller.ts:190`), which needs a path `storeId` this signature
9585
+ * does not carry.
9586
+ *
9587
+ * Publish the definition to a sales channel from the Brainerce dashboard. A
9588
+ * definition with no publishes stays visible to every sales channel of the
9589
+ * store, so leaving it unpublished is the permissive default, not a lockout.
9329
9590
  */
9330
9591
  async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
9331
- return this.adminRequest(
9332
- "POST",
9333
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/publish-vibe-coded`,
9334
- { vibeCodedConnectionId }
9592
+ void definitionId;
9593
+ void vibeCodedConnectionId;
9594
+ throw new BrainerceError(
9595
+ "publishMetafieldDefinitionToVibeCodedSite is not a route on the API-key /v1 surface. Per-sales-channel publishing is served there for products, coupons, customers, categories, brands and tags, but not for metafield definitions, so nothing was published; publish it in the Brainerce dashboard. A definition with no publishes remains visible to every sales channel of the store.",
9596
+ 400
9335
9597
  );
9336
9598
  }
9337
- /** Unpublish a metafield definition from a vibe-coded site (admin mode). */
9599
+ /**
9600
+ * Unpublish a metafield definition from a sales channel (admin mode).
9601
+ *
9602
+ * **Not callable.** Same gap as
9603
+ * {@link publishMetafieldDefinitionToVibeCodedSite} — the `/v1` surface
9604
+ * carries no per-sales-channel routes for metafield definitions under either
9605
+ * the canonical `unpublish-sales-channel` spelling or the deprecated
9606
+ * `unpublish-vibe-coded` alias. The dashboard route is
9607
+ * `metafields.controller.ts:217`.
9608
+ */
9338
9609
  async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
9339
- return this.adminRequest(
9340
- "POST",
9341
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/unpublish-vibe-coded`,
9342
- { vibeCodedConnectionId }
9610
+ void definitionId;
9611
+ void vibeCodedConnectionId;
9612
+ throw new BrainerceError(
9613
+ "unpublishMetafieldDefinitionFromVibeCodedSite is not a route on the API-key /v1 surface. Per-sales-channel publishing is served there for products, coupons, customers, categories, brands and tags, but not for metafield definitions, so nothing was unpublished; unpublish it in the Brainerce dashboard.",
9614
+ 400
9343
9615
  );
9344
9616
  }
9345
9617
  /**
@@ -9485,10 +9757,11 @@ var _BrainerceClient = class _BrainerceClient {
9485
9757
  * ```
9486
9758
  */
9487
9759
  async setDefinitionProducts(definitionId, data) {
9488
- return this.adminRequest(
9489
- "PATCH",
9490
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/products`,
9491
- data
9760
+ void definitionId;
9761
+ void data;
9762
+ throw new BrainerceError(
9763
+ "setDefinitionProducts is not a route on the API-key /v1 surface. There is no metafield-definitions/:id/products endpoint to call, so the definition's product list was not changed; set it in the Brainerce dashboard.",
9764
+ 400
9492
9765
  );
9493
9766
  }
9494
9767
  // -------------------- Metafields: Product Values --------------------
@@ -9534,24 +9807,41 @@ var _BrainerceClient = class _BrainerceClient {
9534
9807
  // -------------------- Product Customization Fields (Admin) --------------------
9535
9808
  /**
9536
9809
  * Get customization fields assigned to a product.
9537
- * Requires Admin mode (apiKey).
9810
+ *
9811
+ * **Not callable.** The API-key `/v1` surface has no
9812
+ * `metafield-definitions/products/:productId/customization-fields` route, so
9813
+ * this 404'd silently. It exists only on the dashboard surface
9814
+ * (`GET /api/stores/:storeId/metafield-definitions/products/:productId/customization-fields`,
9815
+ * `metafields.controller.ts:277`), which needs a path `storeId` this signature
9816
+ * does not carry.
9817
+ *
9818
+ * {@link getProductMetafields} is the closest working call — it returns the
9819
+ * metafield VALUES stored on a product over
9820
+ * `GET /api/v1/products/:productId/metafields`, not the customer-input field
9821
+ * definitions attached to it.
9538
9822
  */
9539
9823
  async getProductCustomizationFields(productId) {
9540
- return this.adminRequest(
9541
- "GET",
9542
- `/api/v1/metafield-definitions/products/${encodePathSegment(productId)}/customization-fields`
9824
+ void productId;
9825
+ throw new BrainerceError(
9826
+ "getProductCustomizationFields is not a route on the API-key /v1 surface. There is no metafield-definitions/products/:productId/customization-fields endpoint to call; read the assignments in the Brainerce dashboard. getProductMetafields returns the product's stored metafield values, which is a different thing.",
9827
+ 400
9543
9828
  );
9544
9829
  }
9545
9830
  /**
9546
9831
  * Set customization fields for a product (replaces all existing assignments).
9547
- * Only definitions marked as `isCustomerInput: true` can be assigned.
9548
- * Requires Admin mode (apiKey).
9832
+ *
9833
+ * **Not callable.** Same gap as {@link getProductCustomizationFields}: the
9834
+ * `/v1` surface carries no customization-field routes. The dashboard route is
9835
+ * `PATCH /api/stores/:storeId/metafield-definitions/products/:productId/customization-fields`
9836
+ * (`metafields.controller.ts:298`), which needs a path `storeId` this
9837
+ * signature does not carry.
9549
9838
  */
9550
9839
  async setProductCustomizationFields(productId, definitionIds) {
9551
- return this.adminRequest(
9552
- "PATCH",
9553
- `/api/v1/metafield-definitions/products/${encodePathSegment(productId)}/customization-fields`,
9554
- { definitionIds }
9840
+ void productId;
9841
+ void definitionIds;
9842
+ throw new BrainerceError(
9843
+ "setProductCustomizationFields is not a route on the API-key /v1 surface. There is no metafield-definitions/products/:productId/customization-fields endpoint to call, so the assignments were not changed; set them in the Brainerce dashboard.",
9844
+ 400
9555
9845
  );
9556
9846
  }
9557
9847
  /**
@@ -9734,6 +10024,27 @@ var _BrainerceClient = class _BrainerceClient {
9734
10024
  }
9735
10025
  // -------------------- Store Team Management (Admin) --------------------
9736
10026
  // Store-level team management. Each store has its own team with roles and permissions.
10027
+ /**
10028
+ * Every store-level team operation is dashboard-only.
10029
+ *
10030
+ * `store-team.controller.ts:53` carries `DashboardOnlyGuard`, which rejects
10031
+ * `api_key` and `app_installation` principals outright, so no SDK caller can
10032
+ * reach these however the URL is spelled. They additionally pointed at
10033
+ * `/api/v1/stores/:storeId/team*`, and `@Controller('v1')`
10034
+ * (external-api.controller.ts:181) has no `stores` root — so what they
10035
+ * actually returned was a 404, not the 403 you would expect from the guard.
10036
+ *
10037
+ * Throwing beats either status code: a 404 reads as "wrong id" and a 403 as
10038
+ * "missing permission", and both send the caller looking for a fix that does
10039
+ * not exist. Use the account-level `getTeamMembers()` family, or the
10040
+ * dashboard.
10041
+ */
10042
+ dashboardOnlyTeamOperation(operation) {
10043
+ throw new BrainerceError(
10044
+ `${operation} is a dashboard-only operation. Store-level team management sits behind DashboardOnlyGuard, which rejects API-key principals, so there is no SDK path to it in any mode. Use the account-level team methods (getTeamMembers, inviteTeamMember, ...) or the Brainerce dashboard.`,
10045
+ 403
10046
+ );
10047
+ }
9737
10048
  /**
9738
10049
  * Get the team for a specific store (members + pending invitations)
9739
10050
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
@@ -9743,11 +10054,8 @@ var _BrainerceClient = class _BrainerceClient {
9743
10054
  * const { members, invitations } = await client.getStoreTeam('store_id');
9744
10055
  * ```
9745
10056
  */
9746
- async getStoreTeam(storeId) {
9747
- return this.adminRequest(
9748
- "GET",
9749
- `/api/v1/stores/${encodePathSegment(storeId)}/team`
9750
- );
10057
+ async getStoreTeam(_storeId) {
10058
+ return this.dashboardOnlyTeamOperation("getStoreTeam");
9751
10059
  }
9752
10060
  /**
9753
10061
  * Invite a new member to a store
@@ -9764,12 +10072,8 @@ var _BrainerceClient = class _BrainerceClient {
9764
10072
  * });
9765
10073
  * ```
9766
10074
  */
9767
- async inviteStoreMember(storeId, data) {
9768
- return this.adminRequest(
9769
- "POST",
9770
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invite`,
9771
- data
9772
- );
10075
+ async inviteStoreMember(_storeId, _data) {
10076
+ return this.dashboardOnlyTeamOperation("inviteStoreMember");
9773
10077
  }
9774
10078
  /**
9775
10079
  * Update a store team member's role and/or permissions
@@ -9786,12 +10090,8 @@ var _BrainerceClient = class _BrainerceClient {
9786
10090
  * });
9787
10091
  * ```
9788
10092
  */
9789
- async updateStoreMember(storeId, memberId, data) {
9790
- return this.adminRequest(
9791
- "PATCH",
9792
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`,
9793
- data
9794
- );
10093
+ async updateStoreMember(_storeId, _memberId, _data) {
10094
+ return this.dashboardOnlyTeamOperation("updateStoreMember");
9795
10095
  }
9796
10096
  /**
9797
10097
  * Replace the set of vibe-coded sales channels a store member is restricted to.
@@ -9812,42 +10112,29 @@ var _BrainerceClient = class _BrainerceClient {
9812
10112
  * });
9813
10113
  * ```
9814
10114
  */
9815
- async updateStoreMemberSalesChannels(storeId, memberId, data) {
9816
- return this.adminRequest(
9817
- "PATCH",
9818
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}/sales-channels`,
9819
- data
9820
- );
10115
+ async updateStoreMemberSalesChannels(_storeId, _memberId, _data) {
10116
+ return this.dashboardOnlyTeamOperation("updateStoreMemberSalesChannels");
9821
10117
  }
9822
10118
  /**
9823
10119
  * Remove a member from a store team
9824
10120
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9825
10121
  */
9826
- async removeStoreMember(storeId, memberId) {
9827
- await this.adminRequest(
9828
- "DELETE",
9829
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`
9830
- );
10122
+ async removeStoreMember(_storeId, _memberId) {
10123
+ return this.dashboardOnlyTeamOperation("removeStoreMember");
9831
10124
  }
9832
10125
  /**
9833
10126
  * Resend a store invitation email
9834
10127
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9835
10128
  */
9836
- async resendStoreInvitation(storeId, invitationId) {
9837
- return this.adminRequest(
9838
- "POST",
9839
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}/resend`
9840
- );
10129
+ async resendStoreInvitation(_storeId, _invitationId) {
10130
+ return this.dashboardOnlyTeamOperation("resendStoreInvitation");
9841
10131
  }
9842
10132
  /**
9843
10133
  * Revoke a store invitation
9844
10134
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9845
10135
  */
9846
- async revokeStoreInvitation(storeId, invitationId) {
9847
- await this.adminRequest(
9848
- "DELETE",
9849
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}`
9850
- );
10136
+ async revokeStoreInvitation(_storeId, _invitationId) {
10137
+ return this.dashboardOnlyTeamOperation("revokeStoreInvitation");
9851
10138
  }
9852
10139
  /**
9853
10140
  * Get public invitation details by token (no auth required)
@@ -9856,7 +10143,7 @@ var _BrainerceClient = class _BrainerceClient {
9856
10143
  async getStoreInvitationByToken(token) {
9857
10144
  return this.request(
9858
10145
  "GET",
9859
- `/api/v1/store-invitations/${encodePathSegment(token)}`
10146
+ `/api/store-invitations/${encodePathSegment(token)}`
9860
10147
  );
9861
10148
  }
9862
10149
  /**
@@ -9864,9 +10151,9 @@ var _BrainerceClient = class _BrainerceClient {
9864
10151
  * Requires Admin mode (apiKey)
9865
10152
  */
9866
10153
  async acceptStoreInvitation(token) {
9867
- await this.adminRequest(
9868
- "POST",
9869
- `/api/v1/store-invitations/${encodePathSegment(token)}/accept`
10154
+ throw new BrainerceError(
10155
+ "acceptStoreInvitation requires a dashboard session belonging to the invited user: the invitation is matched against the email address of that user, which an API key does not have. Send the invitee to the invitation link instead.",
10156
+ 403
9870
10157
  );
9871
10158
  }
9872
10159
  /**
@@ -9883,7 +10170,7 @@ var _BrainerceClient = class _BrainerceClient {
9883
10170
  * ```
9884
10171
  */
9885
10172
  async getMyStores() {
9886
- return this.adminRequest("GET", "/api/v1/me/stores");
10173
+ return this.dashboardOnlyUserContext("getMyStores");
9887
10174
  }
9888
10175
  /**
9889
10176
  * Get the current user's resolved permissions for a specific store
@@ -9897,10 +10184,23 @@ var _BrainerceClient = class _BrainerceClient {
9897
10184
  * }
9898
10185
  * ```
9899
10186
  */
9900
- async getMyStorePermissions(storeId) {
9901
- return this.adminRequest(
9902
- "GET",
9903
- `/api/v1/me/stores/${encodePathSegment(storeId)}/permissions`
10187
+ async getMyStorePermissions(_storeId) {
10188
+ return this.dashboardOnlyUserContext("getMyStorePermissions");
10189
+ }
10190
+ /**
10191
+ * `/me/*` answers "who am I and what can I reach", which only a real user can
10192
+ * ask. `UserContextController` (store-team.controller.ts:246) is guarded by
10193
+ * `DashboardOnlyGuard` for a load-bearing reason its own G16 comment spells
10194
+ * out: both routes resolve access purely from `@CurrentUserId()`, which is
10195
+ * `undefined` for an api_key principal, so the store filter would be stripped
10196
+ * and every store on the platform returned. The guard is the control. These
10197
+ * also pointed at `/api/v1/me/*`, which no controller serves, so the observed
10198
+ * failure was a 404 rather than the guard's 403.
10199
+ */
10200
+ dashboardOnlyUserContext(operation) {
10201
+ throw new BrainerceError(
10202
+ `${operation} resolves the CURRENT USER, so it requires a dashboard session and is rejected for API-key principals by design. An API key is already scoped to one store: read it with getStore(), and read the permissions of a member from getStoreTeam() in the dashboard.`,
10203
+ 403
9904
10204
  );
9905
10205
  }
9906
10206
  // -------------------- Email Settings & Templates (Admin) --------------------
@@ -10043,7 +10343,7 @@ var _BrainerceClient = class _BrainerceClient {
10043
10343
  * Requires Admin mode (apiKey)
10044
10344
  */
10045
10345
  async getSyncConflicts() {
10046
- return this.adminRequest("GET", "/api/v1/sync-conflicts");
10346
+ return this.syncConflictsNotImplemented("getSyncConflicts");
10047
10347
  }
10048
10348
  /**
10049
10349
  * Resolve a sync conflict
@@ -10053,12 +10353,28 @@ var _BrainerceClient = class _BrainerceClient {
10053
10353
  * @param resolution - 'MERGE' to link to existing product, 'CREATE_NEW' to create new product
10054
10354
  */
10055
10355
  async resolveSyncConflict(conflictId, resolution) {
10056
- return this.adminRequest(
10057
- "POST",
10058
- `/api/v1/sync-conflicts/${encodePathSegment(conflictId)}/resolve`,
10059
- {
10060
- resolution
10061
- }
10356
+ void conflictId;
10357
+ void resolution;
10358
+ return this.syncConflictsNotImplemented("resolveSyncConflict");
10359
+ }
10360
+ /**
10361
+ * Sync conflicts were never implemented on the server.
10362
+ *
10363
+ * There is no `sync-conflict` route anywhere in the backend — not under
10364
+ * `@Controller('v1')`, not on any other controller. These two methods have
10365
+ * called a URL that has never existed, and `SyncConflict` /
10366
+ * `SyncConflictResolution` / `ResolveSyncConflictDto` are exported types with
10367
+ * no producer. The METAFIELD conflict siblings below are real
10368
+ * (external-api.controller.ts:4788, :4802) and are easy to mistake for these.
10369
+ *
10370
+ * Kept as throwing stubs rather than deleted: removing exported methods from a
10371
+ * published package is a breaking change, and a caller who has been swallowing
10372
+ * a 404 deserves to be told why.
10373
+ */
10374
+ syncConflictsNotImplemented(operation) {
10375
+ throw new BrainerceError(
10376
+ `${operation} is not implemented: the platform exposes no sync-conflict endpoint. If you are looking for metafield sync conflicts, use getMetafieldConflicts() / resolveMetafieldConflict().`,
10377
+ 501
10062
10378
  );
10063
10379
  }
10064
10380
  // -------------------- Metafield Conflicts (Admin) --------------------
@@ -10920,6 +11236,7 @@ function buildProductJsonLd(product, opts) {
10920
11236
  const inv = product.inventory;
10921
11237
  const availability = !inv ? "https://schema.org/InStock" : inv.inStock ?? (inv.available ?? 0) > 0 ? "https://schema.org/InStock" : inv.canPurchase ? "https://schema.org/BackOrder" : "https://schema.org/OutOfStock";
10922
11238
  const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
11239
+ const omitOffer = product.type === "KIT";
10923
11240
  const itemCondition = "https://schema.org/NewCondition";
10924
11241
  const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
10925
11242
  "@type": "OfferShippingDetails",
@@ -10982,7 +11299,7 @@ function buildProductJsonLd(product, opts) {
10982
11299
  ...product.gtin ? { gtin: product.gtin } : {},
10983
11300
  ...product.mpn ? { mpn: product.mpn } : {},
10984
11301
  ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
10985
- offers: offer,
11302
+ ...omitOffer ? {} : { offers: offer },
10986
11303
  // Google policy: never emit an empty/zero rating block. bestRating /
10987
11304
  // worstRating make the 1-5 scale explicit so aggregators can't misread
10988
11305
  // a 4.8 on an assumed 0-10 scale.