← Back to Home

@pstn2/core - TypeScript/Node.js Library

@pstn2/core - TypeScript/Node.js Library

Official TypeScript implementation of the PSTN2 distributed telecommunications protocol.

Installation

Not yet published to npm. To use the library today, clone the repository and build it locally:

git clone https://github.com/pstn2/pstn2.git
cd pstn2/code/typescript
npm install && npm run build

Once published, installation will be npm install @pstn2/core.

Quick Start

import { PSTN2Client, AuthenticationMode } from '@pstn2/core';

// Initialize client
const client = new PSTN2Client({
  cpId: 'CP1-UK-0001',
  apiEndpoint: 'https://api.yourcp.com/pstn2/v1',
  privateKey: process.env.PSTN2_PRIVATE_KEY,
  authMode: AuthenticationMode.DirectQuery, // or TokenPool
});

// Verify an inbound call
const verification = await client.auth.verifyCall({
  callerID: '+441234567890',
  calledID: '+447700900123',
  callReference: 'unique-call-id',
});

if (verification.verified) {
  console.log(`Call from ${verification.callerName} verified`);
  console.log(`Purpose: ${verification.callPurpose}`);
}

// Request direct routing for outbound call
const routing = await client.routing.requestRouting({
  destinationNumber: '+447700900123',
  callerID: '+441234567890',
  mediaCapabilities: {
    codecs: ['opus', 'g722'],
    encryption: ['srtp-aes256'],
  },
});

if (routing.accepted) {
  // Establish your direct media connection (via your media stack /
  // DTLS-SRTP) using the returned connection details
  console.log('Connect to:', routing.connectionDetails.fqdn, routing.connectionDetails.port);
  console.log('Peer public key:', routing.connectionDetails.publicKey);
} else {
  // Fall back to traditional PSTN routing
  console.log('Routing rejected:', routing.rejectReason);
}

Features

Architecture

Modules

Authentication Modes

Option 1: Direct Query

const client = new PSTN2Client({
  authMode: AuthenticationMode.DirectQuery,
  // ...
});

// Queries originating CP directly to verify call

Option 2: Token Pool

const client = new PSTN2Client({
  authMode: AuthenticationMode.TokenPool,
  tokenPoolEndpoint: 'https://tokenpool.pstn2.org',
  tokenPoolAuth: process.env.TOKEN_POOL_JWT,
  // ...
});

// Creates/verifies tokens in shared pool

Configuration

interface PSTN2Config {
  // CP identification
  cpId: string;                    // Your RCPID
  apiEndpoint: string;             // Your API endpoint URL

  // Security
  privateKey: string;              // For signing messages
  publicKey?: string;              // Will be derived if not provided

  // Authentication
  authMode: AuthenticationMode;    // DirectQuery or TokenPool
  tokenPoolEndpoint?: string;      // If using TokenPool
  tokenPoolAuth?: string;          // JWT for token pool

  // Caching
  cacheDirectory?: boolean;        // Cache other CPs' directories
  cacheTTL?: number;               // Cache TTL in seconds
                                   // (default: 86400 directory, 3600 branding)

  // Network
  timeout?: number;                // Request timeout ms (default: 2000)
  retries?: number;                // Number of retries (default: 3)

  // Fallback
  fallbackToTraditional?: boolean; // Fallback to PSTN (default: true)

  // Logging
  logLevel?: 'error' | 'warn' | 'info' | 'debug';
}

Examples

Runnable examples live in examples/. The numbered examples can be run with:

npm run example:01   # Basic authentication
npm run example:02   # Direct routing
npm run example:03   # Token pool
npm run example:04   # Emergency services
npm run example:05   # Complete call flow

examples/basic-usage.ts and examples/token-pool.ts are minimal quick-start examples. Type-check everything with npm run typecheck:examples.

Verify Inbound Call with Porting Chain

// Recipient CP verifies caller
const verification = await client.auth.verifyCall({
  callerID: '+441234567890',
  calledID: '+447700900123',
  callReference: 'abc-123',
});

// If number is ported, library automatically follows chain
// verification.portingChain shows the path taken
console.log('Porting chain:', verification.portingChain);
// ['CP1-UK-0001', 'CP2-UK-0002', 'CP3-UK-0003']

Direct Routing with Encryption

// Request routing info
const routing = await client.routing.requestRouting({
  destinationNumber: '+447700900123',
  callerID: '+441234567890',
  callReference: 'xyz-789',
  mediaCapabilities: {
    codecs: ['opus'],
    encryption: ['srtp-aes256'],
    video: false,
  },
  // Client automatically includes your public key
});

if (routing.accepted) {
  // Keys are exchanged, ready for encrypted media
  console.log('Connect to:', routing.connectionDetails.fqdn);
  console.log('Encrypt with:', routing.connectionDetails.publicKey);
}

Call with Branding

// Originating CP includes branding
const routing = await client.routing.requestRouting({
  destinationNumber: '+447700900123',
  callerID: '+441234567890',
  branding: {
    displayName: 'ACME Support',
    logo: 'https://cdn.acme.com/logo.png',
    backgroundColor: '#0066cc',
    callPurpose: 'Account Security Alert',
  },
});

// Recipient can display branding before ringing phone

Emergency Call with Live Location

// PSAP queries for location
const location = await client.emergency.getLocation({
  callerID: '+441234567890',
  callReference: 'emergency-456',
  psapID: 'UK-999-LONDON-CENTRAL',
});

console.log('Location:', location.location.latitude, location.location.longitude);
console.log('Accuracy:', location.location.accuracy, 'meters');
console.log('Address:', location.address?.street, location.address?.postcode);

Directory Service (for eventual consistency)

// Pull other CPs' directories (GET {endpoint}/directory/all) into the cache
await client.syncDirectory([
  'https://api.cp2.example.com/pstn2/v1',
  'https://api.cp3.example.com/pstn2/v1',
]);
// or per-CP: await client.directory.pullFromCP('https://api.cp2.example.com/pstn2/v1');

// Query cached directory
const cp = await client.directory.lookup('+441234567890');
console.log('Number hosted by:', cp.cpId);
console.log('API endpoint:', cp.apiEndpoint);
console.log('Routing endpoint:', cp.endpoints.routing);

// Inspect the cache
console.log(client.directory.getCacheStats());

Token Pool Operations

// Create token before placing call
const token = await client.auth.createToken({
  callerID: '+441234567890',
  calledID: '+447700900123',
  callReference: 'token-call-123',
  ttl: 30, // 30 seconds
});

console.log('Include token in INVITE:', token.tokenId);

// Recipient verifies token
const tokenData = await client.auth.verifyToken(token.tokenId);
if (tokenData) {
  console.log('Call from:', tokenData.originatingCP);
  console.log('Caller ID:', tokenData.callerID);
}

Error Handling

import { PSTN2Error, ErrorCode } from '@pstn2/core';

try {
  const verification = await client.auth.verifyCall({...});
} catch (error) {
  if (error instanceof PSTN2Error) {
    switch (error.code) {
      case ErrorCode.Timeout:
        // Fall back to traditional PSTN
        console.log('Verification timeout, using traditional routing');
        break;

      case ErrorCode.CallNotFound:
        // Potential fraud - caller ID not verified
        console.log('WARNING: Unverified caller ID');
        break;

      case ErrorCode.NumberPorted:
        // Library automatically retries with new CP
        console.log('Number ported, retrying...');
        break;

      default:
        console.error('PSTN2 error:', error.message);
    }
  } else {
    throw error;
  }
}

Testing

# Run tests
npm test

# Run with coverage
npm test:coverage

# Watch mode
npm test:watch

Development

# Install dependencies
npm install

# Build
npm run build

# Watch mode
npm run watch

# Lint
npm run lint

# Format
npm run format

API Documentation

Full API documentation generated with TypeDoc:

npm run docs
# Open docs/index.html

License

MIT - See LICENSE file

Support