From 4120aa57eaf6c195a1ab65bac69c6ec0327e4e7a Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Tue, 10 Jun 2025 08:44:45 -0300 Subject: [PATCH 1/3] Release 1.5.1-rc.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/js/index.ts | 2 +- packages/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/index.ts b/packages/js/index.ts index cd228e21..0f9b876a 100644 --- a/packages/js/index.ts +++ b/packages/js/index.ts @@ -3,7 +3,7 @@ import Verto from './src/Verto' import { setAgentName } from '../common/src/messages/blade/Connect' import CantinaAuth from '../common/src/webrtc/CantinaAuth' -export const VERSION = '1.5.0' +export const VERSION = '1.5.1-rc.1' setAgentName(`JavaScript SDK/${VERSION}`) export { Relay, Verto, CantinaAuth } diff --git a/packages/js/package.json b/packages/js/package.json index 6ec8635e..1d81622f 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@signalwire/js", - "version": "1.5.0", + "version": "1.5.1-rc.1", "description": "Relay SDK for JavaScript to connect to SignalWire.", "author": "SignalWire Team ", "main": "dist/index.min.js", From 4b1c252b62083ab64d074e7bfe20dfb8dee9a13d Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Tue, 10 Jun 2025 08:49:16 -0300 Subject: [PATCH 2/3] package-lock --- packages/js/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/package-lock.json b/packages/js/package-lock.json index 4864e56a..930a53dd 100644 --- a/packages/js/package-lock.json +++ b/packages/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@signalwire/js", - "version": "1.5.0", + "version": "1.5.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@signalwire/js", - "version": "1.5.0", + "version": "1.5.1-rc.1", "license": "MIT", "dependencies": { "jest-environment-jsdom": "^29.7.0", From be012f8876a6339e11a35c0d1bbaa19f3f2bb44f Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Thu, 15 Jan 2026 16:17:05 -0300 Subject: [PATCH 3/3] allow host override --- .scripts/get_token.js | 272 +++++++++++++++++ .scripts/playground.js | 288 ++++++++++++++++++ package.json | 4 +- packages/common/src/webrtc/BaseCall.ts | 2 +- packages/common/src/webrtc/Peer.ts | 22 +- .../js/examples/vanilla-calling/index.html | 9 +- 6 files changed, 587 insertions(+), 10 deletions(-) create mode 100755 .scripts/get_token.js create mode 100755 .scripts/playground.js diff --git a/.scripts/get_token.js b/.scripts/get_token.js new file mode 100755 index 00000000..e542819c --- /dev/null +++ b/.scripts/get_token.js @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +/** + * SignalWire JWT Token Generator CLI + * + * Generates JWT tokens for SignalWire Browser SDK authentication. + * See: https://developer.signalwire.com/sdks/browser-sdk/v2/#authentication-using-jwt + * + * Usage: + * node get_token.js --space --project --token [options] + * + * Environment variables (alternative to CLI args): + * SIGNALWIRE_SPACE - Your SignalWire space (e.g., "example" for example.signalwire.com) + * SIGNALWIRE_PROJECT - Your Project ID + * SIGNALWIRE_TOKEN - Your Project Token + * + * Supports .env files - create a .env file in the project root or current directory. + */ + +const https = require('https') +const fs = require('fs') +const path = require('path') + +// Load environment variables from .env file +function loadEnvFile() { + // Check multiple locations for .env file + const locations = [ + path.join(process.cwd(), '.env'), + path.join(__dirname, '..', '.env'), + path.join(__dirname, '.env'), + ] + + for (const envPath of locations) { + if (fs.existsSync(envPath)) { + try { + const content = fs.readFileSync(envPath, 'utf8') + const lines = content.split('\n') + + for (const line of lines) { + // Skip empty lines and comments + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + + // Parse KEY=value format + const match = trimmed.match(/^([^=]+)=(.*)$/) + if (match) { + const key = match[1].trim() + let value = match[2].trim() + + // Remove surrounding quotes if present + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + + // Only set if not already defined (CLI/system env takes precedence) + if (!(key in process.env)) { + process.env[key] = value + } + } + } + + console.log(`Loaded environment from: ${envPath}`) + return true + } catch (err) { + console.warn(`Warning: Could not read ${envPath}: ${err.message}`) + } + } + } + + return false +} + +// Load .env file before parsing args +loadEnvFile() + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2) + const parsed = { + space: process.env.SIGNALWIRE_SPACE, + project: process.env.SIGNALWIRE_PROJECT, + token: process.env.SIGNALWIRE_TOKEN, + resource: undefined, + expiresIn: undefined, + } + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--space': + case '-s': + parsed.space = args[++i] + break + case '--project': + case '-p': + parsed.project = args[++i] + break + case '--token': + case '-t': + parsed.token = args[++i] + break + case '--resource': + case '-r': + parsed.resource = args[++i] || process.env.SIGNALWIRE_REFERENCE + break + case '--expires-in': + case '-e': + parsed.expiresIn = parseInt(args[++i], 10) + break + case '--help': + case '-h': + printHelp() + process.exit(0) + default: + if (args[i].startsWith('-')) { + console.error(`Unknown option: ${args[i]}`) + process.exit(1) + } + } + } + + return parsed +} + +function printHelp() { + console.log(` +SignalWire JWT Token Generator + +Usage: + node get_token.js [options] + +Options: + -s, --space SignalWire space name (e.g., "example" for example.signalwire.com) + -p, --project Project ID + -t, --token Project Token (API token) + -r, --resource Resource name for the endpoint (default: random UUID) + -e, --expires-in Token expiration in minutes (default: 15) + -h, --help Show this help message + +Environment Variables: + SIGNALWIRE_SPACE Alternative to --space + SIGNALWIRE_PROJECT Alternative to --project + SIGNALWIRE_TOKEN Alternative to --token + +.env File Support: + The script automatically loads .env files from: + 1. Current working directory (.env) + 2. Project root (../.env from script location) + 3. Script directory (.scripts/.env) + + Example .env file: + SIGNALWIRE_SPACE=your-space + SIGNALWIRE_PROJECT=your-project-id + SIGNALWIRE_TOKEN=your-api-token + +Examples: + # Using CLI arguments + node get_token.js -s example -p your-project-id -t your-token + + # Using environment variables + export SIGNALWIRE_SPACE=example + export SIGNALWIRE_PROJECT=your-project-id + export SIGNALWIRE_TOKEN=your-token + node get_token.js + + # With custom resource and expiration + node get_token.js -s example -p proj-id -t token -r alice -e 60 +`) +} + +function validateArgs(args) { + const missing = [] + if (!args.space) missing.push('space (--space or SIGNALWIRE_SPACE)') + if (!args.project) missing.push('project (--project or SIGNALWIRE_PROJECT)') + if (!args.token) missing.push('token (--token or SIGNALWIRE_TOKEN)') + + if (missing.length > 0) { + console.error('Error: Missing required parameters:') + missing.forEach((m) => console.error(` - ${m}`)) + console.error('\nUse --help for usage information.') + process.exit(1) + } +} + +function getJwtToken(args) { + return new Promise((resolve, reject) => { + // Build request body + const body = {} + if (args.resource) body.resource = args.resource + if (args.expiresIn) body.expires_in = args.expiresIn + + const bodyStr = JSON.stringify(body) + + // Normalize space (remove .signalwire.com if included) + const space = args.space.replace(/\.signalwire\.com$/i, '') + + const options = { + hostname: `${space}.signalwire.com`, + port: 443, + path: '/api/relay/rest/jwt', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(bodyStr), + Authorization: + 'Basic ' + + Buffer.from(`${args.project}:${args.token}`).toString('base64'), + }, + } + + const req = https.request(options, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const json = JSON.parse(data) + resolve(json) + } catch (e) { + reject(new Error(`Failed to parse response: ${data}`)) + } + } else { + reject( + new Error(`HTTP ${res.statusCode}: ${res.statusMessage}\n${data}`), + ) + } + }) + }) + + req.on('error', (e) => { + reject(new Error(`Request failed: ${e.message}`)) + }) + + req.write(bodyStr) + req.end() + }) +} + +async function main() { + const args = parseArgs() + validateArgs(args) + + console.log(`Requesting JWT token from ${args.space}.signalwire.com...`) + if (args.resource) console.log(` Resource: ${args.resource}`) + if (args.expiresIn) console.log(` Expires in: ${args.expiresIn} minutes`) + + try { + const result = await getJwtToken(args) + + console.log('\n--- JWT Token (use this in the client) ---') + console.log(result.jwt_token) + + if (result.refresh_token) { + console.log('\n--- Refresh Token (keep this secret, use server-side) ---') + console.log(result.refresh_token) + } + + console.log('\n--- Full Response ---') + console.log(JSON.stringify(result, null, 2)) + } catch (error) { + console.error(`\nError: ${error.message}`) + process.exit(1) + } +} + +main() diff --git a/.scripts/playground.js b/.scripts/playground.js new file mode 100755 index 00000000..617e3bba --- /dev/null +++ b/.scripts/playground.js @@ -0,0 +1,288 @@ +#!/usr/bin/env node + +/** + * SignalWire Playground Script + * + * Fetches a JWT token and serves the vanilla-calling example using Python's http.server. + * + * Usage: + * node playground.js [options] + * + * Environment variables (or use .env file): + * SIGNALWIRE_SPACE - Your SignalWire space + * SIGNALWIRE_PROJECT - Your Project ID + * SIGNALWIRE_TOKEN - Your Project Token + */ + +const { spawn, execSync } = require('child_process') +const path = require('path') +const fs = require('fs') + +const EXAMPLE_DIR = path.join( + __dirname, + '..', + 'packages', + 'js', + 'examples', + 'vanilla-calling' +) +const TOKEN_SCRIPT = path.join(__dirname, 'get_token.js') +const DEFAULT_PORT = 9898 + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2) + const parsed = { + port: DEFAULT_PORT, + resource: undefined, + expiresIn: undefined, + help: false, + } + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--port': + case '-P': + parsed.port = parseInt(args[++i], 10) + break + case '--resource': + case '-r': + parsed.resource = args[++i] + break + case '--expires-in': + case '-e': + parsed.expiresIn = args[++i] + break + case '--help': + case '-h': + parsed.help = true + break + } + } + + return parsed +} + +function printHelp() { + console.log(` +SignalWire Playground + +Fetches a JWT token and serves the vanilla-calling example. + +Usage: + node playground.js [options] + +Options: + -P, --port HTTP server port (default: ${DEFAULT_PORT}) + -r, --resource Resource name for the token + -e, --expires-in Token expiration in minutes + -h, --help Show this help message + +Environment Variables (or use .env file): + SIGNALWIRE_SPACE Your SignalWire space + SIGNALWIRE_PROJECT Your Project ID + SIGNALWIRE_TOKEN Your Project Token + +The script will: + 1. Fetch a JWT token using get_token.js + 2. Display the token and project ID for easy copy/paste + 3. Start a Python HTTP server serving the vanilla-calling example + 4. Open http://localhost:${DEFAULT_PORT} in your browser + +Example: + # Basic usage (uses .env file) + node playground.js + + # Custom port and resource + node playground.js -P 8080 -r alice +`) +} + +function getToken(args) { + return new Promise((resolve, reject) => { + const tokenArgs = [] + if (args.resource) tokenArgs.push('-r', args.resource) + if (args.expiresIn) tokenArgs.push('-e', args.expiresIn) + + const proc = spawn('node', [TOKEN_SCRIPT, ...tokenArgs], { + stdio: ['inherit', 'pipe', 'pipe'], + }) + + let stdout = '' + let stderr = '' + + proc.stdout.on('data', (data) => { + stdout += data.toString() + }) + + proc.stderr.on('data', (data) => { + stderr += data.toString() + }) + + proc.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Token script failed:\n${stderr || stdout}`)) + return + } + + // Parse the JSON response from the output + const jsonMatch = stdout.match(/--- Full Response ---\s*([\s\S]+)$/) + if (jsonMatch) { + try { + const json = JSON.parse(jsonMatch[1].trim()) + resolve(json) + } catch (e) { + reject(new Error(`Failed to parse token response: ${e.message}`)) + } + } else { + reject(new Error('Could not find token in output')) + } + }) + }) +} + +function checkPython() { + try { + execSync('python3 --version', { stdio: 'pipe' }) + return 'python3' + } catch { + try { + execSync('python --version', { stdio: 'pipe' }) + return 'python' + } catch { + return null + } + } +} + +function startServer(pythonCmd, port) { + console.log(`\nStarting HTTP server on port ${port}...`) + console.log(`Serving: ${EXAMPLE_DIR}`) + + const server = spawn(pythonCmd, ['-m', 'http.server', port.toString()], { + cwd: EXAMPLE_DIR, + stdio: 'inherit', + }) + + server.on('error', (err) => { + console.error(`Failed to start server: ${err.message}`) + process.exit(1) + }) + + return server +} + +function openBrowser(url) { + const platform = process.platform + let cmd + + switch (platform) { + case 'darwin': + cmd = 'open' + break + case 'win32': + cmd = 'start' + break + default: + cmd = 'xdg-open' + } + + try { + spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref() + } catch { + // Silently fail if we can't open the browser + } +} + +async function main() { + const args = parseArgs() + + if (args.help) { + printHelp() + process.exit(0) + } + + // Check if example directory exists + if (!fs.existsSync(EXAMPLE_DIR)) { + console.error(`Error: Example directory not found: ${EXAMPLE_DIR}`) + process.exit(1) + } + + // Check for Python + const pythonCmd = checkPython() + if (!pythonCmd) { + console.error('Error: Python is required but not found in PATH') + console.error('Please install Python 3 and try again') + process.exit(1) + } + + console.log('='.repeat(60)) + console.log('SignalWire Playground') + console.log('='.repeat(60)) + + // Fetch token + console.log('\nFetching JWT token...') + let tokenData + try { + tokenData = await getToken(args) + } catch (err) { + console.error(`\nError: ${err.message}`) + process.exit(1) + } + + // Load env to get project ID + // Re-run the env loading logic to get SIGNALWIRE_PROJECT + const envLocations = [ + path.join(process.cwd(), '.env'), + path.join(__dirname, '..', '.env'), + path.join(__dirname, '.env'), + ] + + let projectId = process.env.SIGNALWIRE_PROJECT + if (!projectId) { + for (const envPath of envLocations) { + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8') + const match = content.match(/SIGNALWIRE_PROJECT=(.+)/) + if (match) { + projectId = match[1].trim().replace(/^["']|["']$/g, '') + break + } + } + } + } + + // Display credentials + console.log('\n' + '='.repeat(60)) + console.log('CREDENTIALS (copy these to the web UI)') + console.log('='.repeat(60)) + console.log(`\nProject ID: ${projectId || 'N/A'}`) + console.log(`\nJWT Token:\n${tokenData.jwt_token}`) + console.log('\n' + '='.repeat(60)) + + // Start server + const url = `http://localhost:${args.port}` + const server = startServer(pythonCmd, args.port) + + console.log(`\nOpen your browser at: ${url}`) + console.log('Press Ctrl+C to stop the server\n') + + // Give server a moment to start, then open browser + setTimeout(() => { + openBrowser(url) + }, 1000) + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\n\nShutting down server...') + server.kill() + process.exit(0) + }) + + process.on('SIGTERM', () => { + server.kill() + process.exit(0) + }) +} + +main() diff --git a/package.json b/package.json index f8d04733..325b36b1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "description": "SignalWire monorepo root package.json. This package is not published to npm.", "author": "SignalWire Team ", "scripts": { - "setup": "node .scripts/setup.js" + "setup": "node .scripts/setup.js", + "token": "node .scripts/get_token.js", + "playground": "node .scripts/playground.js" }, "dependencies": { "uuid": "^7.0.3" diff --git a/packages/common/src/webrtc/BaseCall.ts b/packages/common/src/webrtc/BaseCall.ts index 090a6f78..ccb8d834 100644 --- a/packages/common/src/webrtc/BaseCall.ts +++ b/packages/common/src/webrtc/BaseCall.ts @@ -52,7 +52,7 @@ export default abstract class BaseCall implements IWebRTCCall { public peer: Peer public options: CallOptions public cause: string - public causeCode: number + public causeCode: numberRe public channels: string[] = [] public role: string = Role.Participant public extension: string = null diff --git a/packages/common/src/webrtc/Peer.ts b/packages/common/src/webrtc/Peer.ts index 2b1ce89f..75e4eda3 100644 --- a/packages/common/src/webrtc/Peer.ts +++ b/packages/common/src/webrtc/Peer.ts @@ -14,7 +14,7 @@ import { RTCPeerConnection, streamIsValid, } from './WebRTC' -import { isFunction } from '../util/helpers' +import { isFunction } from '../util/helpers' import { CallOptions } from './interfaces' import { trigger } from '../services/Handler' import { filterIceServers } from './helpers' @@ -87,7 +87,11 @@ export default class Peer { return null }, ) - const {localElement, localStream = null, screenShare = false} = this.options + const { + localElement, + localStream = null, + screenShare = false, + } = this.options if (streamIsValid(localStream)) { if (typeof this.instance.addTrack === 'function') { const tracks = localStream.getTracks() @@ -123,7 +127,7 @@ export default class Peer { if (!this._isAnswer()) { return } - const {remoteSdp, useStereo} = this.options + const { remoteSdp, useStereo } = this.options const sdp = useStereo ? sdpStereoHack(remoteSdp) : remoteSdp const sessionDescr: RTCSessionDescription = sdpToJsonHack({ sdp, @@ -138,8 +142,12 @@ export default class Peer { } private _setLocalDescription(sessionDescription: RTCSessionDescriptionInit) { - const {useStereo, googleMaxBitrate, googleMinBitrate, googleStartBitrate} = - this.options + const { + useStereo, + googleMaxBitrate, + googleMinBitrate, + googleStartBitrate, + } = this.options if (useStereo) { sessionDescription.sdp = sdpStereoHack(sessionDescription.sdp) } @@ -193,7 +201,7 @@ export default class Peer { // The most typical scenario is an offer with audio only: we // don't want to call getUserMedia with video in this case. if (this._isAnswer()) { - const {remoteSdp, useStereo} = this.options + const { remoteSdp, useStereo } = this.options const sdp = useStereo ? sdpStereoHack(remoteSdp) : remoteSdp const sessionDescr: RTCSessionDescription = sdpToJsonHack({ sdp, @@ -240,7 +248,7 @@ export default class Peer { private async _getSenderByKind(kind: string) { if (this.instance) { const senders = await this.instance.getSenders() - return senders.find(({track}) => track && track.kind === kind) + return senders.find(({ track }) => track && track.kind === kind) } } diff --git a/packages/js/examples/vanilla-calling/index.html b/packages/js/examples/vanilla-calling/index.html index bf537a61..19014f90 100644 --- a/packages/js/examples/vanilla-calling/index.html +++ b/packages/js/examples/vanilla-calling/index.html @@ -34,6 +34,11 @@

SignalWire Relay Call Test Harness

Connect
+
+ + + Enter your SignalWire Space URL. +
@@ -134,6 +139,7 @@

Troubleshooting

var client; var currentCall = null; + var host = localStorage.getItem('relay.example.host') || ''; var project = localStorage.getItem('relay.example.project') || ''; var token = localStorage.getItem('relay.example.token') || ''; var number = localStorage.getItem('relay.example.number') || ''; @@ -159,6 +165,7 @@

Troubleshooting

* On document ready auto-fill the input values from the localStorage. */ ready(function() { + document.getElementById('host').value = host; document.getElementById('project').value = project; document.getElementById('token').value = token; document.getElementById('number').value = number; @@ -172,7 +179,7 @@

Troubleshooting

*/ function connect() { client = new Relay({ - // host: 'relay.swire.io', + host: document.getElementById('host').value, project: document.getElementById('project').value, token: document.getElementById('token').value });