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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@learncard/partner-connect

v0.2.0

Published

LearnCard Partner Connect SDK - Promise-based cross-origin messaging for partner apps

Readme

@learncard/partner-connect

Promise-based JavaScript SDK for managing cross-origin messaging between partner apps and LearnCard

The LearnCard Partner Connect SDK transforms complex postMessage communication into clean, modern Promise-based functions. It handles the entire cross-origin message lifecycle, including request queuing, message validation, and timeout management.

Features

  • 🔒 Secure: Origin validation for all messages
  • 🎯 Type-safe: Full TypeScript support with comprehensive types
  • Promise-based: Modern async/await API
  • 🧹 Clean: Abstracts away all postMessage complexity
  • 📦 Lightweight: Zero dependencies
  • 🛡️ Robust: Built-in timeout handling and error management

Installation

npm install @learncard/partner-connect
pnpm add @learncard/partner-connect
yarn add @learncard/partner-connect

Quick Start

import { createPartnerConnect } from '@learncard/partner-connect';

// Initialize the SDK
const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app'
});

// Request user identity (SSO)
try {
  const identity = await learnCard.requestIdentity();
  console.log('User DID:', identity.user.did);
  console.log('JWT Token:', identity.token);
} catch (error) {
  if (error.code === 'LC_UNAUTHENTICATED') {
    console.log('User is not logged in');
  }
}

Configuration

Options

interface PartnerConnectOptions {
  /**
   * The origin of the LearnCard host (e.g., 'https://learncard.app')
   * All messages will be validated against this origin for security
   * (default: 'https://learncard.app')
   */
  hostOrigin?: string;

  /**
   * Protocol identifier (default: 'LEARNCARD_V1')
   */
  protocol?: string;

  /**
   * Request timeout in milliseconds (default: 30000)
   */
  requestTimeout?: number;
}

Dynamic Origin Configuration

The SDK uses a hierarchical approach to determine the active host origin:

1. Hardcoded Default (Security Anchor)

PartnerConnect.DEFAULT_HOST_ORIGIN // 'https://learncard.app'

2. Query Parameter Override (Staging/Testing)

// Your app URL: https://partner-app.com/?lc_host_override=https://staging.learncard.app

const learnCard = createPartnerConnect({
  hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
});
// Active origin: https://staging.learncard.app (from query param)
// ✅ Only accepts messages from: https://staging.learncard.app
// ✅ Sends messages to: https://staging.learncard.app

How the LearnCard Host Uses This:

  • Production: Iframe URL has no lc_host_override parameter
  • Staging: Iframe URL includes ?lc_host_override=https://staging.learncard.app
  • This allows testing against non-production environments without recompiling partner code

3. Configured Origin (Fallback)

const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app'
});
// Active origin: https://learncard.app (configured)

Origin Whitelist (Security Gate)

When providing multiple origins, they serve as a whitelist for the lc_host_override parameter:

const learnCard = createPartnerConnect({
  hostOrigin: [
    'https://learncard.app',
    'https://staging.learncard.app',
    'https://preview.learncard.app'
  ]
});

// Scenario 1: No query param
// → Uses: https://learncard.app (first in array)

// Scenario 2: Valid override
// URL: ?lc_host_override=https://staging.learncard.app
// → Uses: https://staging.learncard.app ✅

// Scenario 3: Invalid override (not in whitelist)
// URL: ?lc_host_override=https://evil.com
// → Uses: https://learncard.app (falls back to first) ⚠️
// → Logs warning about unauthorized override

Security Model

STRICT Origin Validation:

Incoming Message Origin ≡ Configured Host Origin

The SDK enforces an exact match between incoming message origins and the active host origin:

  • Secure: Even if a malicious actor adds ?lc_host_override=https://evil.com, messages from evil.com will be rejected
  • Cannot be spoofed: Browser security prevents malicious sites from faking their event.origin
  • No wildcards: Only exact matches are accepted
// Active origin: https://staging.learncard.app
// ✅ Accepts: messages from https://staging.learncard.app
// ❌ Rejects: messages from https://learncard.app
// ❌ Rejects: messages from https://evil.com
// ❌ Rejects: messages from any other origin

API Reference

requestIdentity()

Request user identity information (Single Sign-On).

const identity = await learnCard.requestIdentity();
// Returns: { token: string, user: { did: string, ... } }

Error Codes:

  • LC_UNAUTHENTICATED: User is not logged in to LearnCard
  • LC_TIMEOUT: Request timed out

sendCredential(credential)

Send a verifiable credential to the user's LearnCard wallet.

const response = await learnCard.sendCredential({
  '@context': ['https://www.w3.org/2018/credentials/v1'],
  type: ['VerifiableCredential', 'AchievementCredential'],
  credentialSubject: {
    id: identity.user.did,
    achievement: {
      name: 'JavaScript Expert',
      description: 'Mastered advanced JavaScript concepts'
    }
  }
});

console.log('Credential ID:', response.credentialId);

Returns: { credentialId: string }


launchFeature(featurePath, initialPrompt?)

Launch a feature in the LearnCard host application.

await learnCard.launchFeature(
  '/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
  'Explain the postMessage security model'
);

Parameters:

  • featurePath: Path to the feature
  • initialPrompt: Optional initial data or prompt

askCredentialSearch(verifiablePresentationRequest)

Request credentials from the user's wallet using a Verifiable Presentation Request.

const response = await learnCard.askCredentialSearch({
  query: [
    {
      type: 'QueryByTitle',
      credentialQuery: {
        reason: 'We need to verify your teamwork skills',
        title: 'Capstone'
      }
    }
  ],
  challenge: `challenge-${Date.now()}`,
  domain: window.location.hostname
});

if (response.verifiablePresentation) {
  const credentials = response.verifiablePresentation.verifiableCredential;
  console.log(`Received ${credentials.length} credential(s)`);
}

Returns: { verifiablePresentation?: { verifiableCredential: unknown[], ... } }


askCredentialSpecific(credentialId)

Request a specific credential by ID.

const response = await learnCard.askCredentialSpecific('credential-id-123');

if (response.credential) {
  console.log('Received credential:', response.credential);
}

Error Codes:

  • CREDENTIAL_NOT_FOUND: Credential doesn't exist
  • USER_REJECTED: User declined to share

requestConsent(contractUri)

Request user consent for permissions.

const response = await learnCard.requestConsent(
  'lc:network:network.learncard.com/trpc:contract:abc123'
);

if (response.granted) {
  console.log('User granted consent');
} else {
  console.log('User denied consent');
}

Returns: { granted: boolean }


initiateTemplateIssue(templateId, draftRecipients?)

Initiate a template-based credential issuance flow (e.g., Send Boost).

const response = await learnCard.initiateTemplateIssue(
  'lc:network:network.learncard.com/trpc:boost:xyz789',
  ['did:key:z6Mkr...', 'did:key:z6Mks...']
);

if (response.issued) {
  console.log('Template issued successfully');
}

Error Codes:

  • UNAUTHORIZED: Not an admin of this template
  • TEMPLATE_NOT_FOUND: Template doesn't exist

destroy()

Clean up the SDK and remove event listeners. Call this when unmounting your component or closing your app.

learnCard.destroy();

Complete Example

Here's a complete example showing how to refactor a manual postMessage implementation to use the SDK:

Before (Manual postMessage)

// Manual setup - verbose and error-prone
const LEARNCARD_HOST_ORIGIN = 'https://learncard.app';
const PROTOCOL = 'LEARNCARD_V1';
const pendingRequests = new Map();

function sendPostMessage(action, payload = {}) {
  return new Promise((resolve, reject) => {
    const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
    pendingRequests.set(requestId, { resolve, reject });

    window.parent.postMessage({
      protocol: PROTOCOL,
      action,
      requestId,
      payload,
    }, LEARNCARD_HOST_ORIGIN);

    setTimeout(() => {
      if (pendingRequests.has(requestId)) {
        pendingRequests.delete(requestId);
        reject({ code: 'LC_TIMEOUT', message: 'Request timed out' });
      }
    }, 30000);
  });
}

window.addEventListener('message', (event) => {
  if (event.origin !== LEARNCARD_HOST_ORIGIN) return;
  const { protocol, requestId, type, data, error } = event.data;
  if (protocol !== PROTOCOL || !requestId) return;

  const pending = pendingRequests.get(requestId);
  if (!pending) return;

  pendingRequests.delete(requestId);
  if (type === 'SUCCESS') {
    pending.resolve(data);
  } else if (type === 'ERROR') {
    pending.reject(error);
  }
});

// Usage
const identity = await sendPostMessage('REQUEST_IDENTITY');

After (With SDK)

import { createPartnerConnect } from '@learncard/partner-connect';

// Clean, one-line setup
const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app'
});

// Usage - same result, much cleaner
const identity = await learnCard.requestIdentity();

Error Handling

All methods return Promises that reject with a LearnCardError object:

interface LearnCardError {
  code: string;
  message: string;
}

Common Error Codes:

  • LC_TIMEOUT: Request timed out
  • LC_UNAUTHENTICATED: User not logged in
  • USER_REJECTED: User declined the request
  • CREDENTIAL_NOT_FOUND: Credential doesn't exist
  • UNAUTHORIZED: User lacks permission
  • TEMPLATE_NOT_FOUND: Template doesn't exist
  • SDK_NOT_INITIALIZED: SDK initialization failed
  • SDK_DESTROYED: SDK was destroyed before completion

Example:

try {
  const identity = await learnCard.requestIdentity();
  // Success
} catch (error) {
  switch (error.code) {
    case 'LC_UNAUTHENTICATED':
      console.log('Please log in to your LearnCard account');
      break;
    case 'LC_TIMEOUT':
      console.log('Request timed out. Please try again.');
      break;
    default:
      console.error('An error occurred:', error.message);
  }
}

Integration with Astro

---
// src/pages/index.astro
const config = {
  learnCardHostOrigin: import.meta.env.PUBLIC_LEARNCARD_HOST || 'https://learncard.app'
};
---

<script>
  import { createPartnerConnect } from '@learncard/partner-connect';

  const config = window.__LC_CONFIG;
  const learnCard = createPartnerConnect({
    hostOrigin: config.learnCardHostOrigin
  });

  async function init() {
    try {
      const identity = await learnCard.requestIdentity();
      console.log('Logged in as:', identity.user.did);
    } catch (error) {
      console.error('Not authenticated:', error);
    }
  }

  init();
</script>

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

Requires postMessage API and Promise support.

Security

The SDK implements multiple security layers:

1. Strict Origin Validation

  • Messages must come from the exact active host origin
  • No wildcards, no pattern matching, no exceptions
  • Mathematical equivalence: event.origin === activeHostOrigin

2. Query Parameter Whitelist

  • lc_host_override values are validated against configured hostOrigin array
  • Invalid overrides are rejected and logged
  • Falls back to first configured origin on validation failure

3. Anti-Spoofing Protection

Even if a malicious actor injects ?lc_host_override=https://evil.com:

  • The SDK may adopt evil.com as the active origin (if not whitelisted)
  • BUT messages from evil.com will only be accepted if event.origin === 'evil.com'
  • Browser security prevents evil.com from spoofing another domain's origin
  • Malicious messages are silently rejected

4. Additional Security Layers

  • Protocol Validation: Messages must match the expected protocol identifier
  • Request ID Tracking: Only tracked requests with valid IDs are processed
  • Timeout Protection: Requests automatically timeout to prevent hanging
  • Explicit targetOrigin: Never uses '*' in postMessage calls

Example Attack Scenario (Prevented)

// Attacker adds malicious query param
// URL: https://partner-app.com/?lc_host_override=https://evil.com

// SDK configuration
const learnCard = createPartnerConnect({
  hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
});

// What happens:
// 1. SDK detects lc_host_override=https://evil.com
// 2. Validates against whitelist: NOT FOUND
// 3. Falls back to: https://learncard.app
// 4. Sends messages to: https://learncard.app
// 5. Only accepts messages from: https://learncard.app
// 6. Attacker's messages from evil.com: REJECTED ❌

TypeScript

The SDK is written in TypeScript and includes comprehensive type definitions:

import type {
  PartnerConnectOptions,
  IdentityResponse,
  SendCredentialResponse,
  VerifiablePresentationRequest,
  CredentialSearchResponse,
  ConsentResponse,
  LearnCardError,
} from '@learncard/partner-connect';

License

MIT

Contributing

Contributions are welcome! Please see the main LearnCard repository for contribution guidelines.

Support

For issues and questions:

  • GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
  • Documentation: https://docs.learncard.com