npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fixl-tech/igp-personalization

v1.3.0

Published

Unified TypeScript SDK for IGP-style product personalization: local studio engine, storage adapters, validation, signing, and hosted API client.

Readme

igp-personalization

Unified TypeScript SDK for IGP-style product personalization. This package combines the old igp-personalization-core and igp-personalization-sdk responsibilities into one typed package:

  • Local, in-process personalization engine through createStudio(...).
  • Storage adapters through createMemoryStorage() and createJsonFileStorage(...).
  • Product and design validation helpers.
  • Freeform layer manipulation (create/move/rotate/align/group/z-order) for a design canvas.
  • Dependency-free SVG rendering and pixel-buffer image tracing (outline detection, background cutout).
  • HMAC request signing shared by server and client code.
  • Hosted API client through new Client(...).

The package is written in TypeScript, builds to CommonJS in dist/, and emits .d.ts declarations for full type safety.

For a full picture of what's shipped vs. unpublished, what depends on what, and what's planned next (across this package and its React companion package), see ../ROADMAP.md. For a detailed, functionality-by-functionality specification (aimed at the team, not an external consumer), see ../PRD.md. For the formal, ID'd requirements with verification methods, see ../SRS.md.

Commercialization Roadmap

If you are deploying this as a real paid product, see COMMERCIALIZATION_PLAN.md for:

  • Free vs paid feature matrix
  • Minimal technical blueprint
  • Phase-by-phase implementation backlog

Install

npm install @fixl-tech/igp-personalization

Note the scope — the unscoped igp-personalization on npm is an unrelated package published by a different account, not this one.

Requires Node.js 18+ because the remote client uses the built-in fetch, FormData, and Blob APIs.

Browser bundles — @fixl-tech/igp-personalization/browser

The main entry (above) re-exports everything, including modules that use Node's fs (storage adapters, metering persistence) or Node's synchronous crypto module (signing, payment verification, Stripe billing) — neither works from an actual browser bundle, and because the package builds to CommonJS (not tree-shakeable ESM), a bundler resolving any import from the main entry has to resolve those too. If you're importing this package into browser code — a React canvas component, a client-side design tool, anything that doesn't run behind your own server — import from the /browser subpath instead:

import { createTextLayer, renderLayersToSVG, sobelEdgeTrace } from '@fixl-tech/igp-personalization/browser';

It exposes the full local studio engine, layers, rendering, image tracing, order pricing, and the entitlements/metering evaluation functions (not their Node-only persistence backends) — everything genuinely usable client-side. Anything inherently server-side (Client, signing, verifyRazorpaySignature, createJsonFileStorage, the Stripe webhook runtime, the hosted-API runtime) is intentionally absent — not a limitation to work around, those were never meant to ship to a browser. See test/smoke.browser.js for the structural guarantee (it walks the actual compiled require() graph and asserts fs/crypto are unreachable) rather than trusting this list to stay accurate by hand.

Build From This Repo

cd packages/sdk
npm install
npm run build
npm test

Output is generated into dist/:

dist/index.js
dist/index.d.ts
dist/**/*.js
dist/**/*.d.ts

Import

CommonJS:

const {
  Client,
  createStudio,
  createMemoryStorage,
  createJsonFileStorage,
  signing,
} = require('@fixl-tech/igp-personalization');

TypeScript:

import {
  Client,
  createStudio,
  createMemoryStorage,
  type DesignInput,
  type Product,
} from '@fixl-tech/igp-personalization';

Local Studio Engine

Use createStudio({ storage }) when you want the personalization logic to run inside your own process without HTTP.

import { createStudio, createMemoryStorage } from '@fixl-tech/igp-personalization';

const studio = createStudio({
  storage: createMemoryStorage(),
});

const products = await studio.products.list();
const mug = await studio.products.get('mug-white-11oz');

const design = await studio.designs.save({
  productId: mug.id,
  productName: mug.name,
  layers: [{ type: 'text', text: 'Happy Birthday!', x: 300, y: 300 }],
});

createStudio({ storage })

Returns:

{
  storage,
  products,
  designs,
}

Throws if the storage adapter does not expose collection(name).

Product Functions

studio.products.list()

Returns all built-in products plus custom products from storage.

const products = await studio.products.list();

studio.products.get(id)

Returns one product by ID.

const product = await studio.products.get('mug-white-11oz');

Throws NotFoundError if the product does not exist.

studio.products.addCustom(input)

Creates a custom product.

const product = await studio.products.addCustom({
  name: 'Tote Bag',
  mockupUrl: '/mockups/tote.svg',
  price: 499,
  category: 'Bags',
  canvasWidth: 600,
  canvasHeight: 600,
  printAreas: [
    { x: 60, y: 80, width: 280, height: 300, shape: 'rectangle' },
  ],
});

Throws ValidationError when required fields are missing or print areas are invalid.

studio.products.removeCustom(id)

Deletes a custom product.

await studio.products.removeCustom('custom-abc123');

Throws NotFoundError if the ID is unknown.

Design Functions

studio.designs.list()

Returns saved designs, newest first.

const designs = await studio.designs.list();

studio.designs.get(id)

Returns one saved design.

const design = await studio.designs.get('design_id');

Throws NotFoundError if the design does not exist.

studio.designs.save(input)

Saves a design.

const design = await studio.designs.save({
  productId: 'mug-white-11oz',
  productName: 'Classic White Mug (11oz)',
  layers: [
    { type: 'text', text: 'Happy Birthday!', x: 300, y: 300 },
    { type: 'image', url: 'https://cdn.example.com/logo.png', x: 120, y: 180 },
  ],
  previewDataUrl: 'data:image/png;base64,...',
});

Throws ValidationError if productId or layers[] is missing.

studio.designs.delete(id)

Deletes a saved design.

await studio.designs.delete('design_id');

Throws NotFoundError if the design does not exist.

Rendering & Image Tracing

Published as of 1.3.0.

The SDK's rendering surface is deliberately narrow: it turns a layers array into an SVG string — markup, not pixels — using nothing but string templating, so it runs identically in Node or a browser with zero runtime dependencies. It does not decode/encode actual image files (PNG/JPEG/etc.) and does not draw to a <canvas> — those always need either a browser or a Node image library, and picking one for you would mean adding a dependency. Turning the SVG into a raster image, or turning a photo into the raw pixels the tracing functions below need, stays your choice.

renderLayersToSVG(layers, canvas)

import { renderLayersToSVG, createTextLayer } from '@fixl-tech/igp-personalization';

const svg = renderLayersToSVG(
  [createTextLayer({ text: 'Happy Birthday!', x: 50, y: 40, width: 200, height: 30, fontSize: 22 })],
  { width: 300, height: 150, background: '#fef3c7' },
);

canvas.background accepts a CSS color or an image URL/data URL — same value Design.canvasBackground stores. Text/image/shape layers all render; layer types the SDK doesn't recognize render as nothing rather than throwing, so one unrecognized layer doesn't break the rest of the canvas. Text content is XML-escaped.

renderDesignToSVG(design, canvas)

Convenience wrapper — uses design.canvasBackground unless canvas.background is explicitly passed.

import { renderDesignToSVG } from '@fixl-tech/igp-personalization';

const svg = renderDesignToSVG(design, { width: 600, height: 600 });

renderLayerToSVG(layer)

Renders a single layer's fragment — what renderLayersToSVG calls per-layer, exposed directly for callers building their own composition.

Image editing (crop, flip, opacity, filters, duotone, clip-to-shape)

None of these are separate functions — they're optional fields on any layer, read directly by renderLayerToSVG/renderLayersToSVG/renderDesignToSVG. Every one is expressed as SVG markup or a <filter> primitive (never pixel manipulation), so they stay in the same zero-dependency, markup-only lane as the rest of rendering.ts. A layer using none of them renders exactly what it rendered before these existed.

import { createImageLayer } from '@fixl-tech/igp-personalization';

const photo = createImageLayer({
  url: 'https://cdn.example.com/photo.jpg',
  x: 20, y: 20, width: 260, height: 180,
  // naturalWidth/naturalHeight: the image's own pixel size — read this once
  // from your <img>/File when the photo is added (e.g. HTMLImageElement's
  // naturalWidth/naturalHeight), and store it on the layer. The SDK never
  // loads the image itself (no I/O in a rendering module), so crop math has
  // nothing to work with unless you supply this.
  naturalWidth: 1600, naturalHeight: 1200,
  cropX: 0.3, cropY: 0.6, cropZoom: 1.4,  // pan/zoom within the photo
  flipX: true,
  opacity: 0.9,
  clipShape: 'rounded-rect',               // any ALLOWED_SHAPES value; 'custom' reads clipPoints
  filters: { brightness: 0.1, contrast: 0.15, saturation: -0.3, blur: 0, sharpen: 0, temperature: 0 },
  duotone: { shadowColor: '#1e1b4b', highlightColor: '#fde68a' },
});
  • Crop (cropX/cropY/cropZoom) needs naturalWidth/naturalHeight to do anything — without them, crop fields are silently ignored and the layer renders with the pre-crop xMidYMid slice behavior (auto-centered cover-fit), not an error. cropX/cropY are the normalized (0..1) focal point that stays centered in the layer's box; cropZoom (>=1) is how far zoomed in.
  • Flip (flipX/flipY) mirrors around the layer's own center, composing correctly with rotation.
  • Opacity is 0..1; omitted or 1 emits no extra markup.
  • Filters (brightness/contrast/saturation/temperature: -1..1, 0 = unchanged; blur: px std-deviation; sharpen: 0..1) build a single chained <filter> with only the primitives actually needed — an all-defaults filters: {} emits no <filter> at all. These are CSS-filter-style approximations, not a claim of matching any specific photo editor pixel-for-pixel.
  • Duotone maps the layer's luminance to a two-color gradient — the classic duotone photo effect.
  • clipShape clips the layer to one of ALLOWED_SHAPES — reusing the exact same geometry a shape layer would render, so a star clip and a star shape layer can never visually drift apart. clipShape: 'custom' reads clipPoints (same Point[] shape a custom shape layer's points uses).
  • All of the above work on any layer type, not just images — a text or shape layer can be faded, blurred, or clipped too, matching how a real design tool lets you.

replaceImageLayerSource(layers, index, url, options?)

Swaps an image layer's url and resets its crop framing (cropX/cropY/cropZoom/naturalWidth/naturalHeight) — a crop tuned for one photo's framing has no reason to make sense against a different one. This is the one case that isn't just a plain field, which is why it's a dedicated function rather than a raw updateLayerAt call.

import { replaceImageLayerSource } from '@fixl-tech/igp-personalization';

layers = replaceImageLayerSource(layers, 0, 'https://cdn.example.com/new-photo.jpg', {
  naturalWidth: 1200, naturalHeight: 900,
});

Throws ValidationError if index is out of range, the layer there isn't an image layer, or url is empty.

sobelEdgeTrace(buffer, options?)

Outline/edge detection via a 3x3 Sobel operator over grayscale luminance. buffer is a PixelBuffer{ width, height, data }, where data is RGBA (Uint8ClampedArray or a plain number array) of length width * height * 4, the same shape as a browser ImageData. Get the pixels from wherever you like (a <canvas> getImageData(), a Node image library) — this function has no opinion on how they got there.

import { sobelEdgeTrace } from '@fixl-tech/igp-personalization';

// In a browser: const { data, width, height } = ctx.getImageData(0, 0, w, h);
const edges = sobelEdgeTrace({ width, height, data }, { threshold: 60 });
// edges.data is a new RGBA buffer: opaque black edges, transparent elsewhere (or set invert: true for white-on-black).

cutoutBackground(buffer, options?)

Background removal via border flood-fill by color distance — a practical heuristic, not ML subject segmentation. It samples (or accepts) a background color, then flood-fills every pixel reachable from the image's border that's within tolerance of that color to fully transparent. Flood-filling from the border (rather than a global per-pixel threshold) is what keeps a same-colored region inside the subject from being eaten along with the actual background. Works well against a plain/solid backdrop (product photos, mockups); a busy background or a subject sharing the background's color at its edge won't cut out cleanly — an inherent limit of a color-distance heuristic.

import { cutoutBackground } from '@fixl-tech/igp-personalization';

const cutout = cutoutBackground({ width, height, data }, { tolerance: 32 });
// cutout.data: RGB unchanged, alpha set to 0 wherever the flood-fill reached.

Both tracing functions throw ValidationError if width/height aren't positive numbers or data.length doesn't equal width * height * 4.

Storage Adapters

A storage adapter implements:

interface StorageAdapter {
  collection(name: string): {
    all(): Promise<Array<{ id: string }>>;
    get(id: string): Promise<{ id: string } | null>;
    put(doc: { id: string }): Promise<void>;
    remove(id: string): Promise<boolean>;
    count?(): Promise<number>;
  };
}

createMemoryStorage()

Ephemeral storage for tests, scripts, and demos.

const storage = createMemoryStorage();
const studio = createStudio({ storage });

createJsonFileStorage({ dir })

Persists each collection as a JSON file under dir.

const storage = createJsonFileStorage({ dir: './data' });
const studio = createStudio({ storage });

This creates files such as:

data/designs.json
data/custom-products.json

Validation Helpers

sanitizePoints(points)

Validates custom polygon points. Returns sanitized points or null.

const points = sanitizePoints([
  [10, 10],
  [100, 10],
  [100, 100],
]);

sanitizeArea(area, index?)

Validates and normalizes one print area. Returns a PrintArea or null.

const area = sanitizeArea({
  x: 10,
  y: 20,
  width: 200,
  height: 120,
  shape: 'rounded-rect',
});

buildCustomProductFields(input)

Validates custom product input and returns normalized fields without ID or timestamps. ProductService.addCustom(...) uses this internally.

const fields = buildCustomProductFields({
  name: 'Poster',
  mockupUrl: '/mockups/poster.svg',
  printAreas: [{ x: 50, y: 50, width: 300, height: 400, shape: 'rectangle' }],
});

validateDesignInput(input)

Throws ValidationError unless the design has productId and layers[].

validateDesignInput({
  productId: 'mug-white-11oz',
  layers: [{ type: 'text', text: 'Hello' }],
});

Signing

The SDK signs requests with:

HMAC-SHA256(secret, "METHOD\nPATH\nTIMESTAMP\nSHA256(body)")

signing.sign(input)

Creates a signature.

const signature = signing.sign({
  secret: 'sk_test',
  method: 'POST',
  path: '/v1/designs',
  timestamp: Math.floor(Date.now() / 1000),
  body: JSON.stringify({ productId: 'mug-white-11oz', layers: [] }),
});

signing.verify(input)

Verifies the signature and timestamp skew.

const ok = signing.verify({
  secret: 'sk_test',
  method: 'POST',
  path: '/v1/designs',
  timestamp,
  body,
  signature,
});

signing.buildStringToSign(input)

Returns the canonical string used by HMAC.

const canonical = signing.buildStringToSign({
  method: 'GET',
  path: '/v1/products',
  timestamp: 1710000000,
  body: '',
});

signing.sha256Hex(input)

Returns the SHA-256 hex digest of a string or buffer.

const digest = signing.sha256Hex('hello');

The individual functions sign, verify, buildStringToSign, and sha256Hex are also exported directly.

Hosted API Client

Use Client when calling the hosted /v1 API. The API secret stays on your server and is never sent over the network.

import { Client } from '@fixl-tech/igp-personalization';

const client = new Client({
  apiKey: process.env.PERSONALIZATION_API_KEY!,
  apiSecret: process.env.PERSONALIZATION_API_SECRET!,
  baseUrl: 'https://api.yourco.com',
  workspaceId: 'ws_123', // optional; recommended for paid multi-workspace setups
  retry: true, // optional safe retry policy for idempotent requests
});

Optional client options:

  • workspaceId: sends x-workspace-id on each API request.
  • defaultRequestHeaders: merged into every API request.
  • retry: true, false, or retry options (maxAttempts, baseDelayMs, maxDelayMs, retryOnStatuses, retryOnNetworkError).

client.products.list()

const products = await client.products.list();

client.products.get(id)

const product = await client.products.get('mug-white-11oz');

client.products.addCustom(input)

const product = await client.products.addCustom({
  name: 'Tote Bag',
  mockupUrl: '/mockups/tote.svg',
  printAreas: [{ x: 60, y: 80, width: 280, height: 300, shape: 'rectangle' }],
}, {
  idempotencyKey: 'custom-prod-req-001',
});

client.products.removeCustom(id)

await client.products.removeCustom('custom-abc123');

client.designs.list()

const designs = await client.designs.list();

client.designs.get(id)

const design = await client.designs.get('design_id');

client.designs.save(input)

const saved = await client.designs.save({
  productId: 'mug-white-11oz',
  layers: [{ type: 'text', text: 'Hello', x: 100, y: 120 }],
}, {
  idempotencyKey: 'design-save-req-001',
});

client.designs.delete(id)

await client.designs.delete('design_id');

client.uploadImage(data, options?)

Uploads an image using multipart form data. data can be a Buffer, Uint8Array, or Blob.

import fs from 'fs';

const uploaded = await client.uploadImage(fs.readFileSync('./logo.png'), {
  filename: 'logo.png',
  contentType: 'image/png',
});

console.log(uploaded.url);

client.account()

Returns the authenticated tenant, usage, and limits.

const account = await client.account();

client.usage()

Returns usage-focused metrics for the current account/workspace.

const usage = await client.usage();
console.log(usage.requests, usage.limits);

Errors

ValidationError

Thrown by local validation when input is invalid.

try {
  await studio.designs.save({} as never);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(error.code, error.message);
  }
}

NotFoundError

Thrown by local services when a product or design is missing.

PersonalizationApiError

Thrown by Client when the hosted API returns a non-2xx response.

try {
  await client.designs.save({} as never);
} catch (error) {
  if (error instanceof PersonalizationApiError) {
    console.error(error.status, error.apiCode, error.rateLimit, error.body);
  }
}

PlanRequiredError, FeatureNotEnabledError, QuotaExceededError

Typed subclasses thrown by Client when the hosted API returns entitlement and quota failures. These subclasses live in errors.ts alongside the base error classes above. Published as of 1.2.0 — see Public Exports and COMMERCIALIZATION_PLAN.md for what they're for.

  • PlanRequiredError: plan upgrade required.
  • FeatureNotEnabledError: feature is not enabled for the current plan.
  • QuotaExceededError: request quota/rate limit exceeded.

Common statuses:

  • 400: invalid request payload.
  • 401: invalid API key or signature.
  • 402: plan limit reached.
  • 413: upload too large.
  • 429: rate limit or quota exceeded.

Commercial Helpers (Server-Side)

Published as of 1.2.0. Everything in this section (billing.ts, billing-runtime.ts, commercial.ts, commercial-http.ts, hosted-runtime.ts, hosted-http-adapter.ts, metering-pipeline.ts, subscription-store-json.ts, subscription-store-sql.ts) was source-available but excluded from every npm release through 1.1.2 (see CHANGELOG.md). As of 1.2.0 it ships in the package you get from npm install like everything else. It's still deliberately server-side-only code — see COMMERCIALIZATION_PLAN.md for the intended usage pattern (evaluate entitlements/metering in your own hosted API layer, don't ship secrets to a browser bundle).

The SDK's source tree includes lightweight helpers you can use in your hosted API layer.

evaluateEntitlement(input)

Evaluates plan, feature, and quota checks in a deterministic way.

import { evaluateEntitlement } from '@fixl-tech/igp-personalization';

const decision = evaluateEntitlement({
  context: {
    accountId: 'acc_123',
    workspaceId: 'ws_123',
    plan: 'starter',
    features: [{ code: 'designs.collaboration', enabled: true }],
    usage: { requests: 1200 },
    limits: { requests: 5000 },
  },
  requiredFeature: 'designs.collaboration',
  usageKey: 'requests',
  units: 1,
});

if (!decision.ok) {
  // map decision.code to an HTTP response and stop request processing
}

createMeteringEvent(input)

Creates normalized metering payloads for your usage pipeline.

import { createMeteringEvent } from '@fixl-tech/igp-personalization';

const event = createMeteringEvent({
  requestId: 'req_001',
  accountId: 'acc_123',
  workspaceId: 'ws_123',
  endpoint: '/v1/designs',
  units: 1,
  meta: { operation: 'design.save' },
});

authorizeRequest(input) and decisionToHttpError(decision)

Convenience helpers for hosted API middleware. They evaluate entitlement rules and produce API-safe error payloads.

import { authorizeRequest, buildRateLimitHeaders } from '@fixl-tech/igp-personalization';

const gate = authorizeRequest({
  context: accountContext,
  requiredFeature: 'designs.collaboration',
  usageKey: 'requests',
  units: 1,
});

if (!gate.ok) {
  return res.status(gate.error.status).json(gate.error.body);
}

if (gate.decision.limit != null && gate.decision.current != null) {
  res.set(buildRateLimitHeaders({
    limit: gate.decision.limit,
    current: gate.decision.current,
  }));
}

executeHostedRoute(input) and MemoryMeteringSink

End-to-end helper for hosted API routes: signature auth, entitlement gating, handler execution, and metering capture.

import { executeHostedRoute, MemoryMeteringSink } from '@fixl-tech/igp-personalization';

const sink = new MemoryMeteringSink();

const result = await executeHostedRoute({
  request,
  endpoint: '/v1/designs',
  resolveApiKey,
  requirement: {
    requiredFeature: 'designs.collaboration',
    usageKey: 'requests',
    units: 1,
  },
  metering: sink,
  handler: async () => ({ ok: true }),
});

res.status(result.status);
if (result.headers) res.set(result.headers);
res.json(result.body);

Metering Persistence and Rollup Helpers

For usage aggregation jobs, the SDK exports a JSONL sink and rollup helpers.

import {
  JsonFileMeteringSink,
  createMeteringEvent,
  rollupMeteringEvents,
} from '@fixl-tech/igp-personalization';

const sink = new JsonFileMeteringSink({ filePath: './data/metering-events.jsonl' });

await sink.record(createMeteringEvent({
  requestId: 'req_1',
  accountId: 'acc_1',
  workspaceId: 'ws_1',
  endpoint: '/v1/designs',
  units: 1,
}));

const events = await sink.list();
const usageRows = rollupMeteringEvents(events, { period: '2026-07' });

Stripe Subscription Sync Helpers

The SDK includes helper utilities to normalize Stripe subscription webhooks and sync entitlement context.

import {
  normalizeStripeSubscriptionUpdate,
  syncEntitlementContextFromSubscription,
} from '@fixl-tech/igp-personalization';

const update = normalizeStripeSubscriptionUpdate(event, {
  priceToPlan: {
    price_starter_monthly: 'starter',
    price_growth_monthly: 'growth',
    price_scale_monthly: 'scale',
  },
  defaultPlan: 'free',
});

if (update) {
  const nextContext = syncEntitlementContextFromSubscription(currentContext, update, {
    planLimits: {
      starter: { requests: 50000 },
      growth: { requests: 300000 },
      scale: { requests: 2000000 },
    },
    overagePlans: ['scale'],
  });
}

Stripe Webhook Runtime Helpers

Use these helpers in your webhook endpoint to verify signatures, normalize updates, and persist subscription changes.

import {
  MemorySubscriptionStore,
  processStripeWebhook,
} from '@fixl-tech/igp-personalization';

const store = new MemorySubscriptionStore();

const result = await processStripeWebhook({
  rawBody,
  signatureHeader: req.headers['stripe-signature'] as string,
  endpointSecret: process.env.STRIPE_WEBHOOK_SECRET!,
  mapping: {
    priceToPlan: {
      price_starter_monthly: 'starter',
      price_growth_monthly: 'growth',
      price_scale_monthly: 'scale',
    },
    defaultPlan: 'free',
  },
  store,
});

res.status(result.status).json(result.body);

JSON File Subscription Store Adapter

For local development and lightweight deployments, you can persist subscription updates and entitlement contexts into JSON files.

import {
  JsonFileSubscriptionStore,
  processStripeWebhook,
} from '@fixl-tech/igp-personalization';

const store = new JsonFileSubscriptionStore({
  updatesFilePath: './data/subscription-updates.json',
  contextsFilePath: './data/entitlement-contexts.json',
});

const result = await processStripeWebhook({
  rawBody,
  signatureHeader,
  endpointSecret,
  mapping,
  store,
});

SQL Subscription Store and Retry Helpers

For production deployments, use the SQL store adapter and retry-capable webhook processor.

import {
  SqlSubscriptionStore,
  processStripeWebhookWithRetry,
  sqlSubscriptionStoreSchema,
} from '@fixl-tech/igp-personalization';

// Run sqlSubscriptionStoreSchema() outputs in your migration process.
const store = new SqlSubscriptionStore(db);

const result = await processStripeWebhookWithRetry({
  rawBody,
  signatureHeader,
  endpointSecret,
  mapping,
  store,
  retry: { maxAttempts: 3, baseDelayMs: 200, maxDelayMs: 3000 },
});

Hosted HTTP Adapter

executeHostedHttpRoute(...) adapts typical HTTP request objects into the hosted runtime pipeline.

Public Exports

As of 1.2.0, all 21 modules in the source tree were published to npm — see CHANGELOG.md's [1.2.0] entry for the history (through 1.1.2, only 12 of them were released; the commercial layer below was source-available but held back). 1.3.0 adds two more (rendering.ts, trace.ts), for 23 total.

// Core: catalog, products, designs, validation, ids, studio, client
Client
createStudio
createMemoryStorage
createJsonFileStorage
ProductService
DesignService
BUILTIN_PRODUCTS
ALLOWED_SHAPES
normalizeProduct
sanitizeArea
sanitizePoints
buildCustomProductFields
validateDesignInput
createId

// Layers — build/manipulate a design's freeform layers array
createTextLayer
createImageLayer
createShapeLayer
addLayer
removeLayerAt
updateLayerAt
reorderLayer
rotateLayer
bringToFront
sendToBack
bringForward
sendBackward
groupLayers
ungroupLayers
getGroupLayers
alignLayer
getBoundingBox
replaceImageLayerSource

// Rendering & image tracing
renderLayersToSVG
renderDesignToSVG
renderLayerToSVG
sobelEdgeTrace
cutoutBackground

// Order pricing & payments
computeCouponDiscount
assertCouponUsable
computeOrderPricing
verifyRazorpaySignature

// Signing
signing
sign
verify
buildStringToSign
sha256Hex

// Errors
ValidationError
NotFoundError
PersonalizationApiError
PlanRequiredError
FeatureNotEnabledError
QuotaExceededError

// Commercial / hosted API layer — see "Commercial Helpers" above
evaluateEntitlement
createMeteringEvent
authorizeRequest
decisionToHttpError
buildRateLimitHeaders
authenticateRequest
executeHostedRoute
executeHostedHttpRoute
MemoryMeteringSink
JsonFileMeteringSink
rollupMeteringEvents
getBillingPeriod
normalizeStripeSubscriptionUpdate
syncEntitlementContextFromSubscription
deriveEntitlementsFromPlan
mapPriceToPlan
isPaidSubscriptionStatus
verifyStripeWebhookSignature
processStripeWebhook
processStripeWebhookWithRetry
signStripeWebhookPayload
MemorySubscriptionStore
JsonFileSubscriptionStore
SqlSubscriptionStore
sqlSubscriptionStoreSchema

Type exports include Product, PrintArea, Design, DesignInput, DesignVersionInput, DesignPage, DesignLayer, CustomProductInput, StorageAdapter, StorageCollection, Studio, ClientOptions, RequestOptions, RetryOptions, UsageResult, RateLimitInfo, BoundingBox, LayerAlignment, TextLayerOptions, ImageLayerOptions, ShapeLayerOptions, CommonLayerAdjustments, ImageFilterAdjustments, DuotoneAdjustment, RenderCanvasOptions, PixelBuffer, SobelTraceOptions, CutoutOptions, PricingRules, OrderPricingInput, OrderPricingResult, CouponLike, CouponType, CouponUsabilityContext, VerifyRazorpaySignatureInput, EntitlementContext, EntitlementCheckInput, EntitlementDecision, MeteringEvent, UploadResult, CommercialHttpError, RateLimitHeaderInput, HostedRequest, HostedResponse, HostedRouteContext, ApiPrincipal, ApiKeyResolver, MeteringSink, RouteRequirement, HttpRequestLike, HttpRouteAdapterInput, JsonFileMeteringSinkOptions, RollupOptions, UsageRollupRow, BillingSubscriptionStatus, BillingSubscriptionUpdate, StripeWebhookEvent, StripePlanMapping, EntitlementSyncOptions, StripeWebhookSignatureInput, StripeWebhookVerificationResult, StripeWebhookRetryOptions, SubscriptionUpdateStore, ProcessStripeWebhookInput, ProcessStripeWebhookWithRetryInput, ProcessStripeWebhookResult, ProcessStripeWebhookWithRetryResult, JsonFileSubscriptionStoreOptions, SqlQueryExecutor, and SqlSubscriptionStoreOptions.