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

captain-shipping-protection-sdk

v0.1.0

Published

Framework-independent SDK for adding Captain Shipping Protection to a Shopify headless storefront.

Readme

Captain Shipping Protection SDK

Framework-independent SDK for adding Captain Shipping Protection to a Shopify headless storefront.

Use this package when your storefront does not use React, when you already have your own widget UI, or when you want direct control over the integration. It can be used with Vanilla JavaScript, Vue, Svelte, Solid, Angular, or any other browser-based frontend.

The SDK:

  • Loads the Captain configuration for a Shopify store.
  • Calculates the correct protection price and variant for the current cart.
  • Detects whether the cart already contains Captain's protection product.
  • Applies merchant eligibility and exclusion rules.
  • Returns plain data so your application can render any UI.
  • Includes TypeScript declarations.

The SDK does not:

  • Render a widget.
  • Store the shopper's checked state.
  • Add or remove Shopify cart lines.
  • Redirect the shopper to checkout.

Those responsibilities stay in your storefront.

Requirements

  • A Captain Shipping Protection configuration for the target Shopify store.
  • A browser with fetch and Web Crypto support.
  • HTTPS in production. Web Crypto is also available on localhost during local development.
  • Cart prices passed to the SDK must be integer minor units. For USD, 1299 means $12.99.

The package is ESM-only and has no framework dependency.

Installation

With npm:

npm install captain-shipping-protection-sdk

With pnpm:

pnpm add captain-shipping-protection-sdk

With Yarn:

yarn add captain-shipping-protection-sdk

Quick start

The package exports a pre-created shippingProtection singleton:

import {
  shippingProtection,
  type SdkCartData,
} from "captain-shipping-protection-sdk";

await shippingProtection.init({
  shop: "example.myshopify.com",
  country: "US",
  locale: "en",
  currency: "USD",
});

const cartData: SdkCartData = {
  token: "gid://shopify/Cart/your-cart-id",
  currency: "USD",
  total_price: 12900,
  item_count: 2,
  items: [
    {
      id: 456789,
      key: "gid://shopify/CartLine/your-line-id",
      quantity: 2,
      variant_id: 456789,
      product_id: 123456,
      final_line_price: 12900,
      title: "Example Variant",
      product_title: "Example Product",
      sku: "EXAMPLE-SKU",
    },
  ],
};

const info = await shippingProtection.getInfo({cartData});

if (!info.isExcluded) {
  console.log("Protection product:", info.productId);
  console.log("Protection variant:", info.variantsId);
  console.log("Protection price:", info.price);
}

Call init() once for the active shop, country, locale, and currency. Call getInfo() whenever the cart changes and immediately before checkout.

Recommended cart flow

The recommended integration keeps protection out of the Shopify cart while the shopper edits it:

  1. Initialize the SDK.
  2. Convert the current Shopify cart to SdkCartData.
  3. Call getInfo() and render your widget when isExcluded is false.
  4. Keep the shopper's checked choice in your own application state.
  5. If includedProtection is true, keep the widget checked and remove the existing protection line from Shopify.
  6. When checkout starts, call getInfo() again with the latest cart.
  7. If the shopper is checked and the latest info is eligible, add one unit of variantsId.
  8. Redirect immediately to the checkout URL returned by the updated cart.

Adding protection only at checkout prevents repeated cart mutations while the shopper changes products, quantities, or discounts. It also ensures the selected variant matches the latest eligible cart total.

Step 1: Initialize the SDK

import {shippingProtection} from "captain-shipping-protection-sdk";

await shippingProtection.init({
  shop: "example.myshopify.com",
  country: "US",
  locale: "en",
  currency: "USD",
});

Initialization loads and caches the merchant's Captain configuration.

Re-run init() when any initialization value changes:

  • The active Shopify store.
  • The shopper country.
  • The storefront locale.
  • The active currency.

The exported SDK is a singleton. Calling init() again replaces its active context and setting. A normal storefront should initialize it for one active storefront context at a time.

The loaded merchant configuration is available after initialization:

console.log(shippingProtection.setting);

Do not call getInfo() before init() resolves. It throws an error when the SDK has not been initialized.

Step 2: Convert your Shopify cart

Shopify's Storefront API normally returns GraphQL GIDs and decimal money strings. The SDK expects numeric Shopify IDs and integer minor-unit prices.

import type {SdkCartData} from "captain-shipping-protection-sdk";

type StorefrontCart = {
  id: string;
  totalQuantity: number;
  cost: {
    subtotalAmount: {
      amount: string;
      currencyCode: string;
    };
  };
  lines: {
    nodes: Array<{
      id: string;
      quantity: number;
      cost: {
        totalAmount: {
          amount: string;
        };
      };
      merchandise: {
        id: string;
        sku?: string | null;
        title: string;
        product: {
          id: string;
          title: string;
        };
      };
    }>;
  };
};

function numericShopifyId(gid: string): number {
  const value = Number(gid.split("/").pop());

  if (!Number.isFinite(value)) {
    throw new Error(`Invalid Shopify GID: ${gid}`);
  }

  return value;
}

function toMinorUnits(amount: string): number {
  return Math.round(Number(amount) * 100);
}

export function toSdkCartData(cart: StorefrontCart): SdkCartData {
  return {
    token: cart.id,
    currency: cart.cost.subtotalAmount.currencyCode,
    total_price: toMinorUnits(cart.cost.subtotalAmount.amount),
    item_count: cart.totalQuantity,
    items: cart.lines.nodes.map((line) => ({
      id: numericShopifyId(line.merchandise.id),
      key: line.id,
      quantity: line.quantity,
      variant_id: numericShopifyId(line.merchandise.id),
      product_id: numericShopifyId(line.merchandise.product.id),
      final_line_price: toMinorUnits(line.cost.totalAmount.amount),
      title: line.merchandise.title,
      product_title: line.merchandise.product.title,
      sku: line.merchandise.sku ?? "",
    })),
  };
}

Use the cart subtotal before shipping and tax. Include discounts in total_price and final_line_price when they affect what the shopper pays.

Step 3: Resolve protection information

const info = await shippingProtection.getInfo({
  cartData: toSdkCartData(shopifyCart),
});

The result contains:

interface ShippingProtectionInfo {
  productId: string;
  variantsId: string;
  price: string;
  includedProtection: boolean;
  isExcluded: boolean;
  excludedReason?: string;
}
  • productId: Shopify product ID used by Captain Shipping Protection.
  • variantsId: protection variant selected for the current eligible cart total.
  • price: protection price in the active currency's major unit.
  • includedProtection: whether the input cart already contains Captain's protection product.
  • isExcluded: whether protection must not be offered for this cart.
  • excludedReason: machine-readable reason the cart is not eligible.

Render or update your widget only after the promise resolves.

Step 4: Manage checked state

The SDK intentionally does not own UI state. Store checked in your framework, state manager, or plain JavaScript.

Recommended rules:

  • If the cart contains protection, set checked to true.
  • Remove that existing protection line from the cart.
  • After removal, keep checked as true.
  • For the first eligible cart without protection, initialize checked from shippingProtection.setting?.tm_default_display_status === 1.
  • A shopper toggle changes checked only. It should not mutate Shopify.
  • Hide the widget when info.isExcluded is true.

Framework-independent state example:

import type {
  ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";

let checked = false;
let checkedInitialized = false;
let currentInfo: ShippingProtectionInfo | null = null;
let latestRequestId = 0;

async function syncProtection(shopifyCart: StorefrontCart) {
  const requestId = ++latestRequestId;
  const cartData = toSdkCartData(shopifyCart);
  const info = await shippingProtection.getInfo({cartData});

  // Ignore a response for a cart that is no longer current.
  if (requestId !== latestRequestId) {
    return;
  }

  currentInfo = info;

  if (info.includedProtection) {
    checked = true;
    checkedInitialized = true;
    await removeProtectionLinesByProductId(info.productId);
    await refreshHostCart();
  } else if (!checkedInitialized) {
    checked =
      shippingProtection.setting?.tm_default_display_status === 1;
    checkedInitialized = true;
  } else if (info.isExcluded) {
    checked = false;
  }

  renderProtection({
    checked,
    hidden: info.isExcluded,
    info,
    setting: shippingProtection.setting,
  });
}

function onProtectionToggle(nextChecked: boolean) {
  checked = nextChecked;

  if (currentInfo) {
    renderProtection({
      checked,
      hidden: currentInfo.isExcluded,
      info: currentInfo,
      setting: shippingProtection.setting,
    });
  }
}

removeProtectionLinesByProductId, refreshHostCart, and renderProtection are host functions. Implement them with your storefront framework and Shopify cart layer.

Step 5: Render your UI

The SDK returns data only, so you can use your existing design system.

Minimal HTML example:

<section id="shipping-protection" hidden>
  <label>
    <input id="shipping-protection-toggle" type="checkbox" />
    <span id="shipping-protection-title">Shipping Protection</span>
    <span id="shipping-protection-price"></span>
  </label>
  <p id="shipping-protection-description"></p>
</section>
import type {
  ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";

const root = document.querySelector<HTMLElement>("#shipping-protection")!;
const toggle = document.querySelector<HTMLInputElement>(
  "#shipping-protection-toggle",
)!;
const price = document.querySelector<HTMLElement>(
  "#shipping-protection-price",
)!;
const description = document.querySelector<HTMLElement>(
  "#shipping-protection-description",
)!;

toggle.addEventListener("change", () => {
  onProtectionToggle(toggle.checked);
});

function renderProtection({
  checked,
  hidden,
  info,
}: {
  checked: boolean;
  hidden: boolean;
  info: ShippingProtectionInfo;
}) {
  root.hidden = hidden;
  toggle.checked = checked;
  price.textContent = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(Number(info.price));
  description.textContent = checked
    ? "Your order is protected from loss, damage, and theft."
    : "Add protection to your order.";
}

Merchant-configured text and styling are available through shippingProtection.setting. See SdkCartSetting in the package's TypeScript declarations for all currently supported fields.

If you want Captain's standard React widget, use captain-shipping-protection-react instead.

Step 6: Prepare checkout

Always resolve information again immediately before checkout. The cart may have changed since the widget was rendered.

function toVariantGid(variantId: string): string {
  return variantId.startsWith("gid://shopify/ProductVariant/")
    ? variantId
    : `gid://shopify/ProductVariant/${variantId}`;
}

async function prepareProtectionForCheckout() {
  let shopifyCart = await getCurrentShopifyCart();
  let cartData = toSdkCartData(shopifyCart);
  let info = await shippingProtection.getInfo({cartData});

  // Never add a second protection line.
  if (info.includedProtection) {
    await removeProtectionLinesByProductId(info.productId);
    shopifyCart = await getCurrentShopifyCart();
    cartData = toSdkCartData(shopifyCart);
    info = await shippingProtection.getInfo({cartData});
  }

  if (!checked || info.isExcluded) {
    return null;
  }

  return {
    ...info,
    variantGid: toVariantGid(info.variantsId),
  };
}

async function checkout() {
  const protection = await prepareProtectionForCheckout();

  const checkoutUrl = protection
    ? await addProtectionAndGetCheckoutUrl(protection.variantGid)
    : await getCheckoutUrl();

  window.location.assign(checkoutUrl);
}

After adding the protection variant, redirect using the checkout URL returned by that same cart mutation.

Do not add protection, publish the updated cart back to the cart page, and remain there. On a later cart-page load, remove the old protection line, keep the shopper checked, and calculate a fresh variant at checkout.

Shopify Storefront GraphQL

Your Shopify integration needs three operations:

  • Read the current cart.
  • Remove existing protection lines by cart line ID.
  • Add one protection variant and return the resulting checkout URL.

Match existing protection lines by info.productId, not only by variant ID. The correct variant may change when the eligible cart total changes.

Add protection

mutation AddShippingProtection(
  $cartId: ID!
  $lines: [CartLineInput!]!
) {
  cartLinesAdd(cartId: $cartId, lines: $lines) {
    cart {
      id
      checkoutUrl
    }
    userErrors {
      field
      message
    }
  }
}
{
  "cartId": "gid://shopify/Cart/your-cart-id",
  "lines": [
    {
      "merchandiseId": "gid://shopify/ProductVariant/123456789",
      "quantity": 1
    }
  ]
}

Remove existing protection

mutation RemoveShippingProtection(
  $cartId: ID!
  $lineIds: [ID!]!
) {
  cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
    cart {
      id
      checkoutUrl
    }
    userErrors {
      field
      message
    }
  }
}

Always inspect Shopify userErrors before updating local state or redirecting.

Using the SDK with frameworks

Vanilla JavaScript

Initialize the SDK after your application starts, store checked in a module or state object, and call getInfo() after every cart update. The examples above can be used directly without a framework.

Vue

Initialize the SDK from onMounted(), keep checked, info, and pending flags in ref() values, and watch the current cart:

import {onMounted, ref, watch} from "vue";
import {
  shippingProtection,
  type ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";

const checked = ref(false);
const info = ref<ShippingProtectionInfo | null>(null);

onMounted(async () => {
  await shippingProtection.init(initParams);
  await refreshProtection();
});

watch(
  () => cart.value,
  () => void refreshProtection(),
  {deep: true},
);

async function refreshProtection() {
  info.value = await shippingProtection.getInfo({
    cartData: toSdkCartData(cart.value),
  });
}

Apply the checked-state and existing-line rules from the main integration flow inside refreshProtection().

Svelte

Call init() from onMount(), store the latest result in component state, and call getInfo() from your cart update handler:

import {onMount} from "svelte";
import {
  shippingProtection,
  type ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";

let checked = false;
let info: ShippingProtectionInfo | null = null;

onMount(async () => {
  await shippingProtection.init(initParams);
  info = await shippingProtection.getInfo({
    cartData: toSdkCartData(cart),
  });
});

Angular, Solid, and other frameworks

Initialize once in the browser, map the framework's cart state to SdkCartData, and keep checked, info, errors, and pending operations in the framework's normal state primitives. The SDK API is plain asynchronous JavaScript.

SSR applications

Use the SDK from client-side lifecycle code. Do not run init() or getInfo() during the server render.

For Next.js App Router, place the integration behind a Client Component:

"use client";

import {shippingProtection} from "captain-shipping-protection-sdk";

For Hydrogen, Remix, Nuxt, SvelteKit, or other SSR frameworks, call the SDK after the component mounts or from browser-only cart code. Server loaders may fetch the Shopify cart, but pass the mapped cart data to the client before calling the SDK.

Content Security Policy

The SDK sends configuration and eligibility requests to:

https://insurance.captaintop.com

If your storefront has a Content Security Policy, add this origin to connect-src:

connect-src 'self' https://insurance.captaintop.com;

The SDK does not load an external script, iframe, font, or stylesheet. Do not add broad CSP allowances such as * or 'unsafe-inline' for this package.

API reference

shippingProtection.init(params)

Loads the merchant configuration and stores the active SDK context.

Parameters:

  • shop: Shopify domain, for example example.myshopify.com.
  • country: shopper country code used for eligibility, for example US.
  • locale: storefront locale, for example en.
  • currency: active ISO currency code, for example USD.

Returns Promise<void>.

shippingProtection.getInfo({cartData})

Calculates the current protection product, variant, price, inclusion state, and eligibility.

Returns Promise<ShippingProtectionInfo>.

The method requires a successful init() first.

shippingProtection.setting

The active SdkCartSetting, or null before initialization. It contains merchant copy, style values, price configuration, default opt-in state, claims guide values, and exclusion configuration.

Treat the setting as read-only application data. Re-run init() instead of mutating it.

Cart data reference

interface SdkCartData {
  token: string;
  currency: string;
  total_price: number;
  item_count: number;
  items: SdkCartDataItem[];
}

interface SdkCartDataItem {
  id: number;
  quantity: number;
  variant_id: number;
  key: string;
  product_id: number;
  final_line_price?: number;
  title?: string;
  product_title?: string;
  sku?: string;
}

Field requirements:

  • total_price: full cart subtotal in integer minor units.
  • items: all current cart lines, including an existing protection line.
  • product_id: numeric Shopify product ID. Eligibility and existing protection detection depend on it.
  • variant_id: numeric Shopify variant ID. Exclusion rules depend on it.
  • final_line_price: line total in integer minor units. It is needed to remove protection and excluded products from eligible-total calculations.
  • title, product_title, and sku: sent for remote eligibility checks when required by the merchant's exclusion configuration.
  • token, currency, item_count, id, key, and quantity: required by the public cart contract for compatibility with Shopify cart-shaped data. The current quote calculation does not read all of these fields directly.

Common exclusion reasons

  • empty_cart: the cart contains no lines.
  • invalid_cart: at least one line does not have a valid product ID.
  • only_shipping_protection: no non-protection product remains.
  • excluded_variant: the cart contains a merchant-configured excluded variant.
  • check_display_hidden: Captain's eligibility response hides protection.

Exclusion reason values are extensible. Do not assume the list above is exhaustive.

Error handling

Recommended behavior:

  • Catch initialization errors and continue checkout without protection.
  • Catch getInfo() errors and hide or disable your protection UI.
  • Track the latest cart request and ignore stale asynchronous responses.
  • Prevent concurrent removal and checkout operations.
  • Keep the shopper checked when removal of an existing line fails, but block adding another protection line.
  • Inspect Shopify userErrors separately from SDK request errors.
try {
  await shippingProtection.init(initParams);
  const info = await shippingProtection.getInfo({cartData});
  renderProtection(info);
} catch (error) {
  console.error("Shipping Protection is unavailable", error);
  hideProtection();
}

Integration checklist

Before deploying:

  • Install captain-shipping-protection-sdk.
  • Confirm the store has an active Captain Shipping Protection configuration.
  • Confirm shop uses the example.myshopify.com format.
  • Initialize with the active country, locale, and currency.
  • Convert Shopify GIDs to numeric IDs.
  • Pass all cart prices as integer minor units.
  • Include existing protection lines in SdkCartData.
  • Call getInfo() after every cart change.
  • Keep checked state in the host application.
  • Remove existing protection by product ID and keep checked enabled.
  • Call getInfo() again immediately before checkout.
  • Add exactly one unit of the latest eligible variant.
  • Redirect using the checkout URL from the updated Shopify cart.
  • Add https://insurance.captaintop.com to CSP connect-src when CSP is enabled.
  • Test checked, unchecked, excluded, empty-cart, stale-request, API-failure, and cart-mutation-failure flows.

Troubleshooting

ShippingProtection SDK has not been initialized

Wait for shippingProtection.init() to resolve before calling getInfo().

The widget does not appear

This package does not render a widget. Render your own UI when info.isExcluded is false, or use captain-shipping-protection-react for Captain's standard React widget.

The protection price does not update

Call getInfo() with a newly mapped cart after every quantity, product, discount, or currency change. Verify that all prices use integer minor units.

The cart contains duplicate protection lines

Before checkout, remove lines whose product ID matches info.productId, refresh the cart, and call getInfo() again. Add only one unit of the returned variant.

Requests are blocked by CSP

Allow https://insurance.captaintop.com in connect-src.

The returned variant cannot be added to Shopify

Convert a numeric variantsId to gid://shopify/ProductVariant/<variantsId>. Also confirm the protection product is published to the Storefront sales channel.

Updating

Check the installed version:

npm list captain-shipping-protection-sdk

Update to the latest version:

npm install captain-shipping-protection-sdk@latest