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.js CHANGED
@@ -204,7 +204,7 @@ function isDevGuardsEnabled() {
204
204
  }
205
205
 
206
206
  // src/version.ts
207
- var SDK_VERSION = "2.0.3";
207
+ var SDK_VERSION = "2.3.0";
208
208
 
209
209
  // src/client.ts
210
210
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -450,7 +450,7 @@ var _BrainerceClient = class _BrainerceClient {
450
450
  * Works in all three SDK modes (vibe-coded, storefront, admin):
451
451
  * - **Public reads** (`get`, `list`, `getBySlug`): work in any mode.
452
452
  * - **Write operations** (`create`, `update`, `publish`, `unpublish`,
453
- * `remove`): admin mode only — they call `/api/v1/content/...` with
453
+ * `remove`): admin mode only — they call `/api/content/...` with
454
454
  * the API key. Calling from storefront / vibe-coded mode throws.
455
455
  *
456
456
  * **Default key:** every type has `'main'` as its universal default key.
@@ -487,7 +487,7 @@ var _BrainerceClient = class _BrainerceClient {
487
487
  this.content = (() => {
488
488
  const DEFAULT_KEY = "main";
489
489
  const publicGetPath = (type, key) => `/content/${encodeURIComponent(type)}/${encodeURIComponent(key)}`;
490
- const adminBase = () => "/api/v1/content";
490
+ const adminBase = () => "/api/content";
491
491
  const publicGet = async (type, key, locale) => {
492
492
  const query = locale ? { locale } : void 0;
493
493
  const path = publicGetPath(type, key);
@@ -926,10 +926,19 @@ var _BrainerceClient = class _BrainerceClient {
926
926
  this.customerToken = null;
927
927
  }
928
928
  // -------------------- Private Methods --------------------
929
+ /**
930
+ * Turn an `idempotencyKey` option into the header the backend reads.
931
+ *
932
+ * Returns `undefined` when there is no key, so the header is absent rather
933
+ * than empty — the interceptor rejects a present-but-blank key with a 400.
934
+ */
935
+ idempotencyHeaders(options) {
936
+ return options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : void 0;
937
+ }
929
938
  /**
930
939
  * Make a request to the Admin API (requires apiKey)
931
940
  */
932
- async adminRequest(method, path, body, queryParams, responseType = "json") {
941
+ async adminRequest(method, path, body, queryParams, responseType = "json", extraHeaders) {
933
942
  if (!this.apiKey) {
934
943
  throw new BrainerceError(
935
944
  "This operation requires an API key. Initialize with apiKey instead of storeId.",
@@ -959,6 +968,11 @@ var _BrainerceClient = class _BrainerceClient {
959
968
  if (this.locale) {
960
969
  headers["Accept-Language"] = this.locale;
961
970
  }
971
+ if (extraHeaders) {
972
+ for (const [key, value] of Object.entries(extraHeaders)) {
973
+ if (value) headers[key] = value;
974
+ }
975
+ }
962
976
  for (let attempt = 0; attempt < 2; attempt++) {
963
977
  const controller = new AbortController();
964
978
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -2107,11 +2121,9 @@ var _BrainerceClient = class _BrainerceClient {
2107
2121
  queryParams
2108
2122
  );
2109
2123
  }
2110
- return this.adminRequest(
2111
- "GET",
2112
- "/api/v1/search/suggestions",
2113
- void 0,
2114
- queryParams
2124
+ throw new BrainerceError(
2125
+ "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.",
2126
+ 400
2115
2127
  );
2116
2128
  }
2117
2129
  /**
@@ -2285,21 +2297,25 @@ var _BrainerceClient = class _BrainerceClient {
2285
2297
  );
2286
2298
  }
2287
2299
  /**
2288
- * Publish a product to specific platforms
2300
+ * Publish a product to specific platforms.
2289
2301
  *
2290
- * @example
2291
- * ```typescript
2292
- * const result = await client.publishProduct('prod_123', ['SHOPIFY', 'WOOCOMMERCE']);
2293
- * console.log('Publish results:', result.results);
2294
- * ```
2302
+ * **Not callable.** The API-key `/v1` surface has no `products/:id/publish`
2303
+ * route. Platform publishing for products exists only on the dashboard
2304
+ * surface (`POST /api/products/:id/publish?storeId=`,
2305
+ * `products.controller.ts:556`), which resolves the acting user from a
2306
+ * dashboard session an API key does not carry. The generic trigger
2307
+ * `POST /v1/sync` answers `501 Not Implemented` and points at per-resource
2308
+ * publish endpoints — and products have none.
2309
+ *
2310
+ * {@link publishProductToSalesChannel} is a DIFFERENT operation: it controls
2311
+ * visibility on a vibe-coded storefront, not a push to an external platform.
2295
2312
  */
2296
2313
  async publishProduct(productId, platforms) {
2297
- return this.request(
2298
- "POST",
2299
- `/api/v1/products/${encodePathSegment(productId)}/publish`,
2300
- {
2301
- platforms
2302
- }
2314
+ void productId;
2315
+ void platforms;
2316
+ throw new BrainerceError(
2317
+ "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.",
2318
+ 400
2303
2319
  );
2304
2320
  }
2305
2321
  // -------------------- Variants --------------------
@@ -4294,6 +4310,9 @@ var _BrainerceClient = class _BrainerceClient {
4294
4310
  * flags reflect the live state. Call this on cart load if you want to
4295
4311
  * surface drift to the customer before they reach checkout.
4296
4312
  *
4313
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
4314
+ * route for this, so an admin-mode client throws instead of 404ing.
4315
+ *
4297
4316
  * @example
4298
4317
  * ```typescript
4299
4318
  * const cart = await client.recalculateCart('cart_123');
@@ -4318,9 +4337,9 @@ var _BrainerceClient = class _BrainerceClient {
4318
4337
  "cart"
4319
4338
  );
4320
4339
  }
4321
- return this.withGuards(
4322
- this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/recalculate`),
4323
- "cart"
4340
+ throw new BrainerceError(
4341
+ "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.",
4342
+ 400
4324
4343
  );
4325
4344
  }
4326
4345
  /**
@@ -4329,6 +4348,9 @@ var _BrainerceClient = class _BrainerceClient {
4329
4348
  * subsequent `createCheckout` call will then succeed (it would otherwise
4330
4349
  * throw `PRICE_DRIFT`).
4331
4350
  *
4351
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
4352
+ * route for this, so an admin-mode client throws instead of 404ing.
4353
+ *
4332
4354
  * @example
4333
4355
  * ```typescript
4334
4356
  * try {
@@ -4363,12 +4385,9 @@ var _BrainerceClient = class _BrainerceClient {
4363
4385
  "cart"
4364
4386
  );
4365
4387
  }
4366
- return this.withGuards(
4367
- this.adminRequest(
4368
- "POST",
4369
- `/api/v1/cart/${encodePathSegment(cartId)}/refresh-snapshots`
4370
- ),
4371
- "cart"
4388
+ throw new BrainerceError(
4389
+ "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.",
4390
+ 400
4372
4391
  );
4373
4392
  }
4374
4393
  /**
@@ -5812,6 +5831,202 @@ var _BrainerceClient = class _BrainerceClient {
5812
5831
  return this.adminRequest("POST", "/api/v1/gift-cards/balance", { code });
5813
5832
  }
5814
5833
  // ==========================================================================
5834
+ // Gift cards — administration (API key only)
5835
+ //
5836
+ // Full programmatic management, unlike Shopify, which requires you to ask
5837
+ // their support for the equivalent scope. The gate here is the SCOPE the
5838
+ // merchant granted your key, so the separation matters: `gift_cards:issue`
5839
+ // mints stored value and `gift_cards:adjust` rewrites a balance, and neither
5840
+ // is implied by `gift_cards:read`. Ask for the least your integration needs.
5841
+ //
5842
+ // Every call reaches the same service the dashboard uses, so the rules hold
5843
+ // identically: a note is mandatory on anything that moves value, a debit
5844
+ // cannot cross what live checkout holds have reserved, and there is no delete
5845
+ // anywhere — the ledger is append-only.
5846
+ // ==========================================================================
5847
+ /**
5848
+ * List gift cards.
5849
+ *
5850
+ * `search` matches the LAST FOUR of a code or part of a recipient email. It
5851
+ * cannot match a full code: only an HMAC is stored, so there is nothing to
5852
+ * search against.
5853
+ *
5854
+ * Requires `gift_cards:read`.
5855
+ */
5856
+ async listGiftCards(params) {
5857
+ const q = new URLSearchParams();
5858
+ if (params?.page) q.set("page", String(params.page));
5859
+ if (params?.limit) q.set("limit", String(params.limit));
5860
+ if (params?.filter) q.set("filter", params.filter);
5861
+ if (params?.search) q.set("search", params.search);
5862
+ const qs = q.toString();
5863
+ return this.adminRequest(
5864
+ "GET",
5865
+ `/api/v1/gift-cards${qs ? `?${qs}` : ""}`
5866
+ );
5867
+ }
5868
+ /**
5869
+ * Outstanding gift card liability, per currency.
5870
+ *
5871
+ * This is the month-end number. Read `byCurrency` if the store sells in more
5872
+ * than one — currencies are never summed together.
5873
+ *
5874
+ * Requires `gift_cards:read`.
5875
+ */
5876
+ async getGiftCardLiability() {
5877
+ return this.adminRequest("GET", "/api/v1/gift-cards/liability");
5878
+ }
5879
+ /**
5880
+ * One gift card with its full ledger.
5881
+ *
5882
+ * Requires `gift_cards:read`.
5883
+ */
5884
+ async getGiftCard(giftCardId) {
5885
+ return this.adminRequest(
5886
+ "GET",
5887
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}`
5888
+ );
5889
+ }
5890
+ /**
5891
+ * Issue a gift card.
5892
+ *
5893
+ * ⚠️ **The code comes back exactly once.** It is stored only as an HMAC, so
5894
+ * this response is the only time it exists in readable form anywhere. Persist
5895
+ * it or deliver it before you discard the response — no later call, dashboard
5896
+ * screen or database query can recover it.
5897
+ *
5898
+ * A `note` is required. Refused when gift cards are switched off for the
5899
+ * store.
5900
+ *
5901
+ * ⚠️ **Pass an `idempotencyKey`.** It is the ONLY recovery that exists here:
5902
+ * re-sending the identical request with the same key inside 24 hours replays
5903
+ * the cached response, code included. Without one, a retried timeout mints a
5904
+ * SECOND card and a second real liability. The key is sent as the
5905
+ * `Idempotency-Key` header; reuse the same value across retries of the same
5906
+ * logical issue, never a fresh random one.
5907
+ *
5908
+ * Requires `gift_cards:issue`.
5909
+ *
5910
+ * @example
5911
+ * ```typescript
5912
+ * const card = await client.issueGiftCard(
5913
+ * {
5914
+ * amount: '200.00',
5915
+ * note: 'Compensation for order #1042',
5916
+ * recipientEmail: 'dana@example.com',
5917
+ * },
5918
+ * { idempotencyKey: 'compensation-order-1042' }
5919
+ * );
5920
+ * await sendToCustomer(card.plaintextCode); // your only chance
5921
+ * ```
5922
+ */
5923
+ async issueGiftCard(data, options) {
5924
+ return this.adminRequest(
5925
+ "POST",
5926
+ "/api/v1/gift-cards",
5927
+ data,
5928
+ void 0,
5929
+ "json",
5930
+ this.idempotencyHeaders(options)
5931
+ );
5932
+ }
5933
+ /**
5934
+ * Re-issue a gift card onto a new code.
5935
+ *
5936
+ * The answer to a customer losing their code. Mints a new code, moves the
5937
+ * WHOLE balance to it, and REVOKES the old card.
5938
+ *
5939
+ * **This is not a resend.** The old code stops working the moment this
5940
+ * returns — if the customer still holds a printed card, it dies. Refused
5941
+ * while a checkout holds value on the card. The original expiry carries
5942
+ * forward, so this cannot be used to restart an expiry clock.
5943
+ *
5944
+ * The new code is returned exactly once, under the same rules as issuance —
5945
+ * so pass an `idempotencyKey` here for the same reason, and with the same
5946
+ * force: a retried timeout without one revokes the replacement it just made
5947
+ * and mints another.
5948
+ *
5949
+ * Requires `gift_cards:issue`.
5950
+ */
5951
+ async reissueGiftCard(giftCardId, note, options) {
5952
+ return this.adminRequest(
5953
+ "POST",
5954
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/reissue`,
5955
+ { note },
5956
+ void 0,
5957
+ "json",
5958
+ this.idempotencyHeaders(options)
5959
+ );
5960
+ }
5961
+ /**
5962
+ * Adjust a gift card balance.
5963
+ *
5964
+ * `delta` is a SIGNED decimal string: `"25.00"` adds, `"-25.00"` takes away.
5965
+ * The `note` is required and is written to the ledger permanently — it is the
5966
+ * row a finance review reads a year from now.
5967
+ *
5968
+ * A debit cannot take the balance below what live checkout holds have already
5969
+ * reserved; that refusal names the held amount so you can act on it.
5970
+ *
5971
+ * Pass an `idempotencyKey`: an adjustment is a relative move, so a retried
5972
+ * timeout without one applies the delta twice.
5973
+ *
5974
+ * Requires `gift_cards:adjust`.
5975
+ */
5976
+ async adjustGiftCardBalance(giftCardId, delta, note, options) {
5977
+ return this.adminRequest(
5978
+ "PATCH",
5979
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/adjust`,
5980
+ { delta, note },
5981
+ void 0,
5982
+ "json",
5983
+ this.idempotencyHeaders(options)
5984
+ );
5985
+ }
5986
+ /**
5987
+ * Enable, disable or revoke one gift card.
5988
+ *
5989
+ * Deliberately does NOT touch live holds: a checkout that already reserved
5990
+ * value settles normally, because pulling it out from under a shopper
5991
+ * mid-payment would strand a provider charge already in flight. Disabling
5992
+ * stops NEW holds, which is what "off" actually means.
5993
+ *
5994
+ * Requires `gift_cards:write`.
5995
+ */
5996
+ async setGiftCardStatus(giftCardId, status, options) {
5997
+ return this.adminRequest(
5998
+ "PATCH",
5999
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/status`,
6000
+ { status },
6001
+ void 0,
6002
+ "json",
6003
+ this.idempotencyHeaders(options)
6004
+ );
6005
+ }
6006
+ /**
6007
+ * Disable or reactivate many gift cards at once.
6008
+ *
6009
+ * `REVOKED` is not accepted here — it belongs to re-issue, which moves the
6010
+ * balance to a replacement first. Revoking in bulk would strand balances with
6011
+ * nowhere to go. Cards already revoked are skipped, so the returned count is
6012
+ * the honest one and may be lower than the ids you sent.
6013
+ *
6014
+ * There is no bulk delete, here or anywhere: the ledger is append-only and a
6015
+ * card may carry a statutory retention life.
6016
+ *
6017
+ * Requires `gift_cards:write`.
6018
+ */
6019
+ async bulkSetGiftCardStatus(giftCardIds, status, options) {
6020
+ return this.adminRequest(
6021
+ "PATCH",
6022
+ "/api/v1/gift-cards/bulk/status",
6023
+ { giftCardIds, status },
6024
+ void 0,
6025
+ "json",
6026
+ this.idempotencyHeaders(options)
6027
+ );
6028
+ }
6029
+ // ==========================================================================
5815
6030
  // Donations
5816
6031
  // ==========================================================================
5817
6032
  /**
@@ -5954,10 +6169,7 @@ var _BrainerceClient = class _BrainerceClient {
5954
6169
  "/checkouts/shipping-destinations"
5955
6170
  );
5956
6171
  }
5957
- return this.adminRequest(
5958
- "GET",
5959
- "/api/v1/checkouts/shipping-destinations"
5960
- );
6172
+ return this.adminRequest("GET", "/api/checkouts/shipping-destinations");
5961
6173
  }
5962
6174
  /**
5963
6175
  * Set shipping address on checkout (includes customer email).
@@ -6216,11 +6428,14 @@ var _BrainerceClient = class _BrainerceClient {
6216
6428
  if (this.storeId && !this.apiKey) {
6217
6429
  return this.storefrontRequest("GET", "/pickup-locations");
6218
6430
  }
6219
- return this.adminRequest("GET", "/api/v1/checkouts/pickup-locations");
6431
+ return this.adminRequest("GET", "/api/checkouts/pickup-locations");
6220
6432
  }
6221
6433
  /**
6222
6434
  * Set delivery type on checkout (shipping or pickup).
6223
6435
  *
6436
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
6437
+ * route for this, so an admin-mode client throws instead of 404ing.
6438
+ *
6224
6439
  * @example
6225
6440
  * ```typescript
6226
6441
  * const checkout = await client.setDeliveryType('checkout_123', 'pickup');
@@ -6245,12 +6460,9 @@ var _BrainerceClient = class _BrainerceClient {
6245
6460
  }
6246
6461
  );
6247
6462
  }
6248
- return this.adminRequest(
6249
- "PATCH",
6250
- `/api/v1/checkout/${encodePathSegment(checkoutId)}/delivery-type`,
6251
- {
6252
- deliveryType
6253
- }
6463
+ throw new BrainerceError(
6464
+ "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.",
6465
+ 400
6254
6466
  );
6255
6467
  }
6256
6468
  /**
@@ -6258,6 +6470,9 @@ var _BrainerceClient = class _BrainerceClient {
6258
6470
  * This sets the delivery type to "pickup", records customer info, and prepares for payment.
6259
6471
  * Equivalent to setShippingAddress + selectShippingMethod for delivery orders.
6260
6472
  *
6473
+ * **Storefront and vibe-coded modes only.** There is no admin (`apiKey`)
6474
+ * route for this, so an admin-mode client throws instead of 404ing.
6475
+ *
6261
6476
  * @example
6262
6477
  * ```typescript
6263
6478
  * const checkout = await client.selectPickupLocation('checkout_123', {
@@ -6284,10 +6499,9 @@ var _BrainerceClient = class _BrainerceClient {
6284
6499
  data
6285
6500
  );
6286
6501
  }
6287
- return this.adminRequest(
6288
- "PATCH",
6289
- `/api/v1/checkout/${encodePathSegment(checkoutId)}/pickup-location`,
6290
- data
6502
+ throw new BrainerceError(
6503
+ "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.",
6504
+ 400
6291
6505
  );
6292
6506
  }
6293
6507
  /**
@@ -7612,13 +7826,17 @@ var _BrainerceClient = class _BrainerceClient {
7612
7826
  * lifetime earned, the program's display config, earned milestone `badges`,
7613
7827
  * and the `paidMembership` subscription state (null for free members).
7614
7828
  * Requires customerToken. Only available in storefront mode. `program` is
7615
- * null when the store has no loyalty program.
7829
+ * null when the store has no loyalty program. `pointsBalance` excludes points
7830
+ * still inside the return window - those are in `pendingPoints`, and a panel
7831
+ * that ignores them shows a shopper 0 the day they order.
7616
7832
  *
7617
7833
  * @example
7618
7834
  * ```typescript
7619
7835
  * client.setCustomerToken(auth.token);
7620
7836
  * const status = await client.getLoyaltyStatus();
7621
7837
  * if (status.enrolled) console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
7838
+ * // Points from an order just placed are in pendingPoints, NOT pointsBalance.
7839
+ * if (status.pendingPoints > 0) console.log(`+${status.pendingPoints} on ${status.pendingPointsConfirmAt}`);
7622
7840
  * status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
7623
7841
  * if (status.paidMembership?.status === 'ACTIVE') showPremiumPerks(status.paidMembership.plan);
7624
7842
  * ```
@@ -8465,11 +8683,28 @@ var _BrainerceClient = class _BrainerceClient {
8465
8683
  return this.adminRequest("POST", "/api/v1/tags", data);
8466
8684
  }
8467
8685
  /**
8468
- * Update an existing tag
8469
- * Requires Admin mode (apiKey)
8686
+ * Update an existing tag.
8687
+ *
8688
+ * **Not callable.** The API-key `/v1` surface serves `GET`, `POST` and
8689
+ * `DELETE` on tags (`external-api.controller.ts:3276`, `:3310`, `:3331`) but
8690
+ * no update verb — `PATCH /api/v1/tags/:id` 404'd silently. Tag editing lives
8691
+ * only on the dashboard surface (`PATCH /api/stores/:storeId/tags/:id`,
8692
+ * `tags.controller.ts:127`), which needs a `storeId` in the path that this
8693
+ * signature does not carry, and resolves the acting user from a dashboard
8694
+ * session an API key does not have.
8695
+ *
8696
+ * Edit the tag in the Brainerce dashboard. {@link deleteTag} +
8697
+ * {@link createTag} is NOT an equivalent workaround: it drops the tag's
8698
+ * product assignments, and `CreateTagDto` has no `translations` field, so any
8699
+ * per-locale names are lost too.
8470
8700
  */
8471
8701
  async updateTag(tagId, data) {
8472
- return this.adminRequest("PATCH", `/api/v1/tags/${encodePathSegment(tagId)}`, data);
8702
+ void tagId;
8703
+ void data;
8704
+ throw new BrainerceError(
8705
+ "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.",
8706
+ 400
8707
+ );
8473
8708
  }
8474
8709
  /**
8475
8710
  * Delete a tag
@@ -8567,24 +8802,41 @@ var _BrainerceClient = class _BrainerceClient {
8567
8802
  );
8568
8803
  }
8569
8804
  /**
8570
- * Update an attribute option
8571
- * Requires Admin mode (apiKey)
8805
+ * Update an attribute option.
8806
+ *
8807
+ * **Not callable.** The API-key `/v1` surface serves attribute options for
8808
+ * list and create only (`external-api.controller.ts:3517`, `:3543`); there is
8809
+ * no per-option route, so this 404'd silently. Editing an option lives on the
8810
+ * dashboard surface (`PUT /api/stores/:storeId/attributes/:id/options/:optionId`,
8811
+ * `attributes.controller.ts:132` — note it is `PUT` there, not `PATCH`), which
8812
+ * needs a path `storeId` this signature does not carry.
8813
+ *
8814
+ * Edit the option in the Brainerce dashboard.
8572
8815
  */
8573
8816
  async updateAttributeOption(attributeId, optionId, data) {
8574
- return this.adminRequest(
8575
- "PATCH",
8576
- `/api/v1/attributes/${encodePathSegment(attributeId)}/options/${encodePathSegment(optionId)}`,
8577
- data
8817
+ void attributeId;
8818
+ void optionId;
8819
+ void data;
8820
+ throw new BrainerceError(
8821
+ "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.",
8822
+ 400
8578
8823
  );
8579
8824
  }
8580
8825
  /**
8581
- * Delete an attribute option
8582
- * Requires Admin mode (apiKey)
8826
+ * Delete an attribute option.
8827
+ *
8828
+ * **Not callable.** Same gap as {@link updateAttributeOption}: the `/v1`
8829
+ * surface has no per-option route. Deleting an option lives on the dashboard
8830
+ * surface (`DELETE /api/stores/:storeId/attributes/:id/options/:optionId`,
8831
+ * `attributes.controller.ts:145`), which needs a path `storeId` this signature
8832
+ * does not carry.
8583
8833
  */
8584
8834
  async deleteAttributeOption(attributeId, optionId) {
8585
- await this.adminRequest(
8586
- "DELETE",
8587
- `/api/v1/attributes/${encodePathSegment(attributeId)}/options/${encodePathSegment(optionId)}`
8835
+ void attributeId;
8836
+ void optionId;
8837
+ throw new BrainerceError(
8838
+ "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.",
8839
+ 400
8588
8840
  );
8589
8841
  }
8590
8842
  // -------------------- Modifier Groups (Admin) --------------------
@@ -9190,10 +9442,7 @@ var _BrainerceClient = class _BrainerceClient {
9190
9442
  * ```
9191
9443
  */
9192
9444
  async applyTaxPreset(presetKey) {
9193
- return this.adminRequest(
9194
- "POST",
9195
- `/api/v1/tax/presets/${encodePathSegment(presetKey)}/apply`
9196
- );
9445
+ return this.adminRequest("POST", `/api/v1/tax/presets/${encodePathSegment(presetKey)}/apply`);
9197
9446
  }
9198
9447
  /**
9199
9448
  * Get a single tax rate by ID
@@ -9410,25 +9659,48 @@ var _BrainerceClient = class _BrainerceClient {
9410
9659
  // Products/Coupons. When no publishes exist for an entity, it remains
9411
9660
  // visible to all vibe-coded sites of the store (legacy default).
9412
9661
  /**
9413
- * Publish a metafield definition to a vibe-coded site (admin mode).
9414
- * @example
9415
- * ```typescript
9416
- * await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
9417
- * ```
9662
+ * Publish a metafield definition to a sales channel (admin mode).
9663
+ *
9664
+ * **Not callable.** This asked for `publish-vibe-coded`, which is a
9665
+ * deprecated backend alias, not the canonical route. The canonical spelling
9666
+ * is `publish-sales-channel`, and the `/v1` surface serves it for products
9667
+ * (`external-api.controller.ts:862`), coupons (`:947`), customers (`:1548`),
9668
+ * categories (`:3039`), brands (`:3211`) and tags (`:3349`) — but NOT for
9669
+ * metafield definitions. Both spellings 404 there.
9670
+ *
9671
+ * The operation exists only on the dashboard surface
9672
+ * (`POST /api/stores/:storeId/metafield-definitions/:id/publish-sales-channel`,
9673
+ * `metafields.controller.ts:190`), which needs a path `storeId` this signature
9674
+ * does not carry.
9675
+ *
9676
+ * Publish the definition to a sales channel from the Brainerce dashboard. A
9677
+ * definition with no publishes stays visible to every sales channel of the
9678
+ * store, so leaving it unpublished is the permissive default, not a lockout.
9418
9679
  */
9419
9680
  async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
9420
- return this.adminRequest(
9421
- "POST",
9422
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/publish-vibe-coded`,
9423
- { vibeCodedConnectionId }
9681
+ void definitionId;
9682
+ void vibeCodedConnectionId;
9683
+ throw new BrainerceError(
9684
+ "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.",
9685
+ 400
9424
9686
  );
9425
9687
  }
9426
- /** Unpublish a metafield definition from a vibe-coded site (admin mode). */
9688
+ /**
9689
+ * Unpublish a metafield definition from a sales channel (admin mode).
9690
+ *
9691
+ * **Not callable.** Same gap as
9692
+ * {@link publishMetafieldDefinitionToVibeCodedSite} — the `/v1` surface
9693
+ * carries no per-sales-channel routes for metafield definitions under either
9694
+ * the canonical `unpublish-sales-channel` spelling or the deprecated
9695
+ * `unpublish-vibe-coded` alias. The dashboard route is
9696
+ * `metafields.controller.ts:217`.
9697
+ */
9427
9698
  async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
9428
- return this.adminRequest(
9429
- "POST",
9430
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/unpublish-vibe-coded`,
9431
- { vibeCodedConnectionId }
9699
+ void definitionId;
9700
+ void vibeCodedConnectionId;
9701
+ throw new BrainerceError(
9702
+ "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.",
9703
+ 400
9432
9704
  );
9433
9705
  }
9434
9706
  /**
@@ -9574,10 +9846,11 @@ var _BrainerceClient = class _BrainerceClient {
9574
9846
  * ```
9575
9847
  */
9576
9848
  async setDefinitionProducts(definitionId, data) {
9577
- return this.adminRequest(
9578
- "PATCH",
9579
- `/api/v1/metafield-definitions/${encodePathSegment(definitionId)}/products`,
9580
- data
9849
+ void definitionId;
9850
+ void data;
9851
+ throw new BrainerceError(
9852
+ "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.",
9853
+ 400
9581
9854
  );
9582
9855
  }
9583
9856
  // -------------------- Metafields: Product Values --------------------
@@ -9623,24 +9896,41 @@ var _BrainerceClient = class _BrainerceClient {
9623
9896
  // -------------------- Product Customization Fields (Admin) --------------------
9624
9897
  /**
9625
9898
  * Get customization fields assigned to a product.
9626
- * Requires Admin mode (apiKey).
9899
+ *
9900
+ * **Not callable.** The API-key `/v1` surface has no
9901
+ * `metafield-definitions/products/:productId/customization-fields` route, so
9902
+ * this 404'd silently. It exists only on the dashboard surface
9903
+ * (`GET /api/stores/:storeId/metafield-definitions/products/:productId/customization-fields`,
9904
+ * `metafields.controller.ts:277`), which needs a path `storeId` this signature
9905
+ * does not carry.
9906
+ *
9907
+ * {@link getProductMetafields} is the closest working call — it returns the
9908
+ * metafield VALUES stored on a product over
9909
+ * `GET /api/v1/products/:productId/metafields`, not the customer-input field
9910
+ * definitions attached to it.
9627
9911
  */
9628
9912
  async getProductCustomizationFields(productId) {
9629
- return this.adminRequest(
9630
- "GET",
9631
- `/api/v1/metafield-definitions/products/${encodePathSegment(productId)}/customization-fields`
9913
+ void productId;
9914
+ throw new BrainerceError(
9915
+ "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.",
9916
+ 400
9632
9917
  );
9633
9918
  }
9634
9919
  /**
9635
9920
  * Set customization fields for a product (replaces all existing assignments).
9636
- * Only definitions marked as `isCustomerInput: true` can be assigned.
9637
- * Requires Admin mode (apiKey).
9921
+ *
9922
+ * **Not callable.** Same gap as {@link getProductCustomizationFields}: the
9923
+ * `/v1` surface carries no customization-field routes. The dashboard route is
9924
+ * `PATCH /api/stores/:storeId/metafield-definitions/products/:productId/customization-fields`
9925
+ * (`metafields.controller.ts:298`), which needs a path `storeId` this
9926
+ * signature does not carry.
9638
9927
  */
9639
9928
  async setProductCustomizationFields(productId, definitionIds) {
9640
- return this.adminRequest(
9641
- "PATCH",
9642
- `/api/v1/metafield-definitions/products/${encodePathSegment(productId)}/customization-fields`,
9643
- { definitionIds }
9929
+ void productId;
9930
+ void definitionIds;
9931
+ throw new BrainerceError(
9932
+ "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.",
9933
+ 400
9644
9934
  );
9645
9935
  }
9646
9936
  /**
@@ -9823,6 +10113,27 @@ var _BrainerceClient = class _BrainerceClient {
9823
10113
  }
9824
10114
  // -------------------- Store Team Management (Admin) --------------------
9825
10115
  // Store-level team management. Each store has its own team with roles and permissions.
10116
+ /**
10117
+ * Every store-level team operation is dashboard-only.
10118
+ *
10119
+ * `store-team.controller.ts:53` carries `DashboardOnlyGuard`, which rejects
10120
+ * `api_key` and `app_installation` principals outright, so no SDK caller can
10121
+ * reach these however the URL is spelled. They additionally pointed at
10122
+ * `/api/v1/stores/:storeId/team*`, and `@Controller('v1')`
10123
+ * (external-api.controller.ts:181) has no `stores` root — so what they
10124
+ * actually returned was a 404, not the 403 you would expect from the guard.
10125
+ *
10126
+ * Throwing beats either status code: a 404 reads as "wrong id" and a 403 as
10127
+ * "missing permission", and both send the caller looking for a fix that does
10128
+ * not exist. Use the account-level `getTeamMembers()` family, or the
10129
+ * dashboard.
10130
+ */
10131
+ dashboardOnlyTeamOperation(operation) {
10132
+ throw new BrainerceError(
10133
+ `${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.`,
10134
+ 403
10135
+ );
10136
+ }
9826
10137
  /**
9827
10138
  * Get the team for a specific store (members + pending invitations)
9828
10139
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
@@ -9832,11 +10143,8 @@ var _BrainerceClient = class _BrainerceClient {
9832
10143
  * const { members, invitations } = await client.getStoreTeam('store_id');
9833
10144
  * ```
9834
10145
  */
9835
- async getStoreTeam(storeId) {
9836
- return this.adminRequest(
9837
- "GET",
9838
- `/api/v1/stores/${encodePathSegment(storeId)}/team`
9839
- );
10146
+ async getStoreTeam(_storeId) {
10147
+ return this.dashboardOnlyTeamOperation("getStoreTeam");
9840
10148
  }
9841
10149
  /**
9842
10150
  * Invite a new member to a store
@@ -9853,12 +10161,8 @@ var _BrainerceClient = class _BrainerceClient {
9853
10161
  * });
9854
10162
  * ```
9855
10163
  */
9856
- async inviteStoreMember(storeId, data) {
9857
- return this.adminRequest(
9858
- "POST",
9859
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invite`,
9860
- data
9861
- );
10164
+ async inviteStoreMember(_storeId, _data) {
10165
+ return this.dashboardOnlyTeamOperation("inviteStoreMember");
9862
10166
  }
9863
10167
  /**
9864
10168
  * Update a store team member's role and/or permissions
@@ -9875,12 +10179,8 @@ var _BrainerceClient = class _BrainerceClient {
9875
10179
  * });
9876
10180
  * ```
9877
10181
  */
9878
- async updateStoreMember(storeId, memberId, data) {
9879
- return this.adminRequest(
9880
- "PATCH",
9881
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`,
9882
- data
9883
- );
10182
+ async updateStoreMember(_storeId, _memberId, _data) {
10183
+ return this.dashboardOnlyTeamOperation("updateStoreMember");
9884
10184
  }
9885
10185
  /**
9886
10186
  * Replace the set of vibe-coded sales channels a store member is restricted to.
@@ -9901,42 +10201,29 @@ var _BrainerceClient = class _BrainerceClient {
9901
10201
  * });
9902
10202
  * ```
9903
10203
  */
9904
- async updateStoreMemberSalesChannels(storeId, memberId, data) {
9905
- return this.adminRequest(
9906
- "PATCH",
9907
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}/sales-channels`,
9908
- data
9909
- );
10204
+ async updateStoreMemberSalesChannels(_storeId, _memberId, _data) {
10205
+ return this.dashboardOnlyTeamOperation("updateStoreMemberSalesChannels");
9910
10206
  }
9911
10207
  /**
9912
10208
  * Remove a member from a store team
9913
10209
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9914
10210
  */
9915
- async removeStoreMember(storeId, memberId) {
9916
- await this.adminRequest(
9917
- "DELETE",
9918
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`
9919
- );
10211
+ async removeStoreMember(_storeId, _memberId) {
10212
+ return this.dashboardOnlyTeamOperation("removeStoreMember");
9920
10213
  }
9921
10214
  /**
9922
10215
  * Resend a store invitation email
9923
10216
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9924
10217
  */
9925
- async resendStoreInvitation(storeId, invitationId) {
9926
- return this.adminRequest(
9927
- "POST",
9928
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}/resend`
9929
- );
10218
+ async resendStoreInvitation(_storeId, _invitationId) {
10219
+ return this.dashboardOnlyTeamOperation("resendStoreInvitation");
9930
10220
  }
9931
10221
  /**
9932
10222
  * Revoke a store invitation
9933
10223
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9934
10224
  */
9935
- async revokeStoreInvitation(storeId, invitationId) {
9936
- await this.adminRequest(
9937
- "DELETE",
9938
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}`
9939
- );
10225
+ async revokeStoreInvitation(_storeId, _invitationId) {
10226
+ return this.dashboardOnlyTeamOperation("revokeStoreInvitation");
9940
10227
  }
9941
10228
  /**
9942
10229
  * Get public invitation details by token (no auth required)
@@ -9945,7 +10232,7 @@ var _BrainerceClient = class _BrainerceClient {
9945
10232
  async getStoreInvitationByToken(token) {
9946
10233
  return this.request(
9947
10234
  "GET",
9948
- `/api/v1/store-invitations/${encodePathSegment(token)}`
10235
+ `/api/store-invitations/${encodePathSegment(token)}`
9949
10236
  );
9950
10237
  }
9951
10238
  /**
@@ -9953,9 +10240,9 @@ var _BrainerceClient = class _BrainerceClient {
9953
10240
  * Requires Admin mode (apiKey)
9954
10241
  */
9955
10242
  async acceptStoreInvitation(token) {
9956
- await this.adminRequest(
9957
- "POST",
9958
- `/api/v1/store-invitations/${encodePathSegment(token)}/accept`
10243
+ throw new BrainerceError(
10244
+ "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.",
10245
+ 403
9959
10246
  );
9960
10247
  }
9961
10248
  /**
@@ -9972,7 +10259,7 @@ var _BrainerceClient = class _BrainerceClient {
9972
10259
  * ```
9973
10260
  */
9974
10261
  async getMyStores() {
9975
- return this.adminRequest("GET", "/api/v1/me/stores");
10262
+ return this.dashboardOnlyUserContext("getMyStores");
9976
10263
  }
9977
10264
  /**
9978
10265
  * Get the current user's resolved permissions for a specific store
@@ -9986,10 +10273,23 @@ var _BrainerceClient = class _BrainerceClient {
9986
10273
  * }
9987
10274
  * ```
9988
10275
  */
9989
- async getMyStorePermissions(storeId) {
9990
- return this.adminRequest(
9991
- "GET",
9992
- `/api/v1/me/stores/${encodePathSegment(storeId)}/permissions`
10276
+ async getMyStorePermissions(_storeId) {
10277
+ return this.dashboardOnlyUserContext("getMyStorePermissions");
10278
+ }
10279
+ /**
10280
+ * `/me/*` answers "who am I and what can I reach", which only a real user can
10281
+ * ask. `UserContextController` (store-team.controller.ts:246) is guarded by
10282
+ * `DashboardOnlyGuard` for a load-bearing reason its own G16 comment spells
10283
+ * out: both routes resolve access purely from `@CurrentUserId()`, which is
10284
+ * `undefined` for an api_key principal, so the store filter would be stripped
10285
+ * and every store on the platform returned. The guard is the control. These
10286
+ * also pointed at `/api/v1/me/*`, which no controller serves, so the observed
10287
+ * failure was a 404 rather than the guard's 403.
10288
+ */
10289
+ dashboardOnlyUserContext(operation) {
10290
+ throw new BrainerceError(
10291
+ `${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.`,
10292
+ 403
9993
10293
  );
9994
10294
  }
9995
10295
  // -------------------- Email Settings & Templates (Admin) --------------------
@@ -10132,7 +10432,7 @@ var _BrainerceClient = class _BrainerceClient {
10132
10432
  * Requires Admin mode (apiKey)
10133
10433
  */
10134
10434
  async getSyncConflicts() {
10135
- return this.adminRequest("GET", "/api/v1/sync-conflicts");
10435
+ return this.syncConflictsNotImplemented("getSyncConflicts");
10136
10436
  }
10137
10437
  /**
10138
10438
  * Resolve a sync conflict
@@ -10142,12 +10442,28 @@ var _BrainerceClient = class _BrainerceClient {
10142
10442
  * @param resolution - 'MERGE' to link to existing product, 'CREATE_NEW' to create new product
10143
10443
  */
10144
10444
  async resolveSyncConflict(conflictId, resolution) {
10145
- return this.adminRequest(
10146
- "POST",
10147
- `/api/v1/sync-conflicts/${encodePathSegment(conflictId)}/resolve`,
10148
- {
10149
- resolution
10150
- }
10445
+ void conflictId;
10446
+ void resolution;
10447
+ return this.syncConflictsNotImplemented("resolveSyncConflict");
10448
+ }
10449
+ /**
10450
+ * Sync conflicts were never implemented on the server.
10451
+ *
10452
+ * There is no `sync-conflict` route anywhere in the backend — not under
10453
+ * `@Controller('v1')`, not on any other controller. These two methods have
10454
+ * called a URL that has never existed, and `SyncConflict` /
10455
+ * `SyncConflictResolution` / `ResolveSyncConflictDto` are exported types with
10456
+ * no producer. The METAFIELD conflict siblings below are real
10457
+ * (external-api.controller.ts:4788, :4802) and are easy to mistake for these.
10458
+ *
10459
+ * Kept as throwing stubs rather than deleted: removing exported methods from a
10460
+ * published package is a breaking change, and a caller who has been swallowing
10461
+ * a 404 deserves to be told why.
10462
+ */
10463
+ syncConflictsNotImplemented(operation) {
10464
+ throw new BrainerceError(
10465
+ `${operation} is not implemented: the platform exposes no sync-conflict endpoint. If you are looking for metafield sync conflicts, use getMetafieldConflicts() / resolveMetafieldConflict().`,
10466
+ 501
10151
10467
  );
10152
10468
  }
10153
10469
  // -------------------- Metafield Conflicts (Admin) --------------------
@@ -11009,6 +11325,7 @@ function buildProductJsonLd(product, opts) {
11009
11325
  const inv = product.inventory;
11010
11326
  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";
11011
11327
  const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
11328
+ const omitOffer = product.type === "KIT";
11012
11329
  const itemCondition = "https://schema.org/NewCondition";
11013
11330
  const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
11014
11331
  "@type": "OfferShippingDetails",
@@ -11071,7 +11388,7 @@ function buildProductJsonLd(product, opts) {
11071
11388
  ...product.gtin ? { gtin: product.gtin } : {},
11072
11389
  ...product.mpn ? { mpn: product.mpn } : {},
11073
11390
  ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
11074
- offers: offer,
11391
+ ...omitOffer ? {} : { offers: offer },
11075
11392
  // Google policy: never emit an empty/zero rating block. bestRating /
11076
11393
  // worstRating make the 1-5 scale explicit so aggregators can't misread
11077
11394
  // a 4.8 on an assumed 0-10 scale.