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

@kopra-dev/sdk

v0.2.0

Published

Kopra JavaScript/TypeScript SDK - embed custom field editors and tenant configuration

Downloads

288

Readme

@kopra-dev/sdk

Embed custom field editors and tenant configuration into any web app.

Install

npm install @kopra-dev/sdk
# or
yarn add @kopra-dev/sdk
# or
pnpm add @kopra-dev/sdk

For use without a build step, include the IIFE bundle via a <script> tag. This exposes Kopra as a global namespace:

<script src="dist/index.global.js"></script>
<script>
  const { KopraSDK } = Kopra;
  const sdk = new KopraSDK({ /* ... */ });
</script>

Quick Start

import { KopraSDK } from '@kopra-dev/sdk';

const sdk = new KopraSDK({
  currentTenant: 'northstar-staffing',
  getToken: async (req) => {
    const res = await fetch('/api/kopra-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(req),
    });
    return res.json();
  },
  onFieldsSaved: (values) => console.log('Saved:', values),
  onFieldsError: (err) => console.error('Error:', err),
});

// Load the field editor - values autosave after 2s of inactivity
await sdk.loadCustomFields('editor-container', {
  fieldGroupKey: 'customer',
  entityId: 'contact-123',
});

// Load the tenant config panel so customers can manage their own fields
await sdk.loadFieldConfiguration('config-container', {
  fieldGroupKey: 'customer',
  fieldLimit: 10,
});
<div id="editor-container"></div>
<div id="config-container"></div>

Backend Setup (Required)

Your API key must never reach the browser. Create a backend endpoint that proxies token requests to Kopra. The SDK calls your endpoint via the getToken callback.

Express / Node.js

// server.ts
import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/kopra-token', async (req, res) => {
  const response = await fetch(
    `${process.env.KOPRA_URL}/api/auth/token`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.KOPRA_API_KEY!,
      },
      body: JSON.stringify({
        tenantId: req.body.tenantId,
        fieldGroupKey: req.body.fieldGroupKey,
        fieldLimit: req.body.fieldLimit,
      }),
    },
  );

  if (!response.ok) {
    return res.status(response.status).json({ error: 'Token request failed' });
  }

  const json = await response.json();
  // Kopra wraps the payload in { success, data }
  res.json(json.data);
});

app.listen(3000);

The response from POST /api/auth/token looks like:

{
  "success": true,
  "data": {
    "token": "eyJ...",
    "fieldEditorUrl": "https://your-kopra.com/embed/field-editor",
    "tenantConfigUrl": "https://your-kopra.com/embed/tenant-config",
    "fieldsGroupId": "uuid-of-resolved-field-group",
    "expiresAt": "2026-03-28T12:00:00.000Z"
  }
}

Your backend returns json.data directly to the SDK. The SDK expects the TokenResponse shape:

interface TokenResponse {
  token: string;
  fieldEditorUrl: string;
  tenantConfigUrl: string;
  fieldsGroupId?: string;
  expiresAt?: string;
}

Python / Flask

from flask import Flask, jsonify, request
import requests, os

app = Flask(__name__)
KOPRA_URL = os.environ['KOPRA_URL']
KOPRA_API_KEY = os.environ['KOPRA_API_KEY']

@app.post('/api/kopra-token')
def kopra_token():
    resp = requests.post(
        f'{KOPRA_URL}/api/auth/token',
        json=request.get_json() or {},
        headers={'Content-Type': 'application/json', 'X-API-Key': KOPRA_API_KEY},
        timeout=10,
    )
    if not resp.ok:
        return jsonify({'error': 'Token request failed'}), resp.status_code
    return jsonify(resp.json().get('data', {}))

Go

http.HandleFunc("/api/kopra-token", func(w http.ResponseWriter, r *http.Request) {
    req, _ := http.NewRequest("POST", os.Getenv("KOPRA_URL")+"/api/auth/token", r.Body)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", os.Getenv("KOPRA_API_KEY"))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        http.Error(w, `{"error":"Could not reach Kopra"}`, http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()

    var result struct {
        Data json.RawMessage `json:"data"`
    }
    json.NewDecoder(resp.Body).Decode(&result)
    w.Header().Set("Content-Type", "application/json")
    w.Write(result.Data)
})

Full runnable examples: Python | Go | PHP

See /api/docs on your Kopra instance for the full API reference.

Configuration

All options for KopraSDKConfig:

| Option | Type | Required | Description | |--------|------|----------|-------------| | currentTenant | string | Yes | Tenant ID that scopes all operations. | | getToken | (req: TokenRequest) => Promise<TokenResponse> | Recommended | Async callback that calls your backend to get a Kopra token. | | apiKey | string | Dev only | Calls Kopra's token endpoint directly from the browser. Logs a warning. Never ship to production. | | backendUrl | string | Only with apiKey | Base URL of the Kopra server. Not needed when using getToken. | | endpoints | { token?: string } | No | Override default API paths. Default token path: /api/auth/token. | | onFieldsSaved | (values: Record<string, unknown>) => void | No | Called after field values are saved via the editor. | | onFieldsError | (error: unknown) => void | No | Called on field editor errors. | | onConfigSaved | (data: unknown) => void | No | Called after tenant config changes are saved. | | onConfigError | (error: unknown) => void | No | Called on config panel errors. | | autosave | AutosaveConfig | No | Autosave configuration (see Autosave). | | debug | boolean | No | Log SDK internals to the console. Default: false. |

The TokenRequest passed to your getToken callback:

interface TokenRequest {
  tenantId: string;
  fieldGroupKey?: string;
  fieldLimit?: number;
}

Methods

loadCustomFields(containerId, options)

Embeds the field-value editor into the specified container. End users fill in field values here.

await sdk.loadCustomFields('my-container', {
  fieldGroupKey: 'customer',  // which field group to load
  entityId: 'contact-123',    // the entity these values belong to
  initialHeight: '300px',     // optional, default '100px'
  theme: { /* ThemeConfig */ },// optional
});

loadFieldConfiguration(containerId, options)

Embeds the tenant field configuration panel. Tenants can add, edit, and delete their own custom fields.

await sdk.loadFieldConfiguration('config-container', {
  fieldGroupKey: 'customer',
  fieldLimit: 10,             // optional, max fields the tenant can create
  height: '600px',            // optional, default '600px'
});

saveFields(options?)

Programmatically trigger a save of the current field values. Returns a SaveFieldsResult.

const result = await sdk.saveFields({
  timeout: 10000,              // optional, ms to wait (default 10000)
  dryRun: false,               // optional, validate without saving
  validateBeforeSave: true,    // optional, run validation first
});

if (result.success) {
  console.log('Values:', result.values);
} else {
  console.log('Errors:', result.errors);
}

getFieldValues()

Get the current field values from the editor without saving.

const values = await sdk.getFieldValues();
// { company_size: '51-200', industry: 'Healthcare' }

validateFields()

Validate the current field values without saving.

const { valid, errors } = await sdk.validateFields();
if (!valid) console.log('Validation errors:', errors);

enableAutosave(options?)

Enable autosave at runtime. See Autosave.

disableAutosave()

Disable autosave.

updateConfig(newConfig)

Update SDK configuration. Destroys existing embeds - call loadCustomFields / loadFieldConfiguration again after.

sdk.updateConfig({ currentTenant: 'new-tenant-456' });

destroy()

Clean up all iframes, event listeners, and timers.

sdk.destroy();

getDefaultTheme()

Returns the built-in default ThemeConfig object (frozen/immutable). Useful as a starting point for customization.

const base = sdk.getDefaultTheme();

Events

The SDK communicates with embedded iframes via PostMessage. You can listen for these events on window:

Field Editor Events

| Event | Description | |-------|-------------| | kopra-field-values-saved | Field values were saved successfully. | | kopra-field-values-error | An error occurred while saving. | | kopra-field-value-changed | A single field value changed (triggers autosave). | | kopra-resize | The editor iframe resized. |

Tenant Config Events

| Event | Description | |-------|-------------| | kopra-tenant-field-saved | A tenant field was created. | | kopra-tenant-field-updated | A tenant field was updated. | | kopra-tenant-field-deleted | A tenant field was deleted. | | kopra-tenant-field-error | An error occurred in the config panel. | | kopra-config-resize | The config iframe resized. |

window.addEventListener('message', (e) => {
  if (e.data.type === 'kopra-tenant-field-saved') {
    console.log('New field:', e.data.fieldData);
  }
});

Event constants are also exported for type safety:

import { PostMessageEvents } from '@kopra-dev/sdk';

// PostMessageEvents.TENANT_FIELD_SAVED === 'kopra-tenant-field-saved'

Theming

Pass a theme object to loadCustomFields to match your app's design. Every element supports both inline styles and CSS class names.

await sdk.loadCustomFields('container', {
  fieldGroupKey: 'customer',
  entityId: 'contact-123',
  theme: {
    container: { display: 'flex', flexDirection: 'column', gap: '16px' },
    label: { fontSize: '14px', fontWeight: '600', color: '#1e293b' },
    input: {
      width: '100%',
      padding: '10px 12px',
      border: '1px solid #cbd5e1',
      borderRadius: '8px',
    },
    inputFocus: {
      outline: 'none',
      borderColor: '#3b82f6',
      boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
    },
    select: { /* same pattern as input */ },
    textarea: { /* same pattern as input */ },
    fieldGlobal: { borderLeft: '3px solid #3b82f6', paddingLeft: '10px' },
    fieldTenant: { borderLeft: '3px solid #10b981', paddingLeft: '10px' },
    saveButton: { display: 'none' }, // hide to use programmatic save
  },
});

ThemeConfig keys

Each key accepts a CSSStyles object (camelCase CSS properties). Keys ending in Class accept a CSS class name string instead.

| Key | Class variant | Description | |-----|---------------|-------------| | container | containerClass | Outer wrapper around all fields. | | fieldContainer | fieldContainerClass | Wrapper for each field (label + input). | | fieldContainerSingle | fieldContainerSingleClass | Fields that span the full width (textarea, json). | | fieldGlobal | fieldGlobalClass | Fields defined globally by the SaaS owner. | | fieldTenant | fieldTenantClass | Fields defined by the tenant. | | label | labelClass | Field labels. | | input | inputClass | Text/number/date/email/url inputs. | | inputFocus | - | Styles applied on input focus. | | select | selectClass | Select dropdowns. | | textarea | textareaClass | Textarea fields. | | saveButton | saveButtonClass | The save button (set display: 'none' to hide). |

Both fooClass and foo (inline styles) can be set together. Inline styles take precedence on conflict.

Saving

Autosave is enabled by default. Field values save automatically after 2 seconds of inactivity. The embedded editor shows a "Saving..." / "Changes saved" status indicator.

You can also save programmatically at any time:

const result = await sdk.saveFields();
// { success: true, values: { ... } }

Autosave Configuration

Autosave can be customized or disabled:

const sdk = new KopraSDK({
  currentTenant: 'northstar-staffing',
  getToken: myGetToken,
  autosave: {
    enabled: true,              // default: true
    debounceMs: 2000,           // default: 2000
    validateBeforeSave: true,   // default: true
    onAutosaveStart: () => showSpinner(),
    onAutosaveSuccess: (values) => hideSpinner(),
    onAutosaveError: (err) => showToast(err),
  },
});

// Disable autosave to use manual saves only
sdk.disableAutosave();

// Re-enable with custom settings
sdk.enableAutosave({ debounceMs: 1500 });

AutosaveConfig

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | true | Whether autosave is active. | | debounceMs | number | 2000 | Milliseconds to wait after the last change before saving. | | validateBeforeSave | boolean | true | Run validation before autosaving. Skips save on errors. | | onAutosaveStart | () => void | noop | Called when an autosave begins. | | onAutosaveSuccess | (values) => void | noop | Called after a successful autosave. | | onAutosaveError | (error: string) => void | noop | Called when autosave fails. |

Field Types

Kopra supports 12 field types:

| Type | Input | Description | |------|-------|-------------| | string | Text input | Short text. | | text | Text input | Alias for string. | | number | Number input | Numeric value. | | boolean | Checkbox | True/false. | | date | Date picker | ISO date string. | | email | Email input | Validated email address. | | url | URL input | Validated URL. | | select | Dropdown | Single selection from predefined options. | | multiselect | Multi-select | Multiple selections from predefined options. | | enum | Dropdown | Alias for select. | | textarea | Textarea | Multi-line text. | | json | Textarea | Freeform JSON. |

Fields are configured with a schema object when creating global or tenant fields via the API:

{
  "type": "select",
  "validation": {
    "required": true,
    "options": ["Option A", "Option B"]
  },
  "uiHints": {
    "placeholder": "Select...",
    "helpText": "Choose the best match"
  }
}

Framework Examples

Each example below wraps the SDK in a framework-specific lifecycle. For full configuration options, see Configuration and Quick Start above.

React

import { useEffect, useRef } from 'react';
import { KopraSDK } from '@kopra-dev/sdk';

function useKopra(containerId: string, fieldGroupKey: string, entityId: string) {
  const sdkRef = useRef<KopraSDK | null>(null);

  useEffect(() => {
    const sdk = new KopraSDK({
      currentTenant: 'northstar-staffing',
      getToken: async (req) => {
        const res = await fetch('/api/kopra-token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(req),
        });
        return res.json();
      },
    });

    sdkRef.current = sdk;
    sdk.loadCustomFields(containerId, { fieldGroupKey, entityId });

    return () => sdk.destroy();
  }, [containerId, fieldGroupKey, entityId]);

  return sdkRef;
}

// Usage: <div id="editor" /> + useKopra('editor', 'customer', 'contact-123')

Vue 3

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { KopraSDK } from '@kopra-dev/sdk';

const container = ref<HTMLElement | null>(null);
let sdk: KopraSDK | null = null;

onMounted(() => {
  sdk = new KopraSDK({
    currentTenant: 'northstar-staffing',
    getToken: async (req) => {
      const res = await fetch('/api/kopra-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(req),
      });
      return res.json();
    },
  });

  sdk.loadCustomFields('kopra-editor', {
    fieldGroupKey: 'customer',
    entityId: 'contact-123',
  });
});

onUnmounted(() => sdk?.destroy());
</script>

<template>
  <div id="kopra-editor" ref="container" />
</template>

Next.js

The SDK requires the DOM, so it must only run on the client. Use useEffect to avoid SSR issues.

'use client';

import { useEffect, useRef } from 'react';
import type { KopraSDK as KopraSDKType } from '@kopra-dev/sdk';

export default function KopraEditor() {
  const sdkRef = useRef<KopraSDKType | null>(null);

  useEffect(() => {
    let sdk: KopraSDKType;

    import('@kopra-dev/sdk').then(({ KopraSDK }) => {
      sdk = new KopraSDK({
        currentTenant: 'northstar-staffing',
        getToken: async (req) => {
          const res = await fetch('/api/kopra-token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(req),
          });
          return res.json();
        },
      });

      sdkRef.current = sdk;
      sdk.loadCustomFields('kopra-editor', {
        fieldGroupKey: 'customer',
        entityId: 'contact-123',
      });
    });

    return () => sdkRef.current?.destroy();
  }, []);

  return <div id="kopra-editor" />;
}

Svelte

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { KopraSDK } from '@kopra-dev/sdk';

  let container: HTMLElement;
  let sdk: KopraSDK;

  onMount(() => {
    sdk = new KopraSDK({
      currentTenant: 'northstar-staffing',
      getToken: async (req) => {
        const res = await fetch('/api/kopra-token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(req),
        });
        return res.json();
      },
    });

    sdk.loadCustomFields('kopra-editor', {
      fieldGroupKey: 'customer',
      entityId: 'contact-123',
    });
  });

  onDestroy(() => sdk?.destroy());
</script>

<div id="kopra-editor" bind:this={container} />

Troubleshooting

Iframe doesn't load / blank white box

  • Check the browser console for CORS errors. Your Kopra instance must allow the origin of your app.
  • Verify the token endpoint returns { token, fieldEditorUrl, tenantConfigUrl }. If your backend returns the raw Kopra response, unwrap data first: res.json(json.data).

"Authentication timeout" after 30 seconds

  • The SDK sends the token to the iframe via postMessage. If the iframe URL is wrong or unreachable, it can't receive the token.
  • Check that your KOPRA_URL env var points to a running Kopra instance.

Token request returns 401

  • Your API key is missing or invalid. Pass it via the X-API-Key header from your backend.
  • API keys are shown only once when created. Generate a new one from the dashboard if lost.

Token request returns 429

  • You've exceeded 50 token requests per 15 minutes. This limit applies per API key.
  • Cache tokens on your backend. Tokens are valid for 1 hour by default.

Field values don't save

  • Check that onFieldsError is set. Silent failures are logged there.
  • Verify the field group key matches a field group you created in the dashboard.
  • If using autosave, it debounces by 2 seconds. Changes save automatically after the user stops typing.

Fields appear but are empty

  • Field values are scoped to (tenantId, fieldGroupKey, entityId). If any of these change, you get a fresh empty form.
  • Use getFieldValues() to check what's stored for a given combination.

Resources