brainerce 2.0.2 → 2.2.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.1";
118
+ var SDK_VERSION = "2.2.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);
@@ -1132,6 +1132,45 @@ var _BrainerceClient = class _BrainerceClient {
1132
1132
  getStoreDirection(locale) {
1133
1133
  return getDirectionForLocale(locale ?? this.locale);
1134
1134
  }
1135
+ /**
1136
+ * Get what the merchant actually configured on this sales channel: store
1137
+ * identity and multi-language setup, the channel's own settings (low-stock
1138
+ * warning and threshold, reservation strategy and timeout, birthday and
1139
+ * email-verification requirements, granted scopes) and which optional
1140
+ * features are switched on (payment providers, social login, coupons,
1141
+ * discount rules, shipping zones, downloadables, content, loyalty).
1142
+ *
1143
+ * Read this instead of hardcoding. A storefront with a hardcoded low-stock
1144
+ * threshold shows the wrong badge on every store whose merchant chose a
1145
+ * different one, and a storefront that renders a feature nobody enabled
1146
+ * ships a control that silently does nothing.
1147
+ *
1148
+ * Only available in vibe-coded mode (`salesChannelId`). The payload belongs
1149
+ * to one sales channel: `storeId` mode has no channel to read it from, and
1150
+ * an `apiKey` addresses the store rather than any single channel.
1151
+ *
1152
+ * Call it once for the whole app and share the result. It is per channel,
1153
+ * not per product, so fetching it on every page is wasted work.
1154
+ *
1155
+ * @example
1156
+ * ```typescript
1157
+ * const caps = await client.getStoreCapabilities();
1158
+ *
1159
+ * // Low stock: honour the switch before the number.
1160
+ * const threshold = caps.connection.lowStockWarning
1161
+ * ? caps.connection.lowStockThreshold
1162
+ * : 0;
1163
+ *
1164
+ * if (caps.features.hasCoupons) renderCouponInput();
1165
+ * if (caps.store.i18n?.enabled) renderLocaleSwitcher(caps.store.i18n.supportedLocales);
1166
+ * ```
1167
+ */
1168
+ async getStoreCapabilities() {
1169
+ if (this.isVibeCodedMode()) {
1170
+ return this.vibeCodedRequest("GET", "/capabilities");
1171
+ }
1172
+ throw new BrainerceError("getStoreCapabilities is only available in vibe-coded mode", 400);
1173
+ }
1135
1174
  // -------------------- Analytics --------------------
1136
1175
  /**
1137
1176
  * Send a storefront analytics beacon (pageview or engagement).
@@ -1979,11 +2018,9 @@ var _BrainerceClient = class _BrainerceClient {
1979
2018
  queryParams
1980
2019
  );
1981
2020
  }
1982
- return this.adminRequest(
1983
- "GET",
1984
- "/api/v1/search/suggestions",
1985
- void 0,
1986
- queryParams
2021
+ throw new BrainerceError(
2022
+ "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.",
2023
+ 400
1987
2024
  );
1988
2025
  }
1989
2026
  /**
@@ -2959,14 +2996,20 @@ var _BrainerceClient = class _BrainerceClient {
2959
2996
  });
2960
2997
  }
2961
2998
  /**
2962
- * Get platform capabilities for coupon features.
2963
- * Use this to understand what features each platform supports.
2999
+ * Get platform capabilities for coupon features, keyed by platform.
3000
+ *
3001
+ * ⛔ **Returns an empty object today.** Platform coupon capabilities moved
3002
+ * into the standalone connector apps and are not re-exposed here yet, so the
3003
+ * endpoint answers `{}` for every store. Treat a missing key as "unknown",
3004
+ * never as "the platform lacks the feature", and do not index into a platform
3005
+ * key without checking it exists first — there are none to find.
2964
3006
  *
2965
3007
  * @example
2966
3008
  * ```typescript
2967
3009
  * const capabilities = await client.getCouponPlatformCapabilities();
2968
- * if (!capabilities.SHOPIFY.supportsProductExclusions) {
2969
- * console.log('Shopify does not support product exclusions');
3010
+ * const meta = capabilities['GOOGLE'];
3011
+ * if (meta && !meta.supportsProducts) {
3012
+ * console.log('This platform cannot target individual products');
2970
3013
  * }
2971
3014
  * ```
2972
3015
  */
@@ -3092,7 +3135,7 @@ var _BrainerceClient = class _BrainerceClient {
3092
3135
  );
3093
3136
  }
3094
3137
  console.warn(
3095
- `BrainerceClient.${methodName}: passing \`storeId\` is deprecated \u2014 it is now derived from the SDK config. Remove the leading argument; this overload will be removed in SDK 2.0.`
3138
+ `BrainerceClient.${methodName}: passing \`storeId\` is deprecated \u2014 it is now derived from the SDK config. Remove the leading argument; this overload will be removed in a future major version.`
3096
3139
  );
3097
3140
  }
3098
3141
  /**
@@ -5587,6 +5630,324 @@ var _BrainerceClient = class _BrainerceClient {
5587
5630
  "checkout"
5588
5631
  );
5589
5632
  }
5633
+ /**
5634
+ * Apply a gift card to a checkout.
5635
+ *
5636
+ * Merchants issue cards from the dashboard, so live codes exist and this field
5637
+ * is worth building. There is still no way for a shopper to BUY a gift card —
5638
+ * no product type, no purchase flow — so every card in circulation was issued
5639
+ * by hand.
5640
+ *
5641
+ * Redemption is deliberately NOT gated on the store's gift-card switch: a
5642
+ * store that turned the feature off still owes every card already in a
5643
+ * customer's hand, so the field keeps working. Build it unconditionally.
5644
+ *
5645
+ * A gift card is a **means of payment, not a discount**. The order total does
5646
+ * not change and tax stays calculated on the full value; what changes is
5647
+ * `providerAmountDue`, the amount the payment provider will be charged.
5648
+ *
5649
+ * Render it as its own line — "Gift card −₪54.50" beside the total — and NOT
5650
+ * by adding it to `discountAmount`. A shopper who sees stored value folded
5651
+ * into a discount is being shown the wrong thing, and so is their receipt.
5652
+ *
5653
+ * Only as much of the card as the order still owes is applied, so a card
5654
+ * larger than the basket leaves a balance on it for next time, and a smaller
5655
+ * one leaves an amount for the provider to charge.
5656
+ *
5657
+ * @example
5658
+ * const { amountApplied, providerAmountDue, tenderId } =
5659
+ * await client.applyGiftCard('checkout_123', 'A1B2-C3D4-E5F6-G7H8-J9K0');
5660
+ * // total is unchanged; charge the provider providerAmountDue
5661
+ */
5662
+ async applyGiftCard(checkoutId, code) {
5663
+ if (this.isVibeCodedMode()) {
5664
+ return this.vibeCodedRequest(
5665
+ "POST",
5666
+ `/checkout/${encodePathSegment(checkoutId)}/gift-card`,
5667
+ { code }
5668
+ );
5669
+ }
5670
+ if (this.storeId && !this.apiKey) {
5671
+ return this.storefrontRequest(
5672
+ "POST",
5673
+ `/checkout/${encodePathSegment(checkoutId)}/gift-card`,
5674
+ { code }
5675
+ );
5676
+ }
5677
+ return this.adminRequest(
5678
+ "POST",
5679
+ `/api/v1/checkout/${encodePathSegment(checkoutId)}/gift-card`,
5680
+ { code }
5681
+ );
5682
+ }
5683
+ /**
5684
+ * Remove a previously applied gift card from a checkout.
5685
+ *
5686
+ * Takes the `tenderId` returned by {@link applyGiftCard}, not the code — a
5687
+ * checkout can carry more than one card, and the code is never echoed back.
5688
+ *
5689
+ * The held value goes straight back to the card. Nothing was ever debited
5690
+ * while it was applied, so removing costs the shopper nothing.
5691
+ */
5692
+ async removeGiftCard(checkoutId, tenderId) {
5693
+ const path = `/checkout/${encodePathSegment(checkoutId)}/gift-card/${encodePathSegment(tenderId)}`;
5694
+ if (this.isVibeCodedMode()) {
5695
+ return this.vibeCodedRequest("DELETE", path);
5696
+ }
5697
+ if (this.storeId && !this.apiKey) {
5698
+ return this.storefrontRequest("DELETE", path);
5699
+ }
5700
+ return this.adminRequest("DELETE", `/api/v1${path}`);
5701
+ }
5702
+ /**
5703
+ * Check what is left on a gift card.
5704
+ *
5705
+ * Rate limited, and deliberately uninformative: a code that does not exist,
5706
+ * one that has been disabled and one that has expired all return the SAME
5707
+ * response — `{ balance: '0.00', usable: false }` — and take the same time to
5708
+ * do it. Do not build UI that tries to tell those apart, because the API will
5709
+ * not tell you, by design: a gift-card code is bearer value, and an endpoint
5710
+ * that confirmed which codes were real would be a free way to find them.
5711
+ *
5712
+ * Show "we cannot use this code" and let the shopper re-enter it.
5713
+ */
5714
+ async checkGiftCardBalance(code) {
5715
+ if (this.isVibeCodedMode()) {
5716
+ return this.vibeCodedRequest("POST", "/gift-cards/balance", { code });
5717
+ }
5718
+ if (this.storeId && !this.apiKey) {
5719
+ return this.storefrontRequest("POST", "/gift-cards/balance", { code });
5720
+ }
5721
+ return this.adminRequest("POST", "/api/v1/gift-cards/balance", { code });
5722
+ }
5723
+ // ==========================================================================
5724
+ // Gift cards — administration (API key only)
5725
+ //
5726
+ // Full programmatic management, unlike Shopify, which requires you to ask
5727
+ // their support for the equivalent scope. The gate here is the SCOPE the
5728
+ // merchant granted your key, so the separation matters: `gift_cards:issue`
5729
+ // mints stored value and `gift_cards:adjust` rewrites a balance, and neither
5730
+ // is implied by `gift_cards:read`. Ask for the least your integration needs.
5731
+ //
5732
+ // Every call reaches the same service the dashboard uses, so the rules hold
5733
+ // identically: a note is mandatory on anything that moves value, a debit
5734
+ // cannot cross what live checkout holds have reserved, and there is no delete
5735
+ // anywhere — the ledger is append-only.
5736
+ // ==========================================================================
5737
+ /**
5738
+ * List gift cards.
5739
+ *
5740
+ * `search` matches the LAST FOUR of a code or part of a recipient email. It
5741
+ * cannot match a full code: only an HMAC is stored, so there is nothing to
5742
+ * search against.
5743
+ *
5744
+ * Requires `gift_cards:read`.
5745
+ */
5746
+ async listGiftCards(params) {
5747
+ const q = new URLSearchParams();
5748
+ if (params?.page) q.set("page", String(params.page));
5749
+ if (params?.limit) q.set("limit", String(params.limit));
5750
+ if (params?.filter) q.set("filter", params.filter);
5751
+ if (params?.search) q.set("search", params.search);
5752
+ const qs = q.toString();
5753
+ return this.adminRequest(
5754
+ "GET",
5755
+ `/api/v1/gift-cards${qs ? `?${qs}` : ""}`
5756
+ );
5757
+ }
5758
+ /**
5759
+ * Outstanding gift card liability, per currency.
5760
+ *
5761
+ * This is the month-end number. Read `byCurrency` if the store sells in more
5762
+ * than one — currencies are never summed together.
5763
+ *
5764
+ * Requires `gift_cards:read`.
5765
+ */
5766
+ async getGiftCardLiability() {
5767
+ return this.adminRequest("GET", "/api/v1/gift-cards/liability");
5768
+ }
5769
+ /**
5770
+ * One gift card with its full ledger.
5771
+ *
5772
+ * Requires `gift_cards:read`.
5773
+ */
5774
+ async getGiftCard(giftCardId) {
5775
+ return this.adminRequest(
5776
+ "GET",
5777
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}`
5778
+ );
5779
+ }
5780
+ /**
5781
+ * Issue a gift card.
5782
+ *
5783
+ * ⚠️ **The code comes back exactly once.** It is stored only as an HMAC, so
5784
+ * this response is the only time it exists in readable form anywhere. Persist
5785
+ * it or deliver it before you discard the response — no later call, dashboard
5786
+ * screen or database query can recover it.
5787
+ *
5788
+ * A `note` is required. Refused when gift cards are switched off for the
5789
+ * store. Pass an `Idempotency-Key` header to make a retry safe.
5790
+ *
5791
+ * Requires `gift_cards:issue`.
5792
+ *
5793
+ * @example
5794
+ * ```typescript
5795
+ * const card = await client.issueGiftCard({
5796
+ * amount: '200.00',
5797
+ * note: 'Compensation for order #1042',
5798
+ * recipientEmail: 'dana@example.com',
5799
+ * });
5800
+ * await sendToCustomer(card.plaintextCode); // your only chance
5801
+ * ```
5802
+ */
5803
+ async issueGiftCard(data) {
5804
+ return this.adminRequest("POST", "/api/v1/gift-cards", data);
5805
+ }
5806
+ /**
5807
+ * Re-issue a gift card onto a new code.
5808
+ *
5809
+ * The answer to a customer losing their code. Mints a new code, moves the
5810
+ * WHOLE balance to it, and REVOKES the old card.
5811
+ *
5812
+ * **This is not a resend.** The old code stops working the moment this
5813
+ * returns — if the customer still holds a printed card, it dies. Refused
5814
+ * while a checkout holds value on the card. The original expiry carries
5815
+ * forward, so this cannot be used to restart an expiry clock.
5816
+ *
5817
+ * The new code is returned exactly once, under the same rules as issuance.
5818
+ *
5819
+ * Requires `gift_cards:issue`.
5820
+ */
5821
+ async reissueGiftCard(giftCardId, note) {
5822
+ return this.adminRequest(
5823
+ "POST",
5824
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/reissue`,
5825
+ { note }
5826
+ );
5827
+ }
5828
+ /**
5829
+ * Adjust a gift card balance.
5830
+ *
5831
+ * `delta` is a SIGNED decimal string: `"25.00"` adds, `"-25.00"` takes away.
5832
+ * The `note` is required and is written to the ledger permanently — it is the
5833
+ * row a finance review reads a year from now.
5834
+ *
5835
+ * A debit cannot take the balance below what live checkout holds have already
5836
+ * reserved; that refusal names the held amount so you can act on it.
5837
+ *
5838
+ * Requires `gift_cards:adjust`.
5839
+ */
5840
+ async adjustGiftCardBalance(giftCardId, delta, note) {
5841
+ return this.adminRequest(
5842
+ "PATCH",
5843
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/adjust`,
5844
+ { delta, note }
5845
+ );
5846
+ }
5847
+ /**
5848
+ * Enable, disable or revoke one gift card.
5849
+ *
5850
+ * Deliberately does NOT touch live holds: a checkout that already reserved
5851
+ * value settles normally, because pulling it out from under a shopper
5852
+ * mid-payment would strand a provider charge already in flight. Disabling
5853
+ * stops NEW holds, which is what "off" actually means.
5854
+ *
5855
+ * Requires `gift_cards:write`.
5856
+ */
5857
+ async setGiftCardStatus(giftCardId, status) {
5858
+ return this.adminRequest(
5859
+ "PATCH",
5860
+ `/api/v1/gift-cards/${encodePathSegment(giftCardId)}/status`,
5861
+ { status }
5862
+ );
5863
+ }
5864
+ /**
5865
+ * Disable or reactivate many gift cards at once.
5866
+ *
5867
+ * `REVOKED` is not accepted here — it belongs to re-issue, which moves the
5868
+ * balance to a replacement first. Revoking in bulk would strand balances with
5869
+ * nowhere to go. Cards already revoked are skipped, so the returned count is
5870
+ * the honest one and may be lower than the ids you sent.
5871
+ *
5872
+ * There is no bulk delete, here or anywhere: the ledger is append-only and a
5873
+ * card may carry a statutory retention life.
5874
+ *
5875
+ * Requires `gift_cards:write`.
5876
+ */
5877
+ async bulkSetGiftCardStatus(giftCardIds, status) {
5878
+ return this.adminRequest("PATCH", "/api/v1/gift-cards/bulk/status", {
5879
+ giftCardIds,
5880
+ status
5881
+ });
5882
+ }
5883
+ // ==========================================================================
5884
+ // Donations
5885
+ // ==========================================================================
5886
+ /**
5887
+ * Start a donation.
5888
+ *
5889
+ * A donation does not go through the cart. There is no line item, no
5890
+ * quantity, no shipping and no order — a donor names an amount and pays it,
5891
+ * which is a different shape of transaction from a purchase. Do not model a
5892
+ * donation as a product; if you already have, the amount is the giveaway:
5893
+ * you cannot let a donor type one.
5894
+ *
5895
+ * ⛔ A successful return is NOT a completed gift. You get back a PENDING
5896
+ * donation and a provider intent to complete, exactly as with a checkout.
5897
+ * The gift is only real once the provider's webhook confirms it, which is
5898
+ * when the donation reads back as `PAID`. Show a thank-you that reflects
5899
+ * that, and never send a receipt off the back of this call.
5900
+ *
5901
+ * Requires donations to be switched on for the store; a store that has not
5902
+ * turned them on is rejected rather than silently accepting money.
5903
+ *
5904
+ * ```ts
5905
+ * const donation = await brainerce.createDonation({
5906
+ * amount: 180,
5907
+ * feeCoverAmount: 6.3, // offered as a checkbox; most donors accept
5908
+ * donorEmail: 'sarah@example.com',
5909
+ * donorName: 'Sarah Cohen',
5910
+ * tributeType: 'IN_MEMORY',
5911
+ * tributeName: 'Avraham Cohen',
5912
+ * returnPath: '/thank-you',
5913
+ * });
5914
+ * // → complete donation.payment with your provider, then poll getDonation()
5915
+ * ```
5916
+ */
5917
+ async createDonation(input) {
5918
+ if (this.isVibeCodedMode()) {
5919
+ return this.vibeCodedRequest("POST", "/donations", input);
5920
+ }
5921
+ if (this.storeId && !this.apiKey) {
5922
+ return this.storefrontRequest("POST", "/donations", input);
5923
+ }
5924
+ throw new Error(
5925
+ "createDonation is a storefront call. Initialise the SDK with a salesChannelId or storeId \u2014 an admin apiKey cannot start a donation."
5926
+ );
5927
+ }
5928
+ /**
5929
+ * Read a donation back — for a thank-you page, and to see whether it is paid.
5930
+ *
5931
+ * Rate limited to 5 requests a minute, because an id that either resolves or
5932
+ * 404s is a way to enumerate them. Poll it a handful of times after the donor
5933
+ * returns from the provider; do not put it behind a 1-second interval.
5934
+ *
5935
+ * The payload is narrow on purpose: no failure reason, and `donorName` comes
5936
+ * back `null` whenever the gift was marked anonymous — so you can render this
5937
+ * straight onto a public page without leaking anything.
5938
+ */
5939
+ async getDonation(donationId) {
5940
+ const path = `/donations/${encodePathSegment(donationId)}`;
5941
+ if (this.isVibeCodedMode()) {
5942
+ return this.vibeCodedRequest("GET", path);
5943
+ }
5944
+ if (this.storeId && !this.apiKey) {
5945
+ return this.storefrontRequest("GET", path);
5946
+ }
5947
+ throw new Error(
5948
+ "getDonation is a storefront call. Initialise the SDK with a salesChannelId or storeId."
5949
+ );
5950
+ }
5590
5951
  /**
5591
5952
  * Set customer information on checkout
5592
5953
  *
@@ -5662,10 +6023,7 @@ var _BrainerceClient = class _BrainerceClient {
5662
6023
  "/checkouts/shipping-destinations"
5663
6024
  );
5664
6025
  }
5665
- return this.adminRequest(
5666
- "GET",
5667
- "/api/v1/checkouts/shipping-destinations"
5668
- );
6026
+ return this.adminRequest("GET", "/api/checkouts/shipping-destinations");
5669
6027
  }
5670
6028
  /**
5671
6029
  * Set shipping address on checkout (includes customer email).
@@ -5924,7 +6282,7 @@ var _BrainerceClient = class _BrainerceClient {
5924
6282
  if (this.storeId && !this.apiKey) {
5925
6283
  return this.storefrontRequest("GET", "/pickup-locations");
5926
6284
  }
5927
- return this.adminRequest("GET", "/api/v1/checkouts/pickup-locations");
6285
+ return this.adminRequest("GET", "/api/checkouts/pickup-locations");
5928
6286
  }
5929
6287
  /**
5930
6288
  * Set delivery type on checkout (shipping or pickup).
@@ -7938,119 +8296,6 @@ var _BrainerceClient = class _BrainerceClient {
7938
8296
  }
7939
8297
  return this.storefrontRequest("GET", "/customers/me/cart");
7940
8298
  }
7941
- // -------------------- Custom API Integrations --------------------
7942
- // These methods require Admin mode (apiKey)
7943
- /**
7944
- * Get all Custom API integrations for a store
7945
- * Requires Admin mode (apiKey)
7946
- *
7947
- * @example
7948
- * ```typescript
7949
- * const integrations = await client.getCustomApiIntegrations();
7950
- * integrations.forEach(api => {
7951
- * console.log(`${api.name}: ${api.status}`);
7952
- * });
7953
- * ```
7954
- */
7955
- async getCustomApiIntegrations() {
7956
- return this.adminRequest("GET", "/api/v1/custom-api");
7957
- }
7958
- /**
7959
- * Get a single Custom API integration by ID
7960
- * Requires Admin mode (apiKey)
7961
- *
7962
- * @example
7963
- * ```typescript
7964
- * const api = await client.getCustomApiIntegration('api_123');
7965
- * console.log(`API: ${api.name}, URL: ${api.baseUrl}`);
7966
- * ```
7967
- */
7968
- async getCustomApiIntegration(integrationId) {
7969
- return this.adminRequest(
7970
- "GET",
7971
- `/api/v1/custom-api/${encodePathSegment(integrationId)}`
7972
- );
7973
- }
7974
- /**
7975
- * Create a new Custom API integration
7976
- * Requires Admin mode (apiKey)
7977
- *
7978
- * @example
7979
- * ```typescript
7980
- * const api = await client.createCustomApiIntegration({
7981
- * name: 'My External API',
7982
- * baseUrl: 'https://api.example.com',
7983
- * authType: 'api_key',
7984
- * credentials: {
7985
- * apiKey: 'sk_123...',
7986
- * headerName: 'X-API-Key',
7987
- * },
7988
- * syncDirection: 'bidirectional',
7989
- * syncConfig: {
7990
- * products: true,
7991
- * orders: true,
7992
- * inventory: true,
7993
- * },
7994
- * });
7995
- * ```
7996
- */
7997
- async createCustomApiIntegration(data) {
7998
- return this.adminRequest("POST", "/api/v1/custom-api", data);
7999
- }
8000
- /**
8001
- * Update a Custom API integration
8002
- * Requires Admin mode (apiKey)
8003
- *
8004
- * @example
8005
- * ```typescript
8006
- * const api = await client.updateCustomApiIntegration('api_123', {
8007
- * enabled: false,
8008
- * syncConfig: { products: true, orders: false, inventory: true },
8009
- * });
8010
- * ```
8011
- */
8012
- async updateCustomApiIntegration(integrationId, data) {
8013
- return this.adminRequest(
8014
- "PATCH",
8015
- `/api/v1/custom-api/${encodePathSegment(integrationId)}`,
8016
- data
8017
- );
8018
- }
8019
- /**
8020
- * Delete a Custom API integration
8021
- * Requires Admin mode (apiKey)
8022
- *
8023
- * @example
8024
- * ```typescript
8025
- * await client.deleteCustomApiIntegration('api_123');
8026
- * ```
8027
- */
8028
- async deleteCustomApiIntegration(integrationId) {
8029
- await this.adminRequest(
8030
- "DELETE",
8031
- `/api/v1/custom-api/${encodePathSegment(integrationId)}`
8032
- );
8033
- }
8034
- /**
8035
- * Test connection to a Custom API
8036
- * Requires Admin mode (apiKey)
8037
- *
8038
- * @example
8039
- * ```typescript
8040
- * const result = await client.testCustomApiConnection('api_123');
8041
- * if (result.success) {
8042
- * console.log(`Connection OK, latency: ${result.latency}ms`);
8043
- * } else {
8044
- * console.error(`Connection failed: ${result.error}`);
8045
- * }
8046
- * ```
8047
- */
8048
- async testCustomApiConnection(integrationId) {
8049
- return this.adminRequest(
8050
- "POST",
8051
- `/api/v1/custom-api/${encodePathSegment(integrationId)}/test`
8052
- );
8053
- }
8054
8299
  // -------------------- Inventory Reservations --------------------
8055
8300
  // These methods are only available in vibe-coded mode when reservation strategy
8056
8301
  // is set to ON_CART or ON_CHECKOUT
@@ -8690,12 +8935,18 @@ var _BrainerceClient = class _BrainerceClient {
8690
8935
  * ```typescript
8691
8936
  * const rate = await client.createZoneShippingRate('zone_123', {
8692
8937
  * name: 'Standard Shipping',
8693
- * type: 'flat',
8694
- * price: 5.99,
8695
- * minOrderValue: 0,
8696
- * estimatedDays: '3-5',
8938
+ * type: 'FLAT_RATE',
8939
+ * rateConfig: { amount: 5.99 },
8940
+ * minDeliveryDays: 3,
8941
+ * maxDeliveryDays: 5,
8697
8942
  * });
8698
8943
  * ```
8944
+ *
8945
+ * The price lives in `rateConfig`, whose shape follows `type` — `FLAT_RATE`
8946
+ * takes `{ amount }`, `WEIGHT_BASED` and `PRICE_BASED` take tier arrays, and
8947
+ * `FREE` and `LOCAL_PICKUP` take none. Unknown top-level properties are
8948
+ * rejected outright rather than ignored, so a stray `price` or
8949
+ * `estimatedDays` fails the whole call with 400.
8699
8950
  */
8700
8951
  async createZoneShippingRate(zoneId, data) {
8701
8952
  return this.adminRequest(
@@ -8896,6 +9147,10 @@ var _BrainerceClient = class _BrainerceClient {
8896
9147
  *
8897
9148
  * Returns `appliesTax=false` when tax is disabled, the country is missing,
8898
9149
  * or no active rate covers it.
9150
+ *
9151
+ * The preview has no province, so it only sees country-level rates: a
9152
+ * Canadian estimate shows the federal GST alone and the provincial PST/QST
9153
+ * joins it at checkout. Render `note` so the buyer is not surprised.
8899
9154
  */
8900
9155
  async estimateTax(params) {
8901
9156
  const query = params.country ? { country: params.country, subtotal: params.subtotal } : { subtotal: params.subtotal };
@@ -8966,10 +9221,43 @@ var _BrainerceClient = class _BrainerceClient {
8966
9221
  /**
8967
9222
  * Get all tax rates for the store
8968
9223
  * Requires Admin mode (apiKey)
9224
+ *
9225
+ * A jurisdiction can need more than one rate. Canada is the common case: a
9226
+ * country-level `GST` row plus a province-level `PST`/`QST` row, both with
9227
+ * `stackable: true`, which the checkout charges together. Provinces on HST
9228
+ * carry a single combined row with `stackable: false`.
8969
9229
  */
8970
9230
  async getTaxRates() {
8971
9231
  return this.adminRequest("GET", "/api/v1/tax/rates");
8972
9232
  }
9233
+ /**
9234
+ * List the country tax presets available to apply.
9235
+ * Requires Admin mode (apiKey)
9236
+ */
9237
+ async getTaxPresets() {
9238
+ return this.adminRequest("GET", "/api/v1/tax/presets");
9239
+ }
9240
+ /**
9241
+ * Apply a country's whole tax table in one call, instead of creating a rate
9242
+ * per province by hand. Requires Admin mode (apiKey).
9243
+ *
9244
+ * `CA` writes ten rates: federal GST 5% country-wide, one combined HST row
9245
+ * each for ON/NB/NL/NS/PE, and PST/RST/QST for BC/SK/MB/QC charged on top of
9246
+ * the GST. Alberta and the territories need no row — the GST covers them.
9247
+ *
9248
+ * Throws 409 when the store already has rates for that country; delete those
9249
+ * first if you meant to replace them. Rates land in the Standard tax class.
9250
+ *
9251
+ * Brainerce does not register the store for GST/HST and does not file returns.
9252
+ *
9253
+ * @example
9254
+ * ```typescript
9255
+ * const { created } = await client.applyTaxPreset('CA'); // created === 10
9256
+ * ```
9257
+ */
9258
+ async applyTaxPreset(presetKey) {
9259
+ return this.adminRequest("POST", `/api/v1/tax/presets/${encodePathSegment(presetKey)}/apply`);
9260
+ }
8973
9261
  /**
8974
9262
  * Get a single tax rate by ID
8975
9263
  * Requires Admin mode (apiKey)
@@ -9598,6 +9886,27 @@ var _BrainerceClient = class _BrainerceClient {
9598
9886
  }
9599
9887
  // -------------------- Store Team Management (Admin) --------------------
9600
9888
  // Store-level team management. Each store has its own team with roles and permissions.
9889
+ /**
9890
+ * Every store-level team operation is dashboard-only.
9891
+ *
9892
+ * `store-team.controller.ts:53` carries `DashboardOnlyGuard`, which rejects
9893
+ * `api_key` and `app_installation` principals outright, so no SDK caller can
9894
+ * reach these however the URL is spelled. They additionally pointed at
9895
+ * `/api/v1/stores/:storeId/team*`, and `@Controller('v1')`
9896
+ * (external-api.controller.ts:181) has no `stores` root — so what they
9897
+ * actually returned was a 404, not the 403 you would expect from the guard.
9898
+ *
9899
+ * Throwing beats either status code: a 404 reads as "wrong id" and a 403 as
9900
+ * "missing permission", and both send the caller looking for a fix that does
9901
+ * not exist. Use the account-level `getTeamMembers()` family, or the
9902
+ * dashboard.
9903
+ */
9904
+ dashboardOnlyTeamOperation(operation) {
9905
+ throw new BrainerceError(
9906
+ `${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.`,
9907
+ 403
9908
+ );
9909
+ }
9601
9910
  /**
9602
9911
  * Get the team for a specific store (members + pending invitations)
9603
9912
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
@@ -9607,11 +9916,8 @@ var _BrainerceClient = class _BrainerceClient {
9607
9916
  * const { members, invitations } = await client.getStoreTeam('store_id');
9608
9917
  * ```
9609
9918
  */
9610
- async getStoreTeam(storeId) {
9611
- return this.adminRequest(
9612
- "GET",
9613
- `/api/v1/stores/${encodePathSegment(storeId)}/team`
9614
- );
9919
+ async getStoreTeam(_storeId) {
9920
+ return this.dashboardOnlyTeamOperation("getStoreTeam");
9615
9921
  }
9616
9922
  /**
9617
9923
  * Invite a new member to a store
@@ -9628,12 +9934,8 @@ var _BrainerceClient = class _BrainerceClient {
9628
9934
  * });
9629
9935
  * ```
9630
9936
  */
9631
- async inviteStoreMember(storeId, data) {
9632
- return this.adminRequest(
9633
- "POST",
9634
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invite`,
9635
- data
9636
- );
9937
+ async inviteStoreMember(_storeId, _data) {
9938
+ return this.dashboardOnlyTeamOperation("inviteStoreMember");
9637
9939
  }
9638
9940
  /**
9639
9941
  * Update a store team member's role and/or permissions
@@ -9650,12 +9952,8 @@ var _BrainerceClient = class _BrainerceClient {
9650
9952
  * });
9651
9953
  * ```
9652
9954
  */
9653
- async updateStoreMember(storeId, memberId, data) {
9654
- return this.adminRequest(
9655
- "PATCH",
9656
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`,
9657
- data
9658
- );
9955
+ async updateStoreMember(_storeId, _memberId, _data) {
9956
+ return this.dashboardOnlyTeamOperation("updateStoreMember");
9659
9957
  }
9660
9958
  /**
9661
9959
  * Replace the set of vibe-coded sales channels a store member is restricted to.
@@ -9676,42 +9974,29 @@ var _BrainerceClient = class _BrainerceClient {
9676
9974
  * });
9677
9975
  * ```
9678
9976
  */
9679
- async updateStoreMemberSalesChannels(storeId, memberId, data) {
9680
- return this.adminRequest(
9681
- "PATCH",
9682
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}/sales-channels`,
9683
- data
9684
- );
9977
+ async updateStoreMemberSalesChannels(_storeId, _memberId, _data) {
9978
+ return this.dashboardOnlyTeamOperation("updateStoreMemberSalesChannels");
9685
9979
  }
9686
9980
  /**
9687
9981
  * Remove a member from a store team
9688
9982
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9689
9983
  */
9690
- async removeStoreMember(storeId, memberId) {
9691
- await this.adminRequest(
9692
- "DELETE",
9693
- `/api/v1/stores/${encodePathSegment(storeId)}/team/${encodePathSegment(memberId)}`
9694
- );
9984
+ async removeStoreMember(_storeId, _memberId) {
9985
+ return this.dashboardOnlyTeamOperation("removeStoreMember");
9695
9986
  }
9696
9987
  /**
9697
9988
  * Resend a store invitation email
9698
9989
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9699
9990
  */
9700
- async resendStoreInvitation(storeId, invitationId) {
9701
- return this.adminRequest(
9702
- "POST",
9703
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}/resend`
9704
- );
9991
+ async resendStoreInvitation(_storeId, _invitationId) {
9992
+ return this.dashboardOnlyTeamOperation("resendStoreInvitation");
9705
9993
  }
9706
9994
  /**
9707
9995
  * Revoke a store invitation
9708
9996
  * Requires Admin mode (apiKey) and MANAGE_TEAM permission
9709
9997
  */
9710
- async revokeStoreInvitation(storeId, invitationId) {
9711
- await this.adminRequest(
9712
- "DELETE",
9713
- `/api/v1/stores/${encodePathSegment(storeId)}/team/invitations/${encodePathSegment(invitationId)}`
9714
- );
9998
+ async revokeStoreInvitation(_storeId, _invitationId) {
9999
+ return this.dashboardOnlyTeamOperation("revokeStoreInvitation");
9715
10000
  }
9716
10001
  /**
9717
10002
  * Get public invitation details by token (no auth required)
@@ -9720,7 +10005,7 @@ var _BrainerceClient = class _BrainerceClient {
9720
10005
  async getStoreInvitationByToken(token) {
9721
10006
  return this.request(
9722
10007
  "GET",
9723
- `/api/v1/store-invitations/${encodePathSegment(token)}`
10008
+ `/api/store-invitations/${encodePathSegment(token)}`
9724
10009
  );
9725
10010
  }
9726
10011
  /**
@@ -9728,9 +10013,9 @@ var _BrainerceClient = class _BrainerceClient {
9728
10013
  * Requires Admin mode (apiKey)
9729
10014
  */
9730
10015
  async acceptStoreInvitation(token) {
9731
- await this.adminRequest(
9732
- "POST",
9733
- `/api/v1/store-invitations/${encodePathSegment(token)}/accept`
10016
+ throw new BrainerceError(
10017
+ "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.",
10018
+ 403
9734
10019
  );
9735
10020
  }
9736
10021
  /**
@@ -9747,7 +10032,7 @@ var _BrainerceClient = class _BrainerceClient {
9747
10032
  * ```
9748
10033
  */
9749
10034
  async getMyStores() {
9750
- return this.adminRequest("GET", "/api/v1/me/stores");
10035
+ return this.dashboardOnlyUserContext("getMyStores");
9751
10036
  }
9752
10037
  /**
9753
10038
  * Get the current user's resolved permissions for a specific store
@@ -9761,10 +10046,23 @@ var _BrainerceClient = class _BrainerceClient {
9761
10046
  * }
9762
10047
  * ```
9763
10048
  */
9764
- async getMyStorePermissions(storeId) {
9765
- return this.adminRequest(
9766
- "GET",
9767
- `/api/v1/me/stores/${encodePathSegment(storeId)}/permissions`
10049
+ async getMyStorePermissions(_storeId) {
10050
+ return this.dashboardOnlyUserContext("getMyStorePermissions");
10051
+ }
10052
+ /**
10053
+ * `/me/*` answers "who am I and what can I reach", which only a real user can
10054
+ * ask. `UserContextController` (store-team.controller.ts:246) is guarded by
10055
+ * `DashboardOnlyGuard` for a load-bearing reason its own G16 comment spells
10056
+ * out: both routes resolve access purely from `@CurrentUserId()`, which is
10057
+ * `undefined` for an api_key principal, so the store filter would be stripped
10058
+ * and every store on the platform returned. The guard is the control. These
10059
+ * also pointed at `/api/v1/me/*`, which no controller serves, so the observed
10060
+ * failure was a 404 rather than the guard's 403.
10061
+ */
10062
+ dashboardOnlyUserContext(operation) {
10063
+ throw new BrainerceError(
10064
+ `${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.`,
10065
+ 403
9768
10066
  );
9769
10067
  }
9770
10068
  // -------------------- Email Settings & Templates (Admin) --------------------
@@ -9907,7 +10205,7 @@ var _BrainerceClient = class _BrainerceClient {
9907
10205
  * Requires Admin mode (apiKey)
9908
10206
  */
9909
10207
  async getSyncConflicts() {
9910
- return this.adminRequest("GET", "/api/v1/sync-conflicts");
10208
+ return this.syncConflictsNotImplemented("getSyncConflicts");
9911
10209
  }
9912
10210
  /**
9913
10211
  * Resolve a sync conflict
@@ -9917,12 +10215,28 @@ var _BrainerceClient = class _BrainerceClient {
9917
10215
  * @param resolution - 'MERGE' to link to existing product, 'CREATE_NEW' to create new product
9918
10216
  */
9919
10217
  async resolveSyncConflict(conflictId, resolution) {
9920
- return this.adminRequest(
9921
- "POST",
9922
- `/api/v1/sync-conflicts/${encodePathSegment(conflictId)}/resolve`,
9923
- {
9924
- resolution
9925
- }
10218
+ void conflictId;
10219
+ void resolution;
10220
+ return this.syncConflictsNotImplemented("resolveSyncConflict");
10221
+ }
10222
+ /**
10223
+ * Sync conflicts were never implemented on the server.
10224
+ *
10225
+ * There is no `sync-conflict` route anywhere in the backend — not under
10226
+ * `@Controller('v1')`, not on any other controller. These two methods have
10227
+ * called a URL that has never existed, and `SyncConflict` /
10228
+ * `SyncConflictResolution` / `ResolveSyncConflictDto` are exported types with
10229
+ * no producer. The METAFIELD conflict siblings below are real
10230
+ * (external-api.controller.ts:4788, :4802) and are easy to mistake for these.
10231
+ *
10232
+ * Kept as throwing stubs rather than deleted: removing exported methods from a
10233
+ * published package is a breaking change, and a caller who has been swallowing
10234
+ * a 404 deserves to be told why.
10235
+ */
10236
+ syncConflictsNotImplemented(operation) {
10237
+ throw new BrainerceError(
10238
+ `${operation} is not implemented: the platform exposes no sync-conflict endpoint. If you are looking for metafield sync conflicts, use getMetafieldConflicts() / resolveMetafieldConflict().`,
10239
+ 501
9926
10240
  );
9927
10241
  }
9928
10242
  // -------------------- Metafield Conflicts (Admin) --------------------
@@ -10397,9 +10711,7 @@ function validateDateAvailabilityConfig(config, fieldType, surface = "checkout")
10397
10711
  (k) => config[k] !== void 0
10398
10712
  );
10399
10713
  if (relativeKeys.length > 0) {
10400
- errors.push(
10401
- `${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`
10402
- );
10714
+ errors.push(`${relativeKeys.join("/")} only apply to checkout fields, not ${surface} fields`);
10403
10715
  }
10404
10716
  }
10405
10717
  if (config.blockedWeekdays) {
@@ -10422,7 +10734,9 @@ function validateDateAvailabilityConfig(config, fieldType, surface = "checkout")
10422
10734
  const windowsByWeekday = /* @__PURE__ */ new Map();
10423
10735
  for (const w of config.businessHours) {
10424
10736
  if (!Number.isInteger(w.weekday) || w.weekday < 0 || w.weekday > 6) {
10425
- errors.push(`businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`);
10737
+ errors.push(
10738
+ `businessHours.weekday must be an integer 0-6, got ${JSON.stringify(w.weekday)}`
10739
+ );
10426
10740
  continue;
10427
10741
  }
10428
10742
  const openValid = TIME_RE.test(w.open);
@@ -10534,7 +10848,8 @@ function addCalendarDays(dateYYYYMMDD, days) {
10534
10848
  function parseDateFieldValue(raw, fieldType, timezone) {
10535
10849
  const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
10536
10850
  const expected = fieldType === "DATE" ? "expected a calendar date in YYYY-MM-DD format" : "expected an ISO-8601 date/time such as 2026-08-13T13:00:00+03:00";
10537
- if (!str) return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
10851
+ if (!str)
10852
+ return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
10538
10853
  const dateOnly = DATE_ONLY_RE.exec(str);
10539
10854
  const dateTime = dateOnly ? null : DATE_TIME_RE.exec(str);
10540
10855
  const match = dateOnly ?? dateTime;
@@ -10623,7 +10938,14 @@ function timezoneOffsetMs(utcMillis, timezone) {
10623
10938
  }
10624
10939
  const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
10625
10940
  const hour = get("hour") === 24 ? 0 : get("hour");
10626
- const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
10941
+ const asIfUtc = Date.UTC(
10942
+ get("year"),
10943
+ get("month") - 1,
10944
+ get("day"),
10945
+ hour,
10946
+ get("minute"),
10947
+ get("second")
10948
+ );
10627
10949
  return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
10628
10950
  }
10629
10951
  function isCalendarDateAllowed(dateYYYYMMDD, config, clock) {