brainerce 1.52.0 → 1.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,9 +94,9 @@ Violating any of these causes production incidents or broken orders. Read them b
94
94
 
95
95
  ### Token handling
96
96
 
97
- - Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally.
97
+ - Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally. `setCustomerToken` is a plain setter — always follow it with `await client.syncCartOnLogin()`, or the shopper's guest cart is never attached to their account and identity-keyed features (first-order discounts, per-customer usage caps, abandoned-cart recovery) misbehave.
98
98
  - NEVER put the admin API key (`brainerce_*`) in client code. It is a server-only secret.
99
- - OAuth callbacks arrive with a one-time `auth_code` URL param. Call `client.exchangeOAuthCode(authCode)` to swap it for the JWT and apply via `setCustomerToken`. (The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release.)
99
+ - OAuth callbacks arrive with a one-time `auth_code` URL param. Call `client.exchangeOAuthCode(authCode)` to swap it for the JWT, apply via `setCustomerToken`, then call `client.syncCartOnLogin()` to claim the guest cart. (The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release.)
100
100
 
101
101
  ### i18n
102
102
 
@@ -167,9 +167,9 @@ These sequences are non-negotiable. The order of SDK calls matters.
167
167
  ```
168
168
  3. Branch on `result.requiresVerification`:
169
169
  - `true` → store token temporarily, route to verify-email UI (do NOT set token yet)
170
- - `false` → `client.setCustomerToken(result.token)`, route to account
170
+ - `false` → `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to account
171
171
  4. On verify-email: collect 6-digit code → `client.verifyEmail(code)`. Offer resend via `client.resendVerificationEmail()`.
172
- 5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, route to account.
172
+ 5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to account.
173
173
 
174
174
  > Build the verify-email step even if verification is currently disabled — it auto-hides.
175
175
 
@@ -182,7 +182,7 @@ These sequences are non-negotiable. The order of SDK calls matters.
182
182
  ```
183
183
  3. Branch on `result.requiresVerification`:
184
184
  - `true` → route to verify-email
185
- - `false` → `client.setCustomerToken(result.token)`, route to previous page or account
185
+ - `false` → `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to previous page or account
186
186
  4. Always offer OAuth buttons from `client.getAvailableOAuthProviders()` — render the region even when empty, it auto-populates when a provider is enabled.
187
187
  5. Render specific errors (bad credentials, rate limited, disabled) — never swallow them.
188
188
 
@@ -233,7 +233,9 @@ the credential, no customer token needed.
233
233
  const code = params.get('auth_code');
234
234
  if (code) {
235
235
  const result = await client.exchangeOAuthCode(code);
236
- client.setCustomerToken(result.token); // then redirect to account
236
+ client.setCustomerToken(result.token);
237
+ await client.syncCartOnLogin(); // REQUIRED — claims the guest cart
238
+ // then redirect to account
237
239
  }
238
240
  ```
239
241
  The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release — migrate to `auth_code` now.
@@ -717,6 +719,36 @@ const address: SetShippingAddressDto = {
717
719
  };
718
720
  ```
719
721
 
722
+ **And no coordinates.** The address endpoints validate against a strict
723
+ allow-list — one property that isn't on the DTO rejects the whole call with
724
+ `400 "property lat should not exist"`, which doesn't degrade: it blocks the
725
+ address step for every shopper. `getAddressDetails()` resolves an address
726
+ carrying `lat`, `lng` and `formattedAddress`, so this is the one that bites:
727
+
728
+ ```typescript
729
+ // ❌ WRONG — over raw HTTP, this 400s on every address
730
+ await fetch(`${base}/checkout/${id}/shipping-address`, {
731
+ method: 'PATCH',
732
+ body: JSON.stringify({ ...address, email, firstName, lastName }), // lat/lng ride along
733
+ });
734
+
735
+ // ✅ CORRECT — the SDK (>= 1.53.0) strips lat/lng/formattedAddress for you,
736
+ // so the spread is safe here. Pass `placeId`: that is what reaches zone
737
+ // matching, since the server resolves the coordinates itself and never takes
738
+ // them from the client (zone matching decides which rate gets charged).
739
+ await client.setShippingAddress(checkoutId, {
740
+ ...address,
741
+ email,
742
+ firstName,
743
+ lastName,
744
+ placeId: picked.placeId,
745
+ placeSessionToken: sessionToken,
746
+ });
747
+ ```
748
+
749
+ Use `address.lat` / `address.lng` for your own UI — a map pin, a distance
750
+ readout — and nothing else.
751
+
720
752
  ### 10. OAuth - Use `authorizationUrl`, NOT `url`
721
753
 
722
754
  ```typescript
@@ -987,6 +1019,19 @@ const picked = suggestions[0];
987
1019
  const { address, inZone } = await client.getAddressDetails(picked.placeId, sessionToken);
988
1020
  // address: { line1, city, region, postalCode, country, lat, lng, formattedAddress }
989
1021
 
1022
+ // `country` can come back as an EMPTY STRING, and so can `region`. Google
1023
+ // omits the country entirely for places whose sovereignty it declines to
1024
+ // attribute — verified live for ordinary residential addresses such as Ramat
1025
+ // Shlomo, Giv'at Ze'ev, Modi'in Ilit and Ma'ale Adumim. The API deliberately
1026
+ // does not guess one. Ask the shopper to confirm the country in that case; the
1027
+ // rest of the address (line1, city, lat/lng, formattedAddress) is still good.
1028
+ // Note this also makes `inZone` read false when the store's zones are
1029
+ // country-listed — a polygon zone can still match, since the server matches
1030
+ // against the coordinates it resolves from `placeId`.
1031
+ if (!address.country) {
1032
+ // leave your country <select> for the shopper rather than sending ''
1033
+ }
1034
+
990
1035
  // address.region is Google's own administrative-area code — usually (not
991
1036
  // guaranteed) the same ISO 3166-2 subdivision code this store's own region
992
1037
  // lists use. Validate against destinations.regions before trusting it;
@@ -995,12 +1040,19 @@ const destinations = await client.getShippingDestinations();
995
1040
  const validRegions = destinations.regions[address.country] ?? [];
996
1041
  const region = validRegions.some((r) => r.code === address.region) ? address.region : '';
997
1042
 
998
- // Pass `placeId` through. The `lat`/`lng` on `address` are for YOUR use (a map
999
- // pin, say) — the server never accepts coordinates from the client, because
1000
- // zone matching decides which shipping rate gets charged. It re-resolves the
1001
- // placeId itself (cached, so no extra billed call) and matches map-drawn
1002
- // zones against those exact coordinates instead of re-geocoding the text.
1003
- // Clear it if the shopper then edits any address field.
1043
+ // Pass `placeId` through that, not the coordinates, is what reaches zone
1044
+ // matching. The server never accepts `lat`/`lng` from the client, because zone
1045
+ // matching decides which shipping rate gets charged; it re-resolves the placeId
1046
+ // itself (cached, so no extra billed call) and matches map-drawn zones against
1047
+ // those exact coordinates instead of re-geocoding the text. Clear it if the
1048
+ // shopper then edits any address field.
1049
+ //
1050
+ // The `...address` spread below is safe even though `address` carries `lat`,
1051
+ // `lng` and `formattedAddress`: the SDK drops those three before sending. The
1052
+ // endpoint rejects unknown properties outright (`400 "property lat should not
1053
+ // exist"`), so if you call it over raw HTTP instead of the SDK, omit them
1054
+ // yourself. Use `address.lat`/`address.lng` for your own UI — a map pin, a
1055
+ // distance readout — and nothing else.
1004
1056
  await client.setShippingAddress(checkout.id, {
1005
1057
  firstName: 'John',
1006
1058
  lastName: 'Doe',
@@ -2417,7 +2469,14 @@ console.log(rates); // ShippingRate[]
2417
2469
  > address field after picking** — its coordinates describe the suggestion, not
2418
2470
  > the edited text. There is deliberately no `lat`/`lng` field: zone matching
2419
2471
  > decides which rate is charged, so coordinates are never accepted from the
2420
- > client.
2472
+ > client — the server resolves them from `placeId` itself.
2473
+ >
2474
+ > The endpoint rejects **any** unknown property with a `400` ("property lat
2475
+ > should not exist"), which blocks checkout entirely rather than degrading. So
2476
+ > that spreading `getAddressDetails().address` in here can't do that, the SDK
2477
+ > drops its `lat`, `lng` and `formattedAddress` before sending (once per client
2478
+ > it logs a `console.warn` saying so). Calling the REST endpoint directly
2479
+ > instead of through the SDK? Then omit those three yourself.
2421
2480
 
2422
2481
  > **One zone per address:** an address matches exactly ONE shipping zone and you
2423
2482
  > get only that zone's rates. When zones overlap, the lower `priority` wins;
@@ -4512,41 +4571,6 @@ const { region: resolved } = await store.getAutoRegion('DE'); // server-side, on
4512
4571
  > otherwise the checkout is charged in the store base currency. See the Checkout
4513
4572
  > section.
4514
4573
 
4515
- ### Price Lists (per-region pricing)
4516
-
4517
- Each region has a **price list** — entries that override a product's or variant's
4518
- catalog price for buyers in that region. Requires `price-lists:read` /
4519
- `price-lists:write`. Prices are in the region's currency.
4520
-
4521
- > ⚠️ **Data-entry only for now.** Region prices are **not yet applied at checkout** —
4522
- > charging a region-currency price through the store-currency cart needs FX
4523
- > conversion (a later phase). These methods let you populate the price book ahead of
4524
- > activation; until then checkout uses the catalog price.
4525
-
4526
- ```typescript
4527
- // One entry per product OR variant (variant wins). price > 0; compareAtPrice optional.
4528
- const res = await client.upsertPriceListEntries('region_eu', [
4529
- { productId: 'prod_tshirt', price: 27, compareAtPrice: 32 },
4530
- { variantId: 'var_tshirt_l', price: 29 },
4531
- ]);
4532
- res.upserted; // 2
4533
- res.warnings; // e.g. ["PL-004: compareAtPrice not greater than price …"] (non-blocking)
4534
-
4535
- // Inspect
4536
- const meta = await client.getPriceList('region_eu'); // { currency, entryCount, … }
4537
- const { data: entries } = await client.listPriceListEntries('region_eu', {
4538
- productId: 'prod_tshirt',
4539
- });
4540
-
4541
- // Bulk CSV (columns: productSku,variantSku?,price,compareAtPrice?). Unknown SKUs /
4542
- // bad prices are skipped + reported, not fatal.
4543
- const imported = await client.importPrices('region_eu', csvString);
4544
- imported.skipped; // ['row 4: product SKU "NOPE" not found', …]
4545
- const csv = await client.exportPrices('region_eu'); // round-trips with importPrices
4546
-
4547
- await client.deletePriceListEntry('region_eu', 'entry_id'); // line falls back to catalog
4548
- ```
4549
-
4550
4574
  ### Metafield Definitions
4551
4575
 
4552
4576
  ```typescript
package/dist/index.d.mts CHANGED
@@ -3188,8 +3188,12 @@ interface SetShippingAddressDto {
3188
3188
  * which is the correct behaviour once the two no longer agree.
3189
3189
  *
3190
3190
  * Note there is deliberately no `lat`/`lng` field: zone matching decides
3191
- * which shipping rate is offered and charged, so coordinates are never
3192
- * accepted from the client.
3191
+ * which shipping rate is offered and charged, so the server resolves the
3192
+ * coordinates itself from this `placeId` and never accepts them from the
3193
+ * client. You can still spread `getAddressDetails().address` in here — the
3194
+ * SDK drops its `lat`/`lng`/`formattedAddress` before sending, because the
3195
+ * endpoint rejects any unknown property with a `400` that would otherwise
3196
+ * block checkout entirely.
3193
3197
  */
3194
3198
  placeId?: string;
3195
3199
  /**
@@ -3288,11 +3292,44 @@ interface AddressDetailsResult {
3288
3292
  */
3289
3293
  region: string;
3290
3294
  postalCode: string;
3295
+ /**
3296
+ * ISO-3166-1 alpha-2, or **an empty string** — Google omits the country
3297
+ * outright for places whose sovereignty it declines to attribute, which in
3298
+ * practice includes ordinary residential addresses (verified live: Ramat
3299
+ * Shlomo, Giv'at Ze'ev, Modi'in Ilit, Ma'ale Adumim all come back with no
3300
+ * country). The API deliberately does not guess one.
3301
+ *
3302
+ * Treat it exactly like `region`: when it's empty, leave your country
3303
+ * field for the shopper to confirm rather than submitting a blank
3304
+ * `SetShippingAddressDto.country`. Everything else on the address
3305
+ * (`line1`, `city`, `lat`/`lng`, `formattedAddress`) is still valid and
3306
+ * should still be filled in.
3307
+ *
3308
+ * Note that `region` is necessarily empty too whenever this is — region
3309
+ * codes are resolved *within* a country, so with no country there is no
3310
+ * region list to match against. Prompt for both.
3311
+ */
3291
3312
  country: string;
3313
+ /**
3314
+ * Resolved coordinates, **for your own use only** — a map pin, a distance
3315
+ * readout. They are not part of any address payload: zone matching decides
3316
+ * which shipping rate is offered and charged, so the server re-resolves
3317
+ * them from `placeId` instead of trusting the client. Sending them is
3318
+ * harmless (the SDK strips `lat`/`lng`/`formattedAddress` from
3319
+ * `setShippingAddress()` / `setBillingAddress()` bodies), but pass
3320
+ * `placeId` — that is what actually reaches zone matching.
3321
+ */
3292
3322
  lat: number;
3293
3323
  lng: number;
3294
3324
  formattedAddress: string;
3295
3325
  };
3326
+ /**
3327
+ * Soft coverage hint, never a rejection. Note it can read `false` purely
3328
+ * because `country` came back empty and the store's zones are country-listed
3329
+ * — a polygon zone can still match this address, since the server matches it
3330
+ * against the coordinates it resolves from `placeId`. Show a "we'll confirm
3331
+ * by phone" banner; do not block the shopper.
3332
+ */
3296
3333
  inZone: boolean;
3297
3334
  }
3298
3335
  interface CompleteCheckoutResponse {
@@ -6303,6 +6340,14 @@ declare class BrainerceClient {
6303
6340
  private _pendingRecoverCartId;
6304
6341
  private _ga4MeasurementId;
6305
6342
  private _ga4StitchPromise;
6343
+ /**
6344
+ * Fields present on `getAddressDetails().address` that the address endpoints
6345
+ * do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
6346
+ * `{ ...address }` spread doesn't 400 the whole checkout.
6347
+ */
6348
+ private static readonly RESOLVED_ONLY_ADDRESS_FIELDS;
6349
+ /** One warning per client, not per keystroke-driven address submit. */
6350
+ private _warnedResolvedOnlyAddressFields;
6306
6351
  /** localStorage key for session cart reference (sessionToken + cartId) */
6307
6352
  private readonly SESSION_CART_KEY;
6308
6353
  /**
@@ -6376,10 +6421,19 @@ declare class BrainerceClient {
6376
6421
  * Set the customer authentication token (obtained from login/register).
6377
6422
  * Required for accessing customer-specific data in storefront mode.
6378
6423
  *
6424
+ * This is a plain setter — it authenticates subsequent requests and nothing
6425
+ * else. In particular it does NOT attach the shopper's existing guest cart to
6426
+ * their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
6427
+ * stays anonymous and every feature keyed on buyer identity degrades quietly:
6428
+ * "first order only" discounts keep applying to returning customers,
6429
+ * per-customer usage caps stop being enforced at cart time, and abandoned-cart
6430
+ * recovery can't identify who to email.
6431
+ *
6379
6432
  * @example
6380
6433
  * ```typescript
6381
6434
  * const auth = await client.loginCustomer('user@example.com', 'password');
6382
6435
  * client.setCustomerToken(auth.token);
6436
+ * await client.syncCartOnLogin(); // claim the guest cart for this account
6383
6437
  *
6384
6438
  * // Now can access customer data
6385
6439
  * const profile = await client.getMyProfile();
@@ -6525,6 +6579,24 @@ declare class BrainerceClient {
6525
6579
  * called, or if it hasn't resolved any ids by the time this is awaited.
6526
6580
  */
6527
6581
  private withAnalyticsStitchIds;
6582
+ /**
6583
+ * Drop the fields `getAddressDetails()` returns that no address endpoint
6584
+ * accepts, so spreading its `address` straight into `setShippingAddress()`
6585
+ * / `setBillingAddress()` works instead of failing the whole request.
6586
+ *
6587
+ * The address endpoints validate against a strict allow-list: ONE unknown
6588
+ * property rejects the call with `400 "property lat should not exist"`, and
6589
+ * the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
6590
+ * the only realistic way to hit that — they come out of this SDK's own
6591
+ * resolved-address shape, so this SDK cleans up after itself rather than
6592
+ * making every storefront remember to. Nothing else is stripped: a genuine
6593
+ * typo still reaches the server and still fails loudly.
6594
+ *
6595
+ * Coordinates are dropped rather than forwarded because zone matching picks
6596
+ * which shipping rate is offered and charged — the server resolves them
6597
+ * itself from `placeId`, and never takes them from the caller.
6598
+ */
6599
+ private stripResolvedOnlyAddressFields;
6528
6600
  /**
6529
6601
  * Get a list of products with pagination and filtering
6530
6602
  * Works in vibe-coded, storefront (public), and admin mode
@@ -7643,6 +7715,12 @@ declare class BrainerceClient {
7643
7715
  * if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
7644
7716
  * const result = await client.exchangeOAuthCode(params.get('auth_code')!);
7645
7717
  * client.setCustomerToken(result.token);
7718
+ * // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
7719
+ * // guest cart to the account. Without this call the cart stays anonymous,
7720
+ * // and anything keyed on the buyer's identity misbehaves: "first order
7721
+ * // only" discounts re-apply to returning customers, per-customer usage
7722
+ * // caps go unenforced, and abandoned-cart recovery can't reach them.
7723
+ * await client.syncCartOnLogin();
7646
7724
  * // result.customer, result.isNewCustomer, result.redirectUrl, ...
7647
7725
  * } else if (params.get('oauth_error')) {
7648
7726
  * // Failures land on this same page, on `redirectUrl` — never on the API
@@ -7677,6 +7755,11 @@ declare class BrainerceClient {
7677
7755
  *
7678
7756
  * @param authCode - The single-use code from the `?auth_code=` URL param.
7679
7757
  *
7758
+ * Always follow a successful exchange with `syncCartOnLogin()`. Storing the
7759
+ * token does not claim the guest cart, and an unclaimed cart has no buyer
7760
+ * identity — which silently breaks first-order discounts, per-customer usage
7761
+ * caps, and abandoned-cart recovery for everyone who signs in with OAuth.
7762
+ *
7680
7763
  * @example
7681
7764
  * ```typescript
7682
7765
  * const params = new URLSearchParams(window.location.search);
@@ -7685,6 +7768,7 @@ declare class BrainerceClient {
7685
7768
  * const { token, customer, isNewCustomer, redirectUrl } =
7686
7769
  * await client.exchangeOAuthCode(code);
7687
7770
  * client.setCustomerToken(token);
7771
+ * await client.syncCartOnLogin(); // attach the guest cart to the account
7688
7772
  * }
7689
7773
  * ```
7690
7774
  */
@@ -8840,6 +8924,11 @@ declare class BrainerceClient {
8840
8924
  * address text — which is materially less accurate and can place the
8841
8925
  * shopper in a neighbouring city's zone, or in none at all.
8842
8926
  *
8927
+ * Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
8928
+ * and `formattedAddress` are dropped before the request goes out (the
8929
+ * endpoint rejects unknown properties outright, and coordinates are never
8930
+ * taken from the client — the server resolves them from `placeId`).
8931
+ *
8843
8932
  * @example
8844
8933
  * ```typescript
8845
8934
  * const { checkout, rates } = await client.setShippingAddress('checkout_123', {
@@ -11037,7 +11126,7 @@ declare class BrainerceError extends Error {
11037
11126
  constructor(message: string, statusCode: number, details?: unknown);
11038
11127
  }
11039
11128
 
11040
- declare const SDK_VERSION = "1.52.0";
11129
+ declare const SDK_VERSION = "1.53.1";
11041
11130
 
11042
11131
  /**
11043
11132
  * Verify a webhook signature from Brainerce
package/dist/index.d.ts CHANGED
@@ -3188,8 +3188,12 @@ interface SetShippingAddressDto {
3188
3188
  * which is the correct behaviour once the two no longer agree.
3189
3189
  *
3190
3190
  * Note there is deliberately no `lat`/`lng` field: zone matching decides
3191
- * which shipping rate is offered and charged, so coordinates are never
3192
- * accepted from the client.
3191
+ * which shipping rate is offered and charged, so the server resolves the
3192
+ * coordinates itself from this `placeId` and never accepts them from the
3193
+ * client. You can still spread `getAddressDetails().address` in here — the
3194
+ * SDK drops its `lat`/`lng`/`formattedAddress` before sending, because the
3195
+ * endpoint rejects any unknown property with a `400` that would otherwise
3196
+ * block checkout entirely.
3193
3197
  */
3194
3198
  placeId?: string;
3195
3199
  /**
@@ -3288,11 +3292,44 @@ interface AddressDetailsResult {
3288
3292
  */
3289
3293
  region: string;
3290
3294
  postalCode: string;
3295
+ /**
3296
+ * ISO-3166-1 alpha-2, or **an empty string** — Google omits the country
3297
+ * outright for places whose sovereignty it declines to attribute, which in
3298
+ * practice includes ordinary residential addresses (verified live: Ramat
3299
+ * Shlomo, Giv'at Ze'ev, Modi'in Ilit, Ma'ale Adumim all come back with no
3300
+ * country). The API deliberately does not guess one.
3301
+ *
3302
+ * Treat it exactly like `region`: when it's empty, leave your country
3303
+ * field for the shopper to confirm rather than submitting a blank
3304
+ * `SetShippingAddressDto.country`. Everything else on the address
3305
+ * (`line1`, `city`, `lat`/`lng`, `formattedAddress`) is still valid and
3306
+ * should still be filled in.
3307
+ *
3308
+ * Note that `region` is necessarily empty too whenever this is — region
3309
+ * codes are resolved *within* a country, so with no country there is no
3310
+ * region list to match against. Prompt for both.
3311
+ */
3291
3312
  country: string;
3313
+ /**
3314
+ * Resolved coordinates, **for your own use only** — a map pin, a distance
3315
+ * readout. They are not part of any address payload: zone matching decides
3316
+ * which shipping rate is offered and charged, so the server re-resolves
3317
+ * them from `placeId` instead of trusting the client. Sending them is
3318
+ * harmless (the SDK strips `lat`/`lng`/`formattedAddress` from
3319
+ * `setShippingAddress()` / `setBillingAddress()` bodies), but pass
3320
+ * `placeId` — that is what actually reaches zone matching.
3321
+ */
3292
3322
  lat: number;
3293
3323
  lng: number;
3294
3324
  formattedAddress: string;
3295
3325
  };
3326
+ /**
3327
+ * Soft coverage hint, never a rejection. Note it can read `false` purely
3328
+ * because `country` came back empty and the store's zones are country-listed
3329
+ * — a polygon zone can still match this address, since the server matches it
3330
+ * against the coordinates it resolves from `placeId`. Show a "we'll confirm
3331
+ * by phone" banner; do not block the shopper.
3332
+ */
3296
3333
  inZone: boolean;
3297
3334
  }
3298
3335
  interface CompleteCheckoutResponse {
@@ -6303,6 +6340,14 @@ declare class BrainerceClient {
6303
6340
  private _pendingRecoverCartId;
6304
6341
  private _ga4MeasurementId;
6305
6342
  private _ga4StitchPromise;
6343
+ /**
6344
+ * Fields present on `getAddressDetails().address` that the address endpoints
6345
+ * do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
6346
+ * `{ ...address }` spread doesn't 400 the whole checkout.
6347
+ */
6348
+ private static readonly RESOLVED_ONLY_ADDRESS_FIELDS;
6349
+ /** One warning per client, not per keystroke-driven address submit. */
6350
+ private _warnedResolvedOnlyAddressFields;
6306
6351
  /** localStorage key for session cart reference (sessionToken + cartId) */
6307
6352
  private readonly SESSION_CART_KEY;
6308
6353
  /**
@@ -6376,10 +6421,19 @@ declare class BrainerceClient {
6376
6421
  * Set the customer authentication token (obtained from login/register).
6377
6422
  * Required for accessing customer-specific data in storefront mode.
6378
6423
  *
6424
+ * This is a plain setter — it authenticates subsequent requests and nothing
6425
+ * else. In particular it does NOT attach the shopper's existing guest cart to
6426
+ * their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
6427
+ * stays anonymous and every feature keyed on buyer identity degrades quietly:
6428
+ * "first order only" discounts keep applying to returning customers,
6429
+ * per-customer usage caps stop being enforced at cart time, and abandoned-cart
6430
+ * recovery can't identify who to email.
6431
+ *
6379
6432
  * @example
6380
6433
  * ```typescript
6381
6434
  * const auth = await client.loginCustomer('user@example.com', 'password');
6382
6435
  * client.setCustomerToken(auth.token);
6436
+ * await client.syncCartOnLogin(); // claim the guest cart for this account
6383
6437
  *
6384
6438
  * // Now can access customer data
6385
6439
  * const profile = await client.getMyProfile();
@@ -6525,6 +6579,24 @@ declare class BrainerceClient {
6525
6579
  * called, or if it hasn't resolved any ids by the time this is awaited.
6526
6580
  */
6527
6581
  private withAnalyticsStitchIds;
6582
+ /**
6583
+ * Drop the fields `getAddressDetails()` returns that no address endpoint
6584
+ * accepts, so spreading its `address` straight into `setShippingAddress()`
6585
+ * / `setBillingAddress()` works instead of failing the whole request.
6586
+ *
6587
+ * The address endpoints validate against a strict allow-list: ONE unknown
6588
+ * property rejects the call with `400 "property lat should not exist"`, and
6589
+ * the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
6590
+ * the only realistic way to hit that — they come out of this SDK's own
6591
+ * resolved-address shape, so this SDK cleans up after itself rather than
6592
+ * making every storefront remember to. Nothing else is stripped: a genuine
6593
+ * typo still reaches the server and still fails loudly.
6594
+ *
6595
+ * Coordinates are dropped rather than forwarded because zone matching picks
6596
+ * which shipping rate is offered and charged — the server resolves them
6597
+ * itself from `placeId`, and never takes them from the caller.
6598
+ */
6599
+ private stripResolvedOnlyAddressFields;
6528
6600
  /**
6529
6601
  * Get a list of products with pagination and filtering
6530
6602
  * Works in vibe-coded, storefront (public), and admin mode
@@ -7643,6 +7715,12 @@ declare class BrainerceClient {
7643
7715
  * if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
7644
7716
  * const result = await client.exchangeOAuthCode(params.get('auth_code')!);
7645
7717
  * client.setCustomerToken(result.token);
7718
+ * // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
7719
+ * // guest cart to the account. Without this call the cart stays anonymous,
7720
+ * // and anything keyed on the buyer's identity misbehaves: "first order
7721
+ * // only" discounts re-apply to returning customers, per-customer usage
7722
+ * // caps go unenforced, and abandoned-cart recovery can't reach them.
7723
+ * await client.syncCartOnLogin();
7646
7724
  * // result.customer, result.isNewCustomer, result.redirectUrl, ...
7647
7725
  * } else if (params.get('oauth_error')) {
7648
7726
  * // Failures land on this same page, on `redirectUrl` — never on the API
@@ -7677,6 +7755,11 @@ declare class BrainerceClient {
7677
7755
  *
7678
7756
  * @param authCode - The single-use code from the `?auth_code=` URL param.
7679
7757
  *
7758
+ * Always follow a successful exchange with `syncCartOnLogin()`. Storing the
7759
+ * token does not claim the guest cart, and an unclaimed cart has no buyer
7760
+ * identity — which silently breaks first-order discounts, per-customer usage
7761
+ * caps, and abandoned-cart recovery for everyone who signs in with OAuth.
7762
+ *
7680
7763
  * @example
7681
7764
  * ```typescript
7682
7765
  * const params = new URLSearchParams(window.location.search);
@@ -7685,6 +7768,7 @@ declare class BrainerceClient {
7685
7768
  * const { token, customer, isNewCustomer, redirectUrl } =
7686
7769
  * await client.exchangeOAuthCode(code);
7687
7770
  * client.setCustomerToken(token);
7771
+ * await client.syncCartOnLogin(); // attach the guest cart to the account
7688
7772
  * }
7689
7773
  * ```
7690
7774
  */
@@ -8840,6 +8924,11 @@ declare class BrainerceClient {
8840
8924
  * address text — which is materially less accurate and can place the
8841
8925
  * shopper in a neighbouring city's zone, or in none at all.
8842
8926
  *
8927
+ * Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
8928
+ * and `formattedAddress` are dropped before the request goes out (the
8929
+ * endpoint rejects unknown properties outright, and coordinates are never
8930
+ * taken from the client — the server resolves them from `placeId`).
8931
+ *
8843
8932
  * @example
8844
8933
  * ```typescript
8845
8934
  * const { checkout, rates } = await client.setShippingAddress('checkout_123', {
@@ -11037,7 +11126,7 @@ declare class BrainerceError extends Error {
11037
11126
  constructor(message: string, statusCode: number, details?: unknown);
11038
11127
  }
11039
11128
 
11040
- declare const SDK_VERSION = "1.52.0";
11129
+ declare const SDK_VERSION = "1.53.1";
11041
11130
 
11042
11131
  /**
11043
11132
  * Verify a webhook signature from Brainerce
package/dist/index.js CHANGED
@@ -201,7 +201,7 @@ function isDevGuardsEnabled() {
201
201
  }
202
202
 
203
203
  // src/version.ts
204
- var SDK_VERSION = "1.52.0";
204
+ var SDK_VERSION = "1.53.1";
205
205
 
206
206
  // src/client.ts
207
207
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -241,7 +241,7 @@ function parseRetryAfterMs(response) {
241
241
  function sleep(ms) {
242
242
  return new Promise((resolve) => setTimeout(resolve, ms));
243
243
  }
244
- var BrainerceClient = class {
244
+ var _BrainerceClient = class _BrainerceClient {
245
245
  constructor(options) {
246
246
  this.customerToken = null;
247
247
  this.customerCartId = null;
@@ -260,6 +260,8 @@ var BrainerceClient = class {
260
260
  // GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
261
261
  this._ga4MeasurementId = null;
262
262
  this._ga4StitchPromise = null;
263
+ /** One warning per client, not per keystroke-driven address submit. */
264
+ this._warnedResolvedOnlyAddressFields = false;
263
265
  /** localStorage key for session cart reference (sessionToken + cartId) */
264
266
  this.SESSION_CART_KEY = "brainerce_session";
265
267
  /**
@@ -773,10 +775,19 @@ var BrainerceClient = class {
773
775
  * Set the customer authentication token (obtained from login/register).
774
776
  * Required for accessing customer-specific data in storefront mode.
775
777
  *
778
+ * This is a plain setter — it authenticates subsequent requests and nothing
779
+ * else. In particular it does NOT attach the shopper's existing guest cart to
780
+ * their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
781
+ * stays anonymous and every feature keyed on buyer identity degrades quietly:
782
+ * "first order only" discounts keep applying to returning customers,
783
+ * per-customer usage caps stop being enforced at cart time, and abandoned-cart
784
+ * recovery can't identify who to email.
785
+ *
776
786
  * @example
777
787
  * ```typescript
778
788
  * const auth = await client.loginCustomer('user@example.com', 'password');
779
789
  * client.setCustomerToken(auth.token);
790
+ * await client.syncCartOnLogin(); // claim the guest cart for this account
780
791
  *
781
792
  * // Now can access customer data
782
793
  * const profile = await client.getMyProfile();
@@ -1290,6 +1301,39 @@ var BrainerceClient = class {
1290
1301
  return dto;
1291
1302
  }
1292
1303
  }
1304
+ /**
1305
+ * Drop the fields `getAddressDetails()` returns that no address endpoint
1306
+ * accepts, so spreading its `address` straight into `setShippingAddress()`
1307
+ * / `setBillingAddress()` works instead of failing the whole request.
1308
+ *
1309
+ * The address endpoints validate against a strict allow-list: ONE unknown
1310
+ * property rejects the call with `400 "property lat should not exist"`, and
1311
+ * the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
1312
+ * the only realistic way to hit that — they come out of this SDK's own
1313
+ * resolved-address shape, so this SDK cleans up after itself rather than
1314
+ * making every storefront remember to. Nothing else is stripped: a genuine
1315
+ * typo still reaches the server and still fails loudly.
1316
+ *
1317
+ * Coordinates are dropped rather than forwarded because zone matching picks
1318
+ * which shipping rate is offered and charged — the server resolves them
1319
+ * itself from `placeId`, and never takes them from the caller.
1320
+ */
1321
+ stripResolvedOnlyAddressFields(address) {
1322
+ if (!address || typeof address !== "object") return address;
1323
+ const present = _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS.filter(
1324
+ (field) => field in address
1325
+ );
1326
+ if (present.length === 0) return address;
1327
+ const cleaned = { ...address };
1328
+ for (const field of present) delete cleaned[field];
1329
+ if (!this._warnedResolvedOnlyAddressFields) {
1330
+ this._warnedResolvedOnlyAddressFields = true;
1331
+ console.warn(
1332
+ `BrainerceClient: dropped ${present.join("/")} from the address payload \u2014 the API does not accept coordinates from the client. Pass \`placeId\` (and \`placeSessionToken\`) instead so the server resolves them itself and matches map-drawn delivery zones against the exact location.`
1333
+ );
1334
+ }
1335
+ return cleaned;
1336
+ }
1293
1337
  // -------------------- Products --------------------
1294
1338
  /**
1295
1339
  * Get a list of products with pagination and filtering
@@ -2882,6 +2926,12 @@ var BrainerceClient = class {
2882
2926
  * if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
2883
2927
  * const result = await client.exchangeOAuthCode(params.get('auth_code')!);
2884
2928
  * client.setCustomerToken(result.token);
2929
+ * // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
2930
+ * // guest cart to the account. Without this call the cart stays anonymous,
2931
+ * // and anything keyed on the buyer's identity misbehaves: "first order
2932
+ * // only" discounts re-apply to returning customers, per-customer usage
2933
+ * // caps go unenforced, and abandoned-cart recovery can't reach them.
2934
+ * await client.syncCartOnLogin();
2885
2935
  * // result.customer, result.isNewCustomer, result.redirectUrl, ...
2886
2936
  * } else if (params.get('oauth_error')) {
2887
2937
  * // Failures land on this same page, on `redirectUrl` — never on the API
@@ -2934,6 +2984,11 @@ var BrainerceClient = class {
2934
2984
  *
2935
2985
  * @param authCode - The single-use code from the `?auth_code=` URL param.
2936
2986
  *
2987
+ * Always follow a successful exchange with `syncCartOnLogin()`. Storing the
2988
+ * token does not claim the guest cart, and an unclaimed cart has no buyer
2989
+ * identity — which silently breaks first-order discounts, per-customer usage
2990
+ * caps, and abandoned-cart recovery for everyone who signs in with OAuth.
2991
+ *
2937
2992
  * @example
2938
2993
  * ```typescript
2939
2994
  * const params = new URLSearchParams(window.location.search);
@@ -2942,6 +2997,7 @@ var BrainerceClient = class {
2942
2997
  * const { token, customer, isNewCustomer, redirectUrl } =
2943
2998
  * await client.exchangeOAuthCode(code);
2944
2999
  * client.setCustomerToken(token);
3000
+ * await client.syncCartOnLogin(); // attach the guest cart to the account
2945
3001
  * }
2946
3002
  * ```
2947
3003
  */
@@ -5121,6 +5177,11 @@ var BrainerceClient = class {
5121
5177
  * address text — which is materially less accurate and can place the
5122
5178
  * shopper in a neighbouring city's zone, or in none at all.
5123
5179
  *
5180
+ * Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
5181
+ * and `formattedAddress` are dropped before the request goes out (the
5182
+ * endpoint rejects unknown properties outright, and coordinates are never
5183
+ * taken from the client — the server resolves them from `placeId`).
5184
+ *
5124
5185
  * @example
5125
5186
  * ```typescript
5126
5187
  * const { checkout, rates } = await client.setShippingAddress('checkout_123', {
@@ -5140,7 +5201,9 @@ var BrainerceClient = class {
5140
5201
  * ```
5141
5202
  */
5142
5203
  async setShippingAddress(checkoutId, address) {
5143
- const body = await this.withAnalyticsStitchIds(address);
5204
+ const body = await this.withAnalyticsStitchIds(
5205
+ this.stripResolvedOnlyAddressFields(address)
5206
+ );
5144
5207
  if (this.isVibeCodedMode()) {
5145
5208
  return this.vibeCodedRequest(
5146
5209
  "PATCH",
@@ -5453,24 +5516,25 @@ var BrainerceClient = class {
5453
5516
  * ```
5454
5517
  */
5455
5518
  async setBillingAddress(checkoutId, address) {
5519
+ const body = this.stripResolvedOnlyAddressFields(address);
5456
5520
  if (this.isVibeCodedMode()) {
5457
5521
  return this.vibeCodedRequest(
5458
5522
  "PATCH",
5459
5523
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5460
- address
5524
+ body
5461
5525
  );
5462
5526
  }
5463
5527
  if (this.storeId && !this.apiKey) {
5464
5528
  return this.storefrontRequest(
5465
5529
  "PATCH",
5466
5530
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5467
- address
5531
+ body
5468
5532
  );
5469
5533
  }
5470
5534
  return this.adminRequest(
5471
5535
  "PATCH",
5472
5536
  `/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5473
- address
5537
+ body
5474
5538
  );
5475
5539
  }
5476
5540
  /**
@@ -6436,7 +6500,7 @@ var BrainerceClient = class {
6436
6500
  const result = await this.vibeCodedRequest(
6437
6501
  "PATCH",
6438
6502
  `/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
6439
- data.shippingAddress
6503
+ this.stripResolvedOnlyAddressFields(data.shippingAddress)
6440
6504
  );
6441
6505
  checkout = result.checkout;
6442
6506
  }
@@ -6444,7 +6508,7 @@ var BrainerceClient = class {
6444
6508
  checkout = await this.vibeCodedRequest(
6445
6509
  "PATCH",
6446
6510
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
6447
- data.billingAddress
6511
+ this.stripResolvedOnlyAddressFields(data.billingAddress)
6448
6512
  );
6449
6513
  }
6450
6514
  if (!checkout) {
@@ -9285,6 +9349,17 @@ var BrainerceClient = class {
9285
9349
  );
9286
9350
  }
9287
9351
  };
9352
+ /**
9353
+ * Fields present on `getAddressDetails().address` that the address endpoints
9354
+ * do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
9355
+ * `{ ...address }` spread doesn't 400 the whole checkout.
9356
+ */
9357
+ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
9358
+ "lat",
9359
+ "lng",
9360
+ "formattedAddress"
9361
+ ];
9362
+ var BrainerceClient = _BrainerceClient;
9288
9363
  var BrainerceError = class extends Error {
9289
9364
  constructor(message, statusCode, details) {
9290
9365
  super(message);
package/dist/index.mjs CHANGED
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "1.52.0";
118
+ var SDK_VERSION = "1.53.1";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -155,7 +155,7 @@ function parseRetryAfterMs(response) {
155
155
  function sleep(ms) {
156
156
  return new Promise((resolve) => setTimeout(resolve, ms));
157
157
  }
158
- var BrainerceClient = class {
158
+ var _BrainerceClient = class _BrainerceClient {
159
159
  constructor(options) {
160
160
  this.customerToken = null;
161
161
  this.customerCartId = null;
@@ -174,6 +174,8 @@ var BrainerceClient = class {
174
174
  // GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
175
175
  this._ga4MeasurementId = null;
176
176
  this._ga4StitchPromise = null;
177
+ /** One warning per client, not per keystroke-driven address submit. */
178
+ this._warnedResolvedOnlyAddressFields = false;
177
179
  /** localStorage key for session cart reference (sessionToken + cartId) */
178
180
  this.SESSION_CART_KEY = "brainerce_session";
179
181
  /**
@@ -687,10 +689,19 @@ var BrainerceClient = class {
687
689
  * Set the customer authentication token (obtained from login/register).
688
690
  * Required for accessing customer-specific data in storefront mode.
689
691
  *
692
+ * This is a plain setter — it authenticates subsequent requests and nothing
693
+ * else. In particular it does NOT attach the shopper's existing guest cart to
694
+ * their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
695
+ * stays anonymous and every feature keyed on buyer identity degrades quietly:
696
+ * "first order only" discounts keep applying to returning customers,
697
+ * per-customer usage caps stop being enforced at cart time, and abandoned-cart
698
+ * recovery can't identify who to email.
699
+ *
690
700
  * @example
691
701
  * ```typescript
692
702
  * const auth = await client.loginCustomer('user@example.com', 'password');
693
703
  * client.setCustomerToken(auth.token);
704
+ * await client.syncCartOnLogin(); // claim the guest cart for this account
694
705
  *
695
706
  * // Now can access customer data
696
707
  * const profile = await client.getMyProfile();
@@ -1204,6 +1215,39 @@ var BrainerceClient = class {
1204
1215
  return dto;
1205
1216
  }
1206
1217
  }
1218
+ /**
1219
+ * Drop the fields `getAddressDetails()` returns that no address endpoint
1220
+ * accepts, so spreading its `address` straight into `setShippingAddress()`
1221
+ * / `setBillingAddress()` works instead of failing the whole request.
1222
+ *
1223
+ * The address endpoints validate against a strict allow-list: ONE unknown
1224
+ * property rejects the call with `400 "property lat should not exist"`, and
1225
+ * the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
1226
+ * the only realistic way to hit that — they come out of this SDK's own
1227
+ * resolved-address shape, so this SDK cleans up after itself rather than
1228
+ * making every storefront remember to. Nothing else is stripped: a genuine
1229
+ * typo still reaches the server and still fails loudly.
1230
+ *
1231
+ * Coordinates are dropped rather than forwarded because zone matching picks
1232
+ * which shipping rate is offered and charged — the server resolves them
1233
+ * itself from `placeId`, and never takes them from the caller.
1234
+ */
1235
+ stripResolvedOnlyAddressFields(address) {
1236
+ if (!address || typeof address !== "object") return address;
1237
+ const present = _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS.filter(
1238
+ (field) => field in address
1239
+ );
1240
+ if (present.length === 0) return address;
1241
+ const cleaned = { ...address };
1242
+ for (const field of present) delete cleaned[field];
1243
+ if (!this._warnedResolvedOnlyAddressFields) {
1244
+ this._warnedResolvedOnlyAddressFields = true;
1245
+ console.warn(
1246
+ `BrainerceClient: dropped ${present.join("/")} from the address payload \u2014 the API does not accept coordinates from the client. Pass \`placeId\` (and \`placeSessionToken\`) instead so the server resolves them itself and matches map-drawn delivery zones against the exact location.`
1247
+ );
1248
+ }
1249
+ return cleaned;
1250
+ }
1207
1251
  // -------------------- Products --------------------
1208
1252
  /**
1209
1253
  * Get a list of products with pagination and filtering
@@ -2796,6 +2840,12 @@ var BrainerceClient = class {
2796
2840
  * if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
2797
2841
  * const result = await client.exchangeOAuthCode(params.get('auth_code')!);
2798
2842
  * client.setCustomerToken(result.token);
2843
+ * // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
2844
+ * // guest cart to the account. Without this call the cart stays anonymous,
2845
+ * // and anything keyed on the buyer's identity misbehaves: "first order
2846
+ * // only" discounts re-apply to returning customers, per-customer usage
2847
+ * // caps go unenforced, and abandoned-cart recovery can't reach them.
2848
+ * await client.syncCartOnLogin();
2799
2849
  * // result.customer, result.isNewCustomer, result.redirectUrl, ...
2800
2850
  * } else if (params.get('oauth_error')) {
2801
2851
  * // Failures land on this same page, on `redirectUrl` — never on the API
@@ -2848,6 +2898,11 @@ var BrainerceClient = class {
2848
2898
  *
2849
2899
  * @param authCode - The single-use code from the `?auth_code=` URL param.
2850
2900
  *
2901
+ * Always follow a successful exchange with `syncCartOnLogin()`. Storing the
2902
+ * token does not claim the guest cart, and an unclaimed cart has no buyer
2903
+ * identity — which silently breaks first-order discounts, per-customer usage
2904
+ * caps, and abandoned-cart recovery for everyone who signs in with OAuth.
2905
+ *
2851
2906
  * @example
2852
2907
  * ```typescript
2853
2908
  * const params = new URLSearchParams(window.location.search);
@@ -2856,6 +2911,7 @@ var BrainerceClient = class {
2856
2911
  * const { token, customer, isNewCustomer, redirectUrl } =
2857
2912
  * await client.exchangeOAuthCode(code);
2858
2913
  * client.setCustomerToken(token);
2914
+ * await client.syncCartOnLogin(); // attach the guest cart to the account
2859
2915
  * }
2860
2916
  * ```
2861
2917
  */
@@ -5035,6 +5091,11 @@ var BrainerceClient = class {
5035
5091
  * address text — which is materially less accurate and can place the
5036
5092
  * shopper in a neighbouring city's zone, or in none at all.
5037
5093
  *
5094
+ * Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
5095
+ * and `formattedAddress` are dropped before the request goes out (the
5096
+ * endpoint rejects unknown properties outright, and coordinates are never
5097
+ * taken from the client — the server resolves them from `placeId`).
5098
+ *
5038
5099
  * @example
5039
5100
  * ```typescript
5040
5101
  * const { checkout, rates } = await client.setShippingAddress('checkout_123', {
@@ -5054,7 +5115,9 @@ var BrainerceClient = class {
5054
5115
  * ```
5055
5116
  */
5056
5117
  async setShippingAddress(checkoutId, address) {
5057
- const body = await this.withAnalyticsStitchIds(address);
5118
+ const body = await this.withAnalyticsStitchIds(
5119
+ this.stripResolvedOnlyAddressFields(address)
5120
+ );
5058
5121
  if (this.isVibeCodedMode()) {
5059
5122
  return this.vibeCodedRequest(
5060
5123
  "PATCH",
@@ -5367,24 +5430,25 @@ var BrainerceClient = class {
5367
5430
  * ```
5368
5431
  */
5369
5432
  async setBillingAddress(checkoutId, address) {
5433
+ const body = this.stripResolvedOnlyAddressFields(address);
5370
5434
  if (this.isVibeCodedMode()) {
5371
5435
  return this.vibeCodedRequest(
5372
5436
  "PATCH",
5373
5437
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5374
- address
5438
+ body
5375
5439
  );
5376
5440
  }
5377
5441
  if (this.storeId && !this.apiKey) {
5378
5442
  return this.storefrontRequest(
5379
5443
  "PATCH",
5380
5444
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5381
- address
5445
+ body
5382
5446
  );
5383
5447
  }
5384
5448
  return this.adminRequest(
5385
5449
  "PATCH",
5386
5450
  `/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
5387
- address
5451
+ body
5388
5452
  );
5389
5453
  }
5390
5454
  /**
@@ -6350,7 +6414,7 @@ var BrainerceClient = class {
6350
6414
  const result = await this.vibeCodedRequest(
6351
6415
  "PATCH",
6352
6416
  `/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
6353
- data.shippingAddress
6417
+ this.stripResolvedOnlyAddressFields(data.shippingAddress)
6354
6418
  );
6355
6419
  checkout = result.checkout;
6356
6420
  }
@@ -6358,7 +6422,7 @@ var BrainerceClient = class {
6358
6422
  checkout = await this.vibeCodedRequest(
6359
6423
  "PATCH",
6360
6424
  `/checkout/${encodePathSegment(checkoutId)}/billing-address`,
6361
- data.billingAddress
6425
+ this.stripResolvedOnlyAddressFields(data.billingAddress)
6362
6426
  );
6363
6427
  }
6364
6428
  if (!checkout) {
@@ -9199,6 +9263,17 @@ var BrainerceClient = class {
9199
9263
  );
9200
9264
  }
9201
9265
  };
9266
+ /**
9267
+ * Fields present on `getAddressDetails().address` that the address endpoints
9268
+ * do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
9269
+ * `{ ...address }` spread doesn't 400 the whole checkout.
9270
+ */
9271
+ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
9272
+ "lat",
9273
+ "lng",
9274
+ "formattedAddress"
9275
+ ];
9276
+ var BrainerceClient = _BrainerceClient;
9202
9277
  var BrainerceError = class extends Error {
9203
9278
  constructor(message, statusCode, details) {
9204
9279
  super(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.52.0",
3
+ "version": "1.53.1",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",