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

@structdk/extension-sdk

v1.2.0

Published

Framework agnostic SDK for building Struct extensions

Readme

@structdk/extension-sdk

SDK for building extensions for Struct PIM. Extensions run as iframes embedded inside the Struct PIM UI and communicate with the host via a postMessage-based API. This SDK abstracts that messaging layer into a simple, typed API.

Full developer documentation: docs.struct.com

Installation

npm install @structdk/extension-sdk

Setup

Create an SDK instance using createStructSDK. Optionally pass the hostOrigin of your Struct PIM instance to restrict message acceptance to that origin only.

import { createStructSDK } from '@structdk/extension-sdk';

const struct = createStructSDK({ hostOrigin: 'https://your-struct-instance.struct.com' });

If hostOrigin is not provided, all origins are accepted and a warning is printed to the console.

The returned object exposes actions, events, and a destroy() cleanup function.


Actions (struct.actions)

Actions are messages sent from your extension to Struct PIM.

getContext

Requests the current context from Struct PIM. Returns a promise that resolves with the context payload for the current extension point. This is the primary way to retrieve the session context (slug, language, user, entity info, etc.).

import { createStructSDK } from '@structdk/extension-sdk';
import type { TabContextPayload } from '@structdk/extension-sdk';

const struct = createStructSDK();
const ctx = await struct.actions.getContext<TabContextPayload>();
console.log('Entity:', ctx.entityType, ctx.entityId);
console.log('Slug:', ctx.slug);

An optional GetContextOptions object can be passed to override the default timeout (10 000 ms):

const ctx = await struct.actions.getContext<TabContextPayload>({ timeoutMs: 5000 });

The context payload type depends on the extension point. Use the appropriate typed payload for full type safety:

| Extension point | Payload type | Extra fields | |-------------------|-------------------------------|-----------------------------------------------| | Tab | TabContextPayload | entityId, entityType | | Section | SectionContextPayload | entityId, entityType | | Sidebar widget | SidebarWidgetContextPayload | entityId, entityType | | Property | PropertyContextPayload | entityId, entityType | | Exporter | ExporterContextPayload | selectedEntityIds, selectedEntityType | | Search action | SearchActionContextPayload | selectedEntityIds, selectedEntityType | | Page | PageContextPayload | (none) | | Widget | WidgetContextPayload | widgetUid, isSettingsMode | | Import | ImportContextPayload | (none) |

All payload types extend BaseContextPayload, which includes: messagingVersion, slug, currentLanguage?, currentUser?, currentSegments?.

Widget context

A widget extension is rendered once per dashboard widget, and the same app can be added to a dashboard more than once. WidgetContextPayload carries two extra fields so an app can tell those renders apart and store configuration against a single one of them:

import type { WidgetContextPayload } from '@structdk/extension-sdk';

const ctx = await struct.actions.getContext<WidgetContextPayload>();

if (ctx.isSettingsMode) {
  renderConfigurationForm(ctx.widgetUid);
} else {
  renderWidget(await loadConfiguration(ctx.widgetUid));
}
  • widgetUid identifies the dashboard widget the app is rendered in. It is stable for the lifetime of that widget and differs between two copies of the same app on one dashboard, which makes it a suitable key for per-widget configuration stored on your own backend. A modal opened with openModal inherits the widget context of the extension that opened it.

  • isSettingsMode is true while the app renders inside the widget settings surface - both the configure step shown when a user adds the widget and the settings dialog of an existing widget - and false on the dashboard itself. Render your configuration UI when it is true. The value is fixed for the lifetime of the iframe and never changes underneath you.

Three things to be aware of when you store configuration per widget:

  1. widgetUid is an identifier, not a credential. It reaches your page over postMessage, so your backend cannot verify that a caller really is that widget. Authorize requests with getToken() and scope stored configuration by the token's tenantSlug, treating the uid as untrusted input.

  2. A widget uid can refer to a widget that is never saved. A user can configure your app while adding a widget and then leave without saving the dashboard, and Struct sends no signal when a widget is deleted. Expect configuration you will never hear about again, and do not rely on a deletion event.

  3. Struct's own Cancel and Update buttons do not roll back your configuration. They apply only to the widget properties Struct owns. If your settings UI writes immediately, that write has already happened by the time the user presses Cancel - give your settings UI its own save affordance so the user understands when their configuration is stored.

Configuration keyed on widgetUid is shared exactly as widely as the dashboard the widget lives on: a widget on a personal dashboard is seen by one user, while a widget on a shared dashboard is seen by everyone who has that dashboard.


getToken

Requests a short-lived signed token (JWT) from Struct PIM for the current app. Returns a promise that resolves with the token payload.

The token is minted server-side and signed with the app's client secret. It can be verified with the same client secret to confirm it originates from a genuine Struct embedded session for your tenant.

const { token, expiresAt, issuedAt } = await struct.actions.getToken();

The resolved TokenProvidedPayload contains:

| Field | Type | Description | |-------------|----------|--------------------------------------| | token | string | Signed HS256 JWT | | expiresAt | number | Expiry as Unix time in seconds | | issuedAt | number | Issued-at as Unix time in seconds |

An optional GetTokenOptions object can be passed to override the default timeout (10 000 ms):

const { token } = await struct.actions.getToken({ timeoutMs: 5000 });

The promise rejects as soon as Struct PIM reports it cannot mint a token, carrying the reason it gave. If the host does not answer at all, the promise rejects on timeout with a description of what the SDK could observe — whether the page is embedded, the configured host origin, and whether another script or browser extension has replaced window.postMessage or window.addEventListener.


openModal

Opens a modal inside the Struct PIM UI, rendering a URL in an iframe.

struct.actions.openModal({
  id: 'my-modal',
  url: 'https://your-extension.example.com/modal',
  placement: 'center', // 'center' | 'left' | 'right'
  size: 'medium',      // 'small' | 'medium' | 'large'
});

closeModal

Closes an open modal by its ID.

struct.actions.closeModal({ id: 'my-modal' });

closeHostContainer

Asks the Struct PIM host to close the dialog that is currently hosting your extension iframe (e.g. the export-entities dialog or a search-action dialog). Use this when your extension has finished its work and the surrounding host dialog should be dismissed.

struct.actions.closeHostContainer();

Only supported on the Exporter and SearchAction extension points — these are the only extension points whose iframes are embedded inside a dismissible host dialog. On any other extension point the host fires onActionRejectedEvent with reason Action "CLOSE_HOST_CONTAINER" is not supported for extension point type "<type>".

Not to be confused with closeModal, which only closes a modal that the same extension previously opened via openModal. closeHostContainer closes the host dialog that contains the extension iframe itself.


showSnackbarMessage

Displays a snackbar notification in the Struct PIM UI.

struct.actions.showSnackbarMessage({
  message: 'Changes saved successfully',
  placement: 'bottom', // 'top' | 'bottom'
  isError: false,
  durationMs: 3000,    // optional
});

resizeContainer

Manually sets the iframe container height in pixels. Supported on Tab, Section, Property, and Sidebar Widget extension points.

struct.actions.resizeContainer({ height: 400 });

refreshEditor

Asks Struct PIM to refresh the editor that is currently hosting the extension iframe. Re-fetches the editor model, breadcrumbs and thumbnail without a page reload. If the user has unsaved changes, the host prompts them to confirm before discarding their work — if they cancel, no refresh happens and the action completes silently.

struct.actions.refreshEditor();

Only supported on the Tab, Section, Property, and SidebarWidget extension points — these are the only ones that live inside an editor. On any other extension point the host fires onActionRejectedEvent with reason Action "REFRESH_EDITOR" is not supported for extension point type "<type>".


enableAutoResize

Automatically adjusts the iframe height based on content using a ResizeObserver. Only one auto-resize observer can be active at a time. Supported on Tab, Section, Property, and Sidebar Widget extension points.

struct.actions.enableAutoResize({
  maxHeight: 800,    // optional — cap the height
  debounceMs: 200,   // optional — debounce delay (default 100ms)
});

setIsDirtyState

Notifies Struct PIM whether the extension currently has unsaved changes. While any extension reports isDirty: true, the surrounding editor will show its standard "you have unsaved changes" confirmation dialog if the user attempts to navigate away (which would destroy the iframe and lose those changes).

Call with isDirty: true when the user starts editing, and with isDirty: false once changes are saved or discarded.

struct.actions.setIsDirtyState({ isDirty: true });
// ...later, after saving:
struct.actions.setIsDirtyState({ isDirty: false });

Only supported on Tab, Section, and Property extension points. On any other extension point the host fires onActionRejectedEvent with reason Action "SET_IS_DIRTY_STATE" is not supported for extension point type "<type>".


navigate

Navigates the Struct PIM host to a relative path within the PIM. The host resolves the path against the current tenant origin — extensions do not need to know the tenant host.

struct.actions.navigate({ path: '/pim/search' });

path must be a relative, app-absolute path starting with a single / (e.g. /pim/search). Absolute URLs (https://…) and protocol-relative paths (//host) are rejected by the host so an extension cannot drive the top window to a foreign origin — a rejected navigation surfaces an error to the user.

Use navigateToEntity instead when navigating to the editor for a specific entity — it resolves the route from the entity type and ID, so your extension doesn't depend on the host's route structure.


navigateToEntity

Navigates the Struct PIM host to the editor for a given entity. The host resolves the URL from entityType + entityId — extensions do not need to know the host's route structure.

struct.actions.navigateToEntity({
  entityType: 'Product',
  entityId: '7d3f...e91',
});

Supported entity types: Product, Variant, VariantGroup, Category, Asset, GlobalList, GlobalListValue.


Events (struct.events)

Events are messages sent from Struct PIM to your extension. Each listener returns an unsubscribe function.

onEntityChangedEvent

Fired whenever the entity the extension is associated with is saved/updated. Relevant for tab, section, sidebar widget, and property extension points.

const unsubscribe = struct.events.onEntityChangedEvent((payload) => {
  console.log(payload.entityType, 'with id', payload.entityId, 'changed');
});

onLanguageChangedEvent

Fired whenever the user switches the active language in Struct PIM. Use this to re-fetch localized data or update the UI.

struct.events.onLanguageChangedEvent((payload) => {
  console.log('Language changed to:', payload.currentLanguage);
});

onSegmentChangedEvent

Fired whenever the active segment selection changes in Struct PIM.

struct.events.onSegmentChangedEvent((payload) => {
  console.log('Segments changed:', payload.currentSegments);
});

onActionRejectedEvent

Fired when the host rejects an action (e.g., a resize on an unsupported extension point).

struct.events.onActionRejectedEvent((payload) => {
  console.warn(`Action rejected: ${payload.rejectedAction} — ${payload.reason}`);
});

Cleanup

Call destroy() to unsubscribe all event listeners and stop any active auto-resize observer.

struct.destroy();

Quick start

A minimal extension that retrieves its context and listens for changes:

import { createStructSDK } from '@structdk/extension-sdk';
import type { TabContextPayload } from '@structdk/extension-sdk';

// 1. Create SDK instance (optionally lock to a specific origin)
const struct = createStructSDK({ hostOrigin: 'https://your-struct-instance.struct.com' });

// 2. Request context from the host
const ctx = await struct.actions.getContext<TabContextPayload>();
console.log('Entity:', ctx.entityType, ctx.entityId);
console.log('Slug:', ctx.slug);

// 3. Auto-resize the iframe to fit content
struct.actions.enableAutoResize({ maxHeight: 800 });

// 4. Register event listeners for ongoing changes
struct.events.onEntityChangedEvent((payload) => {
  console.log('Entity updated:', payload.entityId);
});

struct.events.onLanguageChangedEvent((payload) => {
  console.log('Language:', payload.currentLanguage);
});

// 5. Clean up when done
struct.destroy();

Types

All payload types are exported from the package root:

import type {
  // SDK options
  StructSDKOptions,
  // Context payloads
  BaseContextPayload,
  TabContextPayload,
  SectionContextPayload,
  SidebarWidgetContextPayload,
  PropertyContextPayload,
  ExporterContextPayload,
  SearchActionContextPayload,
  PageContextPayload,
  WidgetContextPayload,
  ImportContextPayload,
  // Event payloads
  EntityChangedPayload,
  LanguageChangedPayload,
  SegmentChangedPayload,
  ActionRejectedPayload,
  TokenProvidedPayload,
  // Action payloads
  OpenModalPayload,
  CloseModalPayload,
  CloseHostContainerPayload,
  ShowSnackbarMessagePayload,
  ResizeContainerPayload,
  RefreshEditorPayload,
  SetIsDirtyStatePayload,
  NavigatePayload,
  NavigateToEntityPayload,
  // Options
  AutoResizeOptions,
  GetContextOptions,
  GetTokenOptions,
} from '@structdk/extension-sdk';

The StructEntityType enum is also exported:

import { StructEntityType } from '@structdk/extension-sdk';
// StructEntityType.Product | .Category | .Variant | .VariantGroup | .Asset

Versioning

The SDK version is available at runtime via ExtensionSdkVersion:

import { ExtensionSdkVersion } from '@structdk/extension-sdk';

console.log('SDK version:', ExtensionSdkVersion);

Changelog

[1.2.0] - 2026-10-01

Added

  • WidgetContextPayload now carries widgetUid and isSettingsMode, so a widget extension can tell which dashboard widget it renders in and whether it is being shown on the dashboard or in the widget settings surface. Together they let an app store configuration scoped to a single widget on its own backend. widgetUid is a render-slot identifier rather than a credential: authorize with getToken() and scope stored configuration by the token's tenantSlug. See "Widget context" in the README for the full rules, including the fact that a widget uid can refer to a widget that is never saved.

[1.1.0] — 2026-08-11

Changed

  • getToken() and getContext() now reject as soon as Struct PIM reports it cannot serve the request, instead of waiting out the timeout. The rejection carries the host's reason, e.g. Struct PIM rejected getToken: Failed to mint a token for this app instance. A rejection addressed to a different action is ignored.
  • Timeout errors now describe what the SDK could observe rather than guessing at a cause: whether the page is embedded at all, the configured host origin, and whether another script or browser extension has replaced window.postMessage or window.addEventListener. The previous wording ("is the SDK running inside a Struct PIM iframe?") was reported even when the SDK was correctly embedded.

[1.0.8] — 2026-06-03

Added

  • struct.actions.navigate({ path }) — navigate the Struct PIM host to a relative path within the PIM (e.g. /pim/search). The host resolves the path against the current tenant origin, so extensions do not need to know the tenant host. Absolute and protocol-relative URLs are rejected. Supported on all extension points.
  • struct.actions.getToken() — request a short-lived signed token (JWT) from Struct PIM for the current app. The token is minted server-side and signed with the app's client secret, and can be verified with the same secret to confirm it originates from a genuine Struct embedded session. Resolves with { token, expiresAt, issuedAt }. Supported on all extension points.

[1.0.7] — 2026-05-26

Added

  • struct.actions.navigateToEntity({ entityType, entityId }) — navigate the Struct PIM host to the editor for a given entity. Supported entity types: Product, Variant, VariantGroup, Category, Asset, GlobalList, GlobalListValue.
  • struct.actions.refreshEditor() — ask the host to refresh the editor that is hosting the extension iframe. Re-fetches the editor model, breadcrumbs and thumbnail without a page reload. If the user has unsaved changes the host prompts them to confirm before discarding their work. Supported on Tab, Section, Property, and SidebarWidget extension points.
  • struct.actions.setIsDirtyState({ isDirty }) — notify the host whether the extension currently has unsaved changes, so the surrounding editor can show its standard unsaved-changes confirmation dialog when the user attempts to navigate away. Supported on Tab, Section, and Property extension points.

[1.0.6] — 2026-04-08

Added

  • struct.actions.closeHostContainer() — ask the host to dismiss the dialog hosting the extension iframe. Supported on the Exporter and SearchAction extension points.

[1.0.5] — 2026-03-23

Changed

  • README rewritten to document the post-createStructSDK API surface (actions, events, context payload types, cleanup).

[1.0.4] — 2026-03-19

Added

  • createStructSDK(options?) factory — returns the { actions, events, destroy } object that is now the SDK's primary entry point. Replaces the previous module-level action/event imports.
  • struct.actions.enableAutoResize({ maxHeight?, debounceMs? }) — auto- adjusts the iframe height based on content via a ResizeObserver.
  • struct.events.onActionRejectedEvent(handler) — fired when the host rejects an action (e.g. an unsupported action for the current extension point type).
  • struct.destroy() — unsubscribes all event listeners and stops any active auto-resize observer.
  • StructSDKOptions.hostOrigin — when provided, the SDK only accepts messages from that origin. If omitted, all origins are accepted and a warning is logged.

[1.0.3] — 2026-03-18

Changed

  • Breaking: Initialisation switched from push-based to pull-based. Extensions now call struct.actions.getContext() (returns a Promise) instead of subscribing to onInitEvent. This removes the race between iframe load and the host's init message.

Removed

  • onInitEvent — replaced by getContext.

[1.0.1] — 2026-03-12

Fixed

  • Internal messaging and packaging fixes following the initial publish.

[1.0.0] — 2026-03-10

Added

  • Initial public release.
  • openModal, closeModal, showSnackbarMessage, resizeContainer actions.
  • onInitEvent for receiving the extension's startup context from the host (replaced in 1.0.3).

License

MIT