@tabcommerceio/buy-together-sdk 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tab commerce technologies llc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # Product Bundle SDK
2
+
3
+ `@tabcommerceio/buy-together-sdk` provides the Product Bundle storefront workflow:
4
+ Offer lookup, exact Variant re-quotes, verified **Add bundle**, and independent
5
+ Bundle event delivery. It is browser-targeted ESM with TypeScript declarations
6
+ and no runtime dependencies. It does not import or mount the Cross Sell SDK.
7
+
8
+ ## Install and choose the SDK
9
+
10
+ ```bash
11
+ npm install @tabcommerceio/buy-together-sdk
12
+ ```
13
+
14
+ Use `@tabcommerceio/cross-sell-sdk` for individual recommendations and Cross Sell
15
+ tracking. Use this package for Product Bundle Offers. An Online Store can use
16
+ both packages, with separate clients, roots, tokens, and event observers.
17
+
18
+ `1.0.0` is the first public release of the current component-based contract.
19
+ It replaces the repository's earlier experimental role-based API: components
20
+ are identified by `component_id`, and every purchasable selection carries a
21
+ short `quote_expires_at`. Code using older workspace builds must migrate all
22
+ component tuples and implement the re-quote flow below. The package name stays
23
+ `buy-together-sdk`; a product display-name change does not require an npm rename.
24
+
25
+ ## Before connecting
26
+
27
+ Use the installed shop's actual App Proxy path; the examples use the development
28
+ path `/apps/tab-cross-sell-dev`. Requests run in the shopper's Online Store
29
+ browser through Shopify's signed proxy. Do not put Admin API credentials into
30
+ browser code. Resolve and quote require the shop's `api_access` entitlement
31
+ (currently Optimize, or an otherwise eligible enabled plan).
32
+
33
+ An Offer is a peer Product set. Every member Product can be its PDP source.
34
+ The server owns eligible variants, contextual prices, totals, tokens, and the
35
+ Bundle instance identity. Never calculate a replacement quote in the browser.
36
+ The current protocol accepts two to four distinct component Products.
37
+
38
+ ## Custom UI: resolve, re-quote, add
39
+
40
+ The following controller uses public exports only. Bind your option controls to
41
+ `chooseVariants`, refresh displayed totals from its returned Offer, and enable
42
+ Add only when `canAdd()` is true. Disable option controls while Add is running.
43
+ Each selection contains **every** component tuple, including unchanged members.
44
+ Replace the example country, currency, proxy path, slot, and locale root with
45
+ current shop facts. Use `offer_ids` instead of `slot_key` when selecting explicit
46
+ Offers; never send both selectors.
47
+
48
+ ```ts
49
+ import {
50
+ init,
51
+ createQuoteRequestFence,
52
+ type BundleOffer,
53
+ type BundleQuoteSelection,
54
+ } from '@tabcommerceio/buy-together-sdk'
55
+
56
+ const client = init({ proxyBase: '/apps/tab-cross-sell-dev' })
57
+ const context = { country: 'US' }
58
+ const cart = { currency: 'USD' }
59
+ const routeRoot = '/' // Use the shop's Shopify.routes.root, e.g. '/fr/'.
60
+ const fence = createQuoteRequestFence()
61
+ const resolved = await client.resolve({ slot_key: 'summer-bundles', context, cart })
62
+ let tokenSource: BundleOffer | null = resolved?.offers[0] ?? null
63
+ let ready: BundleOffer | null = tokenSource
64
+ let adding = false
65
+ let needsCartRecovery = false
66
+
67
+ export function canAdd(): boolean {
68
+ return !adding && !needsCartRecovery && ready !== null &&
69
+ Date.parse(ready.quote_expires_at) > Date.now()
70
+ }
71
+
72
+ export async function chooseVariants(selections: BundleQuoteSelection[]) {
73
+ if (!tokenSource || adding || needsCartRecovery) return null
74
+ const tracking_token = tokenSource.tracking_token
75
+ ready = null // The previous selection cannot remain purchasable.
76
+ const attempt = fence.begin() // Aborts and invalidates the previous request.
77
+ const result = await client.quote({ tracking_token, selections, context, cart }, attempt.signal)
78
+ if (!fence.isCurrent(attempt)) return null
79
+ if (!result?.eligible || !result.offer) return null
80
+ tokenSource = result.offer
81
+ ready = result.offer
82
+ return ready // Render these server-returned totals and selected variants.
83
+ }
84
+
85
+ export async function addSelectedBundle() {
86
+ if (!canAdd() || !ready) return null
87
+ adding = true
88
+ try {
89
+ const result = await client.addAll(ready, { routeRoot })
90
+ if (result.status === 'added') {
91
+ ready = null
92
+ tokenSource = null // Resolve again before starting another Bundle instance.
93
+ } else if (result.status === 'recoverable_error') {
94
+ needsCartRecovery = true
95
+ ready = null
96
+ window.location.assign(`${routeRoot}cart`)
97
+ }
98
+ return result
99
+ } finally {
100
+ adding = false
101
+ }
102
+ }
103
+
104
+ export function destroyBundleUI() {
105
+ fence.cancel()
106
+ client.destroy()
107
+ }
108
+ ```
109
+
110
+ Read a component's eligible `variants` to construct its selected
111
+ `{ component_id, product_id, variant_id }` tuple. Pass only the latest eligible
112
+ server Offer to `addAll()`. Preserve its `bundle_instance_id` and tracking token;
113
+ never mint a replacement instance after the server has signed it. Re-resolve
114
+ when the live Offer is stale. Update country/cart facts and re-resolve when the
115
+ shopper's market or currency changes.
116
+
117
+ ### Add outcomes
118
+
119
+ | Status | Meaning and next action |
120
+ |---|---|
121
+ | `added` | Both the add response and a fresh cart read prove the exact component/property tuples. Refresh your cart UI; resolve a new instance for another add. |
122
+ | `failed` | A fresh read proves a clean no-op. Let the shopper retry only with a still-valid quote. |
123
+ | `compensated` | Partial instance lines were removed and the clean result was verified. A deliberate retry with a valid quote is safe. |
124
+ | `recoverable_error` | The cart outcome could not be proven. Stop retries and navigate to a fresh cart; do not mint another request/instance to retry blindly. |
125
+
126
+ The SDK sends one Shopify Ajax `/cart/add.js` multi-item request with the five
127
+ `_tab_bundle_*` private properties. Its compensation targets only lines from
128
+ that exact instance/token. It queues `add_to_cart` only after full cart proof;
129
+ custom consumers must not send a duplicate event. Resolve/quote return `null`
130
+ for transport failures; an ineligible quote must leave Add disabled.
131
+
132
+ ## Rendering and tracking
133
+
134
+ `resolvePDP()` returns server-rendered HTML. `mountPDP()` inserts that HTML,
135
+ fences overlapping mounts, and observes impressions. Its request requires the
136
+ currently selected source Product/Variant GIDs and a new `crypto.randomUUID()`
137
+ instance **before** requesting a signed fragment. It does not wire custom UI
138
+ option/add handlers: those remain the host's responsibility. The official
139
+ Theme App Block bundles its own interaction controller and needs no npm install.
140
+
141
+ For custom cards, use `createBundleImpressionObserver` with an observe target
142
+ containing the actual Offer's `offer_id`, `tracking_token`, and `request_id`;
143
+ forward its `onImpression` payload to `client.track`. The public observer enforces
144
+ 50% visibility for 500 ms and excludes hidden-tab time. Track clicks with the
145
+ same Offer identity. `client.flush()` drains queued events, and
146
+ `client.destroy()` releases client resources; disconnect any observer you create
147
+ separately. No Bundle root or event belongs to the Cross Sell client.
148
+
149
+ In browsers `init` uses the page's document/window for visibility and pagehide
150
+ flushes. Explicit refs are only needed for custom environments.
151
+
152
+ ## Using both packages
153
+
154
+ ```ts
155
+ import { init as initCrossSell } from '@tabcommerceio/cross-sell-sdk'
156
+ import { init as initBundles } from '@tabcommerceio/buy-together-sdk'
157
+
158
+ const proxyBase = '/apps/tab-cross-sell-dev'
159
+ const crossSell = initCrossSell({ proxyBase })
160
+ const bundles = initBundles({ proxyBase })
161
+ // Bind each client to its own cards and event identities.
162
+ // On teardown: crossSell.destroy(); bundles.destroy().
163
+ ```
164
+
165
+ The public ESM API supplies data/commerce primitives for your own markup. The
166
+ internal official Theme IIFE is built separately as
167
+ `tab-buy-together-storefront.js` and is excluded from this npm package. After a
168
+ proven official add it synchronizes the theme cart and emits
169
+ `tab:buy-together:cart-changed`; custom ESM consumers refresh their own cart UI.
170
+
171
+ ## Maintainer release
172
+
173
+ Source: `tabcommerce/tab-cross-sell`, directory `packages/web/buy-together-sdk`.
174
+ The shared workflow `publish-cross-sell-sdk.yml` publishes exact manifest
175
+ versions for both SDKs from `main` using package-scoped npm Trusted Publishing.
176
+ The initial publication requires an authenticated package owner; configure the
177
+ Bundle package's trust binding after that first publish. Local validation uses
178
+ `pnpm publish:buy-together:dry`; the authorized manual release command is
179
+ `pnpm publish:buy-together`. See `scripts/release/README.md` in the repository
180
+ for digest, authentication, tagging, and post-publication checks.
@@ -0,0 +1,335 @@
1
+ /** One neutral member Product resolved to the exact Variant for this Offer. */
2
+ type BundleComponent = {
3
+ component_id: string;
4
+ product_id: string;
5
+ variant_id: string;
6
+ };
7
+ type BundleEligibleVariant = {
8
+ variant_id: string;
9
+ title: string;
10
+ option_values: string[];
11
+ available: boolean;
12
+ price: BundleMoney;
13
+ };
14
+ /** Display snapshot returned only by the agency/API resolve route. */
15
+ type BundleResolvedComponent = BundleComponent & {
16
+ product_title: string;
17
+ image_url?: string;
18
+ options: string[];
19
+ variants: BundleEligibleVariant[];
20
+ };
21
+ type BundleMoney = {
22
+ amount: string;
23
+ currency: string;
24
+ };
25
+ /** Minimum cart identity needed to safely add one exact Bundle Offer. */
26
+ type BundleCartOffer = {
27
+ offer_id: string;
28
+ tracking_token: string;
29
+ components: BundleComponent[];
30
+ /**
31
+ * The exact instance UUID signed into tracking_token by the rendered route.
32
+ * Add bundle must use this value verbatim; it must never mint a replacement.
33
+ */
34
+ bundle_instance_id: string;
35
+ /** Server-enforced short authorization window for this exact selection. */
36
+ quote_expires_at: string;
37
+ };
38
+ type BundleCartLine = {
39
+ product_id?: string;
40
+ variant_id?: string;
41
+ quantity?: number;
42
+ };
43
+ /** Current contextual facts accepted by the Bundle storefront routes. */
44
+ type BundleCartContext = {
45
+ currency: string;
46
+ lines?: BundleCartLine[];
47
+ };
48
+ type BundleRequestContext = {
49
+ country: string;
50
+ };
51
+ /**
52
+ * Official PDP route: the selected source Product and Variant. Every member
53
+ * Product can be the source; the selected Variant determines that member's
54
+ * cart line while the server resolves first-available Variants for the other
55
+ * neutral members.
56
+ */
57
+ type BundlePDPRenderedRequest = {
58
+ source_product_id: string;
59
+ source_variant_id: string;
60
+ /** Client-generated UUID supplied before token issuance. */
61
+ bundle_instance_id: string;
62
+ context: BundleRequestContext;
63
+ cart: BundleCartContext;
64
+ };
65
+ /** Agency/API route: exactly one selector (slot_key or offer_ids). */
66
+ type BundleResolveRequest = {
67
+ slot_key: string;
68
+ offer_ids?: never;
69
+ limit?: number;
70
+ context: BundleRequestContext;
71
+ cart: BundleCartContext;
72
+ } | {
73
+ slot_key?: never;
74
+ offer_ids: string[];
75
+ limit?: number;
76
+ context: BundleRequestContext;
77
+ cart: BundleCartContext;
78
+ };
79
+ /** Fully validated agency/API resolve Offer. */
80
+ type BundleOffer = Omit<BundleCartOffer, 'components'> & {
81
+ campaign_id: string;
82
+ publish_version_id: string;
83
+ campaign_version: number;
84
+ revision: number;
85
+ position: number;
86
+ name: string;
87
+ message: string;
88
+ components: BundleResolvedComponent[];
89
+ regular_total: BundleMoney;
90
+ offer_total: BundleMoney;
91
+ savings: BundleMoney;
92
+ request_id: string;
93
+ surface: 'pdp' | 'api_slot' | 'api_offer_ids';
94
+ slot_key?: string;
95
+ };
96
+ type BundleQuoteSelection = BundleComponent;
97
+ type BundleQuoteRequest = {
98
+ /** Previously issued capability; Offer identity and surface come only from its verified claims. */
99
+ tracking_token: string;
100
+ selections: BundleQuoteSelection[];
101
+ context: BundleRequestContext;
102
+ cart: BundleCartContext;
103
+ };
104
+ type BundleQuoteResponse = {
105
+ request_id: string;
106
+ eligible: boolean;
107
+ reason?: 'stale_offer' | 'invalid_selection' | 'temporarily_unavailable';
108
+ offer?: BundleOffer;
109
+ };
110
+ type BundleResolveResponse = {
111
+ request_id: string;
112
+ offers: BundleOffer[];
113
+ };
114
+ type BundleRenderedResponse = {
115
+ html: string;
116
+ request_id: string;
117
+ };
118
+ type BundleEventType = 'impression' | 'click' | 'add_to_cart';
119
+ type BundleTrackInput = {
120
+ event_type: BundleEventType;
121
+ tracking_token: string;
122
+ offer_id: string;
123
+ request_id: string;
124
+ attrs?: Record<string, unknown>;
125
+ event_id?: string;
126
+ occurred_at?: string;
127
+ };
128
+ type BundleStorefrontEvent = Required<Pick<BundleTrackInput, 'event_id' | 'occurred_at'>> & Omit<BundleTrackInput, 'event_id' | 'occurred_at'>;
129
+ type FetchLike = (input: string, init?: {
130
+ method?: string;
131
+ headers?: Record<string, string>;
132
+ body?: string;
133
+ signal?: AbortSignal;
134
+ keepalive?: boolean;
135
+ credentials?: RequestCredentials;
136
+ }) => Promise<{
137
+ ok: boolean;
138
+ status: number;
139
+ json(): Promise<unknown>;
140
+ }>;
141
+ type CartLineLike = {
142
+ id?: string | number;
143
+ key?: string;
144
+ quantity?: number;
145
+ properties?: Record<string, unknown> | null;
146
+ };
147
+ type ObserveTarget = {
148
+ element: Element;
149
+ tracking_token: string;
150
+ offer_id: string;
151
+ request_id: string;
152
+ };
153
+ type VisibilityEvidence = {
154
+ ratio: number;
155
+ duration_ms: number;
156
+ };
157
+
158
+ /**
159
+ * `failed` is a proven clean no-op: a successful fresh cart read showed that
160
+ * nothing of this instance reached the cart, so no compensation ran.
161
+ * `compensated` means partially written lines were zeroed.
162
+ * `recoverable_error` covers every unproven outcome: instance lines whose
163
+ * cart/update.js compensation failed, and double network failures where the
164
+ * add may have been accepted but the cart read could not prove either way.
165
+ * Callers must not blind-retry a `recoverable_error` as if it were `failed`.
166
+ */
167
+ type BundleAddResult = {
168
+ status: 'added';
169
+ } | {
170
+ status: 'failed';
171
+ } | {
172
+ status: 'compensated';
173
+ } | {
174
+ status: 'recoverable_error';
175
+ };
176
+ type AddBundleToCartOptions = {
177
+ offer: BundleCartOffer;
178
+ fetchImpl?: FetchLike;
179
+ track: (input: BundleTrackInput) => void;
180
+ /** Injectable for deterministic tests; defaults to crypto.randomUUID(). */
181
+ createId?: () => string;
182
+ /** Shopify.routes.root, e.g. `/` or `/fr/`. */
183
+ routeRoot?: string;
184
+ onAdded?: () => void | Promise<void>;
185
+ /** Injectable epoch milliseconds for quote-expiry tests. */
186
+ now?: () => number;
187
+ };
188
+ /**
189
+ * Adds every exact signed component in one Shopify Ajax Cart request. It does
190
+ * not optimistically track success: the add response *and* a fresh cart read
191
+ * must both prove each exact Variant/private-property tuple.
192
+ */
193
+ declare function addBundleToCart(options: AddBundleToCartOptions): Promise<BundleAddResult>;
194
+
195
+ type BuyTogetherInitOptions = {
196
+ proxyBase: string;
197
+ fetchImpl?: FetchLike;
198
+ flushIntervalMs?: number;
199
+ flushThreshold?: number;
200
+ createEventId?: () => string;
201
+ now?: () => number;
202
+ documentRef?: Document;
203
+ windowRef?: Window;
204
+ IntersectionObserverImpl?: typeof IntersectionObserver;
205
+ };
206
+ type BundleMountPDPOptions = {
207
+ root: Element;
208
+ request: BundlePDPRenderedRequest;
209
+ signal?: AbortSignal;
210
+ };
211
+ type BuyTogetherClient = {
212
+ resolve: (request: BundleResolveRequest, signal?: AbortSignal) => Promise<BundleResolveResponse | null>;
213
+ resolvePDP: (request: BundlePDPRenderedRequest, signal?: AbortSignal) => Promise<BundleRenderedResponse | null>;
214
+ quote: (request: BundleQuoteRequest, signal?: AbortSignal) => Promise<BundleQuoteResponse | null>;
215
+ mountPDP: (options: BundleMountPDPOptions) => Promise<() => void>;
216
+ addAll: (offer: BundleCartOffer, options?: Omit<AddBundleToCartOptions, 'offer' | 'track' | 'fetchImpl'>) => Promise<BundleAddResult>;
217
+ track: (input: BundleTrackInput) => void;
218
+ flush: () => Promise<void>;
219
+ destroy: () => void;
220
+ };
221
+ /**
222
+ * The Bundle renderer has independent data attributes. Do not make a card
223
+ * trackable from Cross Sell provenance or from generic product markup.
224
+ */
225
+ declare function collectBundleObserveTargets(root: Element): ObserveTarget[];
226
+ declare function init(options: BuyTogetherInitOptions): BuyTogetherClient;
227
+
228
+ declare const MAX_BUNDLE_EVENTS_BATCH = 100;
229
+ type BundleEventQueueOptions = {
230
+ proxyBase?: string;
231
+ fetchImpl?: FetchLike;
232
+ createEventId?: () => string;
233
+ now?: () => number;
234
+ onEnqueued?: (count: number) => void;
235
+ };
236
+ /**
237
+ * Deliberately separate from the Cross Sell queue: Bundle event payloads carry
238
+ * Offer identity and are delivered only to the Bundle event route.
239
+ */
240
+ declare function createBundleEventQueue(options?: BundleEventQueueOptions): {
241
+ track: (input: BundleTrackInput) => BundleStorefrontEvent;
242
+ enqueue: (event: BundleStorefrontEvent) => void;
243
+ flush: () => Promise<void>;
244
+ pending: () => BundleStorefrontEvent[];
245
+ };
246
+
247
+ type BundleFlushControllerOptions = {
248
+ flush: () => Promise<void>;
249
+ intervalMs?: number;
250
+ threshold?: number;
251
+ setIntervalImpl?: (callback: () => void, intervalMs: number) => number | ReturnType<typeof setInterval>;
252
+ clearIntervalImpl?: (handle: number | ReturnType<typeof setInterval>) => void;
253
+ documentRef?: Document;
254
+ windowRef?: Window;
255
+ };
256
+ /** Page-lifecycle delivery coordinator owned solely by the Bundle event queue. */
257
+ declare function createBundleFlushController(options: BundleFlushControllerOptions): {
258
+ start: () => void;
259
+ stop: () => void;
260
+ flushNow: () => Promise<void>;
261
+ notifyEnqueued(count: number): void;
262
+ };
263
+
264
+ declare const BUNDLE_IMPRESSION_MIN_RATIO = 0.5;
265
+ declare const BUNDLE_IMPRESSION_MIN_DURATION_MS = 500;
266
+ type BundleImpressionPayload = {
267
+ tracking_token: string;
268
+ offer_id: string;
269
+ request_id: string;
270
+ attrs: {
271
+ visibility: VisibilityEvidence;
272
+ };
273
+ };
274
+ type BundleImpressionObserverOptions = {
275
+ onImpression: (payload: BundleImpressionPayload) => void;
276
+ IntersectionObserverImpl?: typeof IntersectionObserver;
277
+ now?: () => number;
278
+ setTimeoutImpl?: (callback: () => void, delayMs: number) => number | ReturnType<typeof setTimeout>;
279
+ clearTimeoutImpl?: (handle: number | ReturnType<typeof setTimeout>) => void;
280
+ documentRef?: Document;
281
+ };
282
+ /** Independent 50%/500ms Bundle observer, never shared with Cross Sell. */
283
+ declare function createBundleImpressionObserver(options: BundleImpressionObserverOptions): {
284
+ observe: (_items: ObserveTarget[]) => void;
285
+ disconnect: () => void;
286
+ seenTokens: () => Set<string>;
287
+ };
288
+
289
+ type PDPRequestSequencerOptions<Request, Result> = {
290
+ resolve: (request: Request, signal: AbortSignal) => Promise<Result | null>;
291
+ /** Called synchronously for every source-variant change. */
292
+ onClear: () => void;
293
+ /** Only receives results that still belong to the current request. */
294
+ onResult: (result: Result) => void;
295
+ };
296
+ /**
297
+ * Keeps PDP output tied to the currently selected variant. A variation switch
298
+ * immediately removes stale bundle content, aborts the old fetch, and fences
299
+ * late responses by monotonically increasing request generation.
300
+ */
301
+ declare function createPDPRequestSequencer<Request, Result>(options: PDPRequestSequencerOptions<Request, Result>): {
302
+ run: (request: Request) => Promise<Result | null>;
303
+ cancel: () => void;
304
+ };
305
+
306
+ type QuoteAttempt = {
307
+ revision: number;
308
+ signal: AbortSignal;
309
+ };
310
+ /** One-lane quote fence: every new selection aborts and invalidates its predecessor. */
311
+ declare function createQuoteRequestFence(): {
312
+ begin(): QuoteAttempt;
313
+ isCurrent(attempt: QuoteAttempt): boolean;
314
+ cancel(): void;
315
+ };
316
+
317
+ type ResolveOptions = {
318
+ /** App Proxy base, e.g. `/apps/tab-cross-sell-dev`. */
319
+ proxyBase: string;
320
+ fetchImpl?: FetchLike;
321
+ signal?: AbortSignal;
322
+ };
323
+ /**
324
+ * Reject malformed selector requests before they cross the App Proxy. This is
325
+ * defense-in-depth; customer-api repeats all selector validation authoritatively.
326
+ */
327
+ declare function isValidBundleResolveRequest(request: BundleResolveRequest): boolean;
328
+ /** Agency SDK resolve; failed/invalid response remains an empty integration. */
329
+ declare function resolveBundleOffers(request: BundleResolveRequest, options: ResolveOptions): Promise<BundleResolveResponse | null>;
330
+ /** Official PDP-only rendered resolve. It does not accept slots or explicit IDs. */
331
+ declare function resolveBundlePDP(request: BundlePDPRenderedRequest, options: ResolveOptions): Promise<BundleRenderedResponse | null>;
332
+ /** Re-quotes one exact selection; malformed/stale responses never become addable. */
333
+ declare function quoteBundleOffer(request: BundleQuoteRequest, options: ResolveOptions): Promise<BundleQuoteResponse | null>;
334
+
335
+ export { type AddBundleToCartOptions, BUNDLE_IMPRESSION_MIN_DURATION_MS, BUNDLE_IMPRESSION_MIN_RATIO, type BundleAddResult, type BundleCartContext, type BundleCartLine, type BundleCartOffer, type BundleComponent, type BundleEligibleVariant, type BundleEventType, type BundleMoney, type BundleMountPDPOptions, type BundleOffer, type BundlePDPRenderedRequest, type BundleQuoteRequest, type BundleQuoteResponse, type BundleQuoteSelection, type BundleRenderedResponse, type BundleRequestContext, type BundleResolveRequest, type BundleResolveResponse, type BundleResolvedComponent, type BundleStorefrontEvent, type BundleTrackInput, type BuyTogetherClient, type BuyTogetherInitOptions, type CartLineLike, type FetchLike, MAX_BUNDLE_EVENTS_BATCH, type ObserveTarget, type VisibilityEvidence, addBundleToCart, collectBundleObserveTargets, createBundleEventQueue, createBundleFlushController, createBundleImpressionObserver, createPDPRequestSequencer, createQuoteRequestFence, init, isValidBundleResolveRequest, quoteBundleOffer, resolveBundleOffers, resolveBundlePDP };