npm package: @kitenzo/core
Description: Framework-agnostic SDK for [Kitenzo Bundle Builder](https://apps.shopify.com/bundlebuilder). Provides an API client, bundle configuration state machine, types, and utilities for building custom bundle experiences on any JavaScript platform.
Latest version: 0.1.2

# @kitenzo/core

Framework-agnostic SDK for [Kitenzo Bundle Builder](https://apps.shopify.com/bundlebuilder). Provides an API client, bundle configuration state machine, types, and utilities for building custom bundle experiences on any JavaScript platform.

Use this package directly for vanilla JS, Vue, Svelte, or server-side integrations. For React, use [`@kitenzo/react`](https://www.npmjs.com/package/@kitenzo/react) which wraps this package with hooks and components.

## Install

```bash
npm install @kitenzo/core
```

## Quick Start

```ts
import { KitenzoClient, createBundleBuilder } from '@kitenzo/core';

// 1. Create a client
const client = new KitenzoClient({
    apiKey: 'kit_live_...',
});

// 2. Fetch bundle with full product data
const bundle = await client.getFullBundle(42);

// 3. Build a bundle configuration
const builder = createBundleBuilder(bundle);

builder.subscribe(() => {
    const state = builder.getState();
    console.log('Selections:', state.selections);
    console.log('Section:', state.currentSectionIndex);
    console.log('Valid:', state.isComplete);
});

const section = bundle.sections[0];
builder.addItem(section.id, section.products[0].variants[0].id);

// 4. Submit to get cart-ready data
const result = await client.submitBundle(bundle, builder.getState().selections);
```

## API Client

```ts
const client = new KitenzoClient({
    apiKey: 'kit_live_...',       // Required
    baseUrl: '/api/headless/v1',  // Optional override
    apiVersion: 'v1',             // Optional (default: 'v1')
});
```

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `listBundles()` | `Bundle[]` | Published bundles |
| `getFullBundle(id)` | `BundleDetail` | Bundle with sections, products, and variants (fetches + merges in one call) |
| `getBundle(id)` | `RawBundleDetail` | Raw bundle with section/product IDs only |
| `getBundleProducts(id)` | `RawBundleProduct[]` | Product and variant data |
| `submitBundle(bundle, selections, options?)` | `ConfigureResponse` | Submit builder selections — handles product mapping automatically. Accepts both `BundleDetail` and `RawBundleDetail`. |
| `configureBundle(id, request)` | `ConfigureResponse` | Submit a raw configure request (prefer `submitBundle`) |
| `calculatePrice(id, request)` | `PriceResponse` | Calculate price via API (prefer `calculateLocalPrice` instead) |
| `getSettings()` | `ShopSettings` | Shop settings (currency, moneyFormat, features). Auto-fetched by `KitenzoProvider` in React. |

Variant IDs accept GID format (`gid://shopify/ProductVariant/123`) or plain numeric strings (`"123"`).

### API Endpoints

All endpoints require a Bearer token (`Authorization: Bearer kit_live_...`).

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/headless/v1/bundles` | List published bundles |
| `GET` | `/api/headless/v1/bundles/:id` | Bundle detail |
| `GET` | `/api/headless/v1/bundles/:id/products` | Product and variant data |
| `POST` | `/api/headless/v1/bundles/:id/configure` | Validate and create configuration |
| `POST` | `/api/headless/v1/bundles/:id/price` | Calculate price |
| `GET` | `/api/headless/v1/settings` | Shop settings |

Rate limit: 100 requests/minute per API key (configurable).

## Bundle Builder State Machine

`createBundleBuilder(bundle)` returns a state machine for step-by-step bundle configuration. It follows the `subscribe`/`getState` pattern, making it compatible with React's `useSyncExternalStore`, Svelte stores, or plain callbacks.

```ts
import { createBundleBuilder } from '@kitenzo/core';

const builder = createBundleBuilder(bundleDetail);

// Subscribe to state changes
const unsubscribe = builder.subscribe(() => {
    const state = builder.getState();
    renderUI(state);
});

// Mutations
builder.addItem(sectionId, variantId, quantity?);
builder.removeItem(sectionId, variantId);
builder.updateQuantity(sectionId, variantId, newQuantity);
builder.reset();

// Navigation
builder.nextSection();
builder.prevSection();
builder.goToSection(index);

// Queries
builder.getSectionQuantity(sectionId); // number
```

### Snapshot Shape

`builder.getState()` returns a `BundleBuilderSnapshot`:

```ts
{
    bundle: BundleDetail;
    selections: SectionSelections;       // Record<sectionId, BundleSelection[]>
    currentSectionIndex: number;
    currentSection: BundleSection | null;
    isSectionValid: boolean;
    isValid: boolean;
    isComplete: boolean;                 // alias for isValid
    allItems: BundleSelection[];         // flat list of all selections
}
```

The snapshot is immutable — a new object reference is created on each state change (for efficient React reconciliation or shallow comparison).

## Pricing

### `calculateLocalPrice(bundle, selections, currency?)`

Calculate bundle pricing locally — no API call needed. Handles flat discounts (percentage, fixed, price) and tiered discounts with all operation types.

```ts
import { calculateLocalPrice } from '@kitenzo/core';

const pricing = calculateLocalPrice(bundle, builder.getState().selections);
console.log(pricing.originalPrice);    // "45.00"
console.log(pricing.discountedPrice);  // "40.50"
console.log(pricing.discountType);     // "percentage"
console.log(pricing.discountValue);    // "10.00"
```

## Cart Utilities

### `buildCartPayload(result, existingAttributes?)`

Build everything needed to add a configured bundle to a Shopify cart. Returns `{ lines, attributes }` ready for the Storefront API.

```ts
import { buildCartPayload } from '@kitenzo/core';

const result = await client.submitBundle(bundle, selections);
const { lines, attributes } = buildCartPayload(result, cart.attributes);

// Hydrogen
cart.linesAdd(lines);
cart.cartAttributesUpdate(attributes);

// Or for a new cart:
cart.cartCreate({ lines, attributes });
```

### `buildCartLines(result)`

Build just the cart lines from a `SubmitBundleResult` (without attribute merging).

```ts
import { buildCartLines } from '@kitenzo/core';

const lines = buildCartLines(result);
```

### `mergeBundlesAttribute(existingAttributes, newBundlesAttribute)`

Low-level utility for merging `_bundles` cart attributes. Prefer `buildCartPayload` which handles this automatically.

```ts
import { mergeBundlesAttribute } from '@kitenzo/core';

const updatedAttributes = mergeBundlesAttribute(cart.attributes, bundlesAttr);
```

### `mergeBundleWithProducts(bundle, products)`

Hydrates a raw bundle detail response with product data, filtering to available variants. Prefer `client.getFullBundle(id)` which does this automatically.

```ts
import { mergeBundleWithProducts } from '@kitenzo/core';

const raw = await client.getBundle(42);
const products = await client.getBundleProducts(42);
const hydrated = mergeBundleWithProducts(raw, products);

// Or simply:
const bundle = await client.getFullBundle(42);
```

### `computeSectionQuantity(selections, sectionId)`

Returns the total quantity selected in a section.

### `isSectionMet(selections, section)`

Returns whether a section's minimum quantity constraint is satisfied.

## Styles

An optional reference stylesheet is included for `data-kitenzo-*` attributes:

```ts
import '@kitenzo/core/styles.css';
```

Override CSS custom properties to theme:

```css
[data-kitenzo-builder] {
    --kitenzo-color-primary: #4f46e5;
    --kitenzo-color-text: #1f2937;
    --kitenzo-radius: 8px;
    --kitenzo-spacing: 16px;
}
```

## Setup

### Enable the Headless API

In the Kitenzo admin panel, go to **Settings > Headless** to enable the headless API.

### Create an API Key

On the same page, click **Create API key**. Keys are prefixed `kit_live_` (production) or `kit_test_` (development).

## Troubleshooting

**CORS errors** -- Check that your origin is listed in the API key's Allowed origins (Settings > Headless).

**401/403** -- Verify the API key is correct, active, and headless is enabled.

**Cart lines added but no discount** -- Make sure the `_bundles` cart attribute is set using `mergeBundlesAttribute()`.