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

@edge-markets/connect-node

v1.11.2

Published

Server SDK for EDGE Connect token exchange and API calls

Readme

@edge-markets/connect-node

Server SDK for EDGE Connect token exchange and API calls.

Features

  • 🔐 Secure token exchange - Exchange codes for tokens with PKCE
  • 🔄 Token refresh - Automatic refresh token handling
  • 📡 Full API client - User, balance, transfers via forUser() pattern
  • 🛡️ Typed errors - Specific error classes for each scenario
  • 📝 TypeScript first - Complete type definitions
  • 🪝 Webhook signature verification - Constant-time HMAC-SHA256 with replay protection
  • Webhook parsing + validation - Verify raw bodies and fail closed on malformed event payloads
  • 🔁 Webhook reconciliation - Cursor-based sync for catching missed events
  • 📥 Webhook inbox orchestration - Durable accept/process/replay control flow with partner-owned storage
  • 🔐 Token vault - Versioned AES-256-GCM token encryption with key rotation
  • 🔄 User sessions - Token-store callbacks with refresh, encryption, and single-flight refresh protection
  • ⚙️ Env config helper - Build SDK config from standard EDGE env vars

These helpers are framework-neutral: the SDK owns repeatable Connect mechanics such as token refresh, webhook validation, replay control flow, and transfer intent checks, while partner applications keep ownership of persistence, wallet mutation, admin authorization, and business rules.

Installation

npm install @edge-markets/connect-node
# or
pnpm add @edge-markets/connect-node
# or
yarn add @edge-markets/connect-node

Quick Start

import { createEdgeConnectServerFromEnv } from '@edge-markets/connect-node'

// Create instance once (reuse for all requests). Reads EDGE_CLIENT_ID,
// EDGE_CLIENT_SECRET, EDGE_ENVIRONMENT, optional mTLS and partner sync env vars.
const { server: edge } = createEdgeConnectServerFromEnv()

// Exchange code from EdgeLink for tokens
const tokens = await edge.exchangeCode(code, codeVerifier)

// Create a user-scoped client and make API calls
const client = edge.forUser(tokens.accessToken)
const user = await client.getUser()
const balance = await client.getBalance()

⚠️ Security

This SDK requires your client secret. Use it ONLY on your backend server!

Never expose your client secret to browsers or client-side code.

Configuration

From environment variables

Most partner backends should use createEdgeConnectServerFromEnv() so mTLS PEM handling, partner sync credentials, URL overrides, and timeout parsing are consistent across integrations.

import { createEdgeConnectServerFromEnv } from '@edge-markets/connect-node'

const { server: edge, capabilities, warnings } = createEdgeConnectServerFromEnv({
  requireMtls: process.env.NODE_ENV === 'production',
})

warnings.forEach((warning) => logger.warn(warning))

if (!capabilities.webhookSyncConfigured) {
  logger.warn('EDGE webhook sync is not configured; set EDGE_PARTNER_CLIENT_ID and EDGE_PARTNER_CLIENT_SECRET')
}

Supported environment variables:

| Variable | Description | |----------|-------------| | EDGE_CLIENT_ID | User-facing OAuth client ID | | EDGE_CLIENT_SECRET | User-facing OAuth client secret | | EDGE_ENVIRONMENT | production or sandbox | | EDGE_API_BASE_URL | Optional Connect API override | | EDGE_OAUTH_BASE_URL | Optional OAuth/token URL override | | EDGE_PARTNER_API_BASE_URL | Optional partner dashboard API override | | EDGE_PARTNER_CLIENT_ID | Partner machine-to-machine client ID for webhook sync | | EDGE_PARTNER_CLIENT_SECRET | Partner machine-to-machine client secret for webhook sync | | EDGE_MTLS_CERT / EDGE_MTLS_CERT_PATH | Client certificate PEM or file path | | EDGE_MTLS_KEY / EDGE_MTLS_KEY_PATH | Client private key PEM or file path | | EDGE_SERVER_CA_PEM / EDGE_SERVER_CA_PEM_PATH | Optional extra server trust root PEM or file path | | EDGE_MTLS_CA / EDGE_MTLS_CA_PATH | Legacy alias for optional extra server trust root PEM or file path | | EDGE_REQUIRE_MTLS | Set to true to fail startup unless client cert and key are configured | | EDGE_TIMEOUT_MS | Optional request timeout in milliseconds |

Use inline PEMs with escaped newlines (\n) or file paths. If both are set, the inline value wins and the helper returns a warning.

Manual configuration

interface EdgeConnectServerConfig {
  clientId: string              // Your user-facing OAuth client ID
  clientSecret: string          // Your user-facing OAuth client secret (keep secret!)
  environment: EdgeEnvironment  // 'production' | 'sandbox'

  // Optional
  apiBaseUrl?: string           // Custom API URL (dev only)
  partnerApiBaseUrl?: string    // Custom partner dashboard API URL (dev only)
  oauthBaseUrl?: string         // Custom OAuth URL (dev only)
  timeout?: number              // Request timeout (default: 30000ms)
  retry?: RetryConfig           // Retry configuration
  onRequest?: (info) => void    // Hook called before each request
  onResponse?: (info) => void   // Hook called after each response

  // Optional: Partner-level credentials for non-user-scoped endpoints.
  // Required ONLY when calling syncWebhookEvents — the rest of the SDK
  // works without them. These are a DIFFERENT OAuth client than clientId
  // (machine-to-machine, not user-facing).
  partnerClientId?: string
  partnerClientSecret?: string

  // By default the SDK derives partnerApiBaseUrl from apiBaseUrl by replacing
  // /connect/v1 with /v1. For example:
  // https://sandbox.connect.staging.edgeboost.io/connect/v1
  // -> https://sandbox.connect.staging.edgeboost.io/v1

  // Optional: Message Level Encryption (Connect endpoints only)
  mle?: {
    enabled: boolean
    edgePublicKey: string       // EDGE public encryption key (PEM)
    edgeKeyId: string           // EDGE key ID (kid) used for requests
    partnerPrivateKey: string   // Your private key (PEM) to decrypt responses
    partnerKeyId: string        // Your key ID (kid) expected in response headers
    strictResponseEncryption?: boolean // default true
  }

  // Optional: Mutual TLS client authentication
  mtls?: {
    enabled: boolean
    cert: string                // Your client certificate PEM
    key: string                 // Your client private key PEM
    ca?: string | string[]      // Additional server trust roots, if EDGE provides them
  }
}

Message Level Encryption (MLE)

const edge = new EdgeConnectServer({
  clientId: process.env.EDGE_CLIENT_ID!,
  clientSecret: process.env.EDGE_CLIENT_SECRET!,
  environment: 'sandbox',
  mle: {
    enabled: true,
    edgePublicKey: process.env.EDGE_MLE_EDGE_PUBLIC_KEY!,
    edgeKeyId: process.env.EDGE_MLE_EDGE_KEY_ID!,
    partnerPrivateKey: process.env.EDGE_MLE_PARTNER_PRIVATE_KEY!,
    partnerKeyId: process.env.EDGE_MLE_PARTNER_KEY_ID!,
  },
})

When enabled, the SDK sends X-Edge-MLE: v1, encrypts request bodies, and decrypts encrypted Connect responses.

Mutual TLS (mTLS)

Use mtls when EDGE has issued your partner backend a client certificate and key:

const edge = new EdgeConnectServer({
  clientId: process.env.EDGE_CLIENT_ID!,
  clientSecret: process.env.EDGE_CLIENT_SECRET!,
  environment: 'sandbox',
  mtls: {
    enabled: true,
    cert: process.env.EDGE_MTLS_CERT!,
    key: process.env.EDGE_MTLS_KEY!,
  },
})

cert and key authenticate your backend to EDGE Connect. They are client authentication material and must stay on your server.

ca is optional server trust material. Public EDGE Connect gateways such as https://connect.edgeboost.io and https://sandbox.connect.staging.edgeboost.io use public certificates, so most partners should omit ca. If EDGE provides a private server CA for a non-public endpoint, pass it as ca; the SDK appends it to Node's default public trust roots instead of replacing them.

Do not disable TLS verification and do not set NODE_TLS_REJECT_UNAUTHORIZED=0.

Next.js and Vercel Route Handlers

@edge-markets/connect-node is a Node.js server SDK. In Next.js Route Handlers, force the Node runtime before importing or constructing the SDK:

export const runtime = 'nodejs'

import { createEdgeConnectServerFromEnv } from '@edge-markets/connect-node'

const { server: edge } = createEdgeConnectServerFromEnv({
  requireMtls: true,
})

Do not import this package from Client Components, browser code, middleware, or Edge Runtime handlers. It reads server-only secrets and uses Node TLS APIs for mTLS.

If you are pinned to an older SDK version and your Next.js/Turbopack build logs show dynamic usage of require is not supported, externalize the package as an app-level mitigation until you can upgrade:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  serverExternalPackages: ['@edge-markets/connect-node', 'undici'],
}

export default nextConfig

Token Exchange

After EdgeLink completes, exchange the code for tokens:

// In your /api/edge/exchange endpoint
export async function POST(req: Request) {
  const { code, codeVerifier } = await req.json()
  
  try {
    const tokens = await edge.exchangeCode(code, codeVerifier)
    
    // Store tokens securely (encrypted in database)
    await db.edgeConnections.upsert({
      userId: req.user.id,
      accessToken: encrypt(tokens.accessToken),
      refreshToken: encrypt(tokens.refreshToken),
      expiresAt: new Date(tokens.expiresAt),
    })
    
    return Response.json({ success: true })
  } catch (error) {
    if (error instanceof EdgeTokenExchangeError) {
      // Code expired or already used
      return Response.json({ error: 'Please try again' }, { status: 400 })
    }
    throw error
  }
}

Encrypting stored tokens

Store access and refresh tokens encrypted at rest. EdgeTokenVault gives partners a stable AES-256-GCM envelope with key IDs and key rotation support; you still choose the database table and user mapping.

import { createEdgeTokenVault } from '@edge-markets/connect-node'

const tokenVault = createEdgeTokenVault({
  currentKey: {
    id: process.env.EDGE_TOKEN_ENCRYPTION_KEY_ID ?? '2026-06',
    key: process.env.EDGE_TOKEN_ENCRYPTION_KEY!, // 32 bytes as base64, base64url, or hex
  },
  previousKeys: [
    // { id: '2026-05', key: process.env.EDGE_TOKEN_ENCRYPTION_KEY_2026_05! },
  ],
})

const tokens = await edge.exchangeCode(code, codeVerifier)
await db.edgeConnections.upsert({
  userId,
  encryptedTokens: tokenVault.encryptTokens(tokens),
})

const connection = await db.edgeConnections.get(userId)
const decrypted = tokenVault.decryptTokens(connection.encryptedTokens)
const client = edge.forUser(decrypted.accessToken)

Token Refresh And User Sessions

Use EdgeUserSession when you store encrypted tokens and want the SDK to handle decrypt-refresh-save-client lifecycle. You still own the database table and user mapping.

const session = edge.createUserSession({
  subjectId: userId,
  tokenVault,
  tokenStore: {
    load: async (subjectId) => {
      const row = await db.edgeConnections.get(subjectId)
      return row?.encryptedTokens
    },
    save: async (subjectId, encryptedTokens, { tokens }) => {
      await db.edgeConnections.update(subjectId, {
        encryptedTokens,
        expiresAt: new Date(tokens.expiresAt),
      })
    },
  },
  refreshSkewMs: 60_000,
})

const balance = await session.withClient((client) => client.getBalance())

EdgeUserSession refreshes near-expiry tokens, preserves refresh tokens when the token endpoint does not rotate them, saves the refreshed token envelope before returning a client, and single-flights concurrent refreshes on the same process. In horizontally scaled services, keep your storage callbacks idempotent or add DB-level optimistic locking.

For migrations from split token columns, use createSessionTokenRecord, parseSessionTokenRecord, normalizeSessionExpiresAt, and encryptLegacySessionTokens. Plaintext migration requires an explicit allowPlaintext: true option so production request paths do not silently accept raw tokens.

API Methods

All user-scoped API methods live on EdgeUserClient, created via edge.forUser(accessToken):

const client = edge.forUser(accessToken)

User & Balance

const user = await client.getUser()
// Returns: { id, email, firstName, lastName, createdAt }

const balance = await client.getBalance()
// Returns: { userId, availableBalance, currency, asOf }

Transfers

// 1. Initiate the transfer — returns a pending transfer that requires verification.
const transfer = await client.initiateTransfer({
  type: 'debit',           // 'debit' = pull from user, 'credit' = push to user
  amount: '100.00',
  idempotencyKey: `txn_${userId}_${Date.now()}`,
})
// Returns: { transferId, status: 'pending_verification', otpMethod }

// 2. Create an EDGE-hosted verification session. The returned `verificationUrl`
//    is a single-use, short-lived URL you embed in an iframe or popup on your
//    frontend. The user enters their OTP inside the EDGE-hosted UI — OTP secrets
//    never touch your infrastructure.
const session = await client.createVerificationSession(transfer.transferId, {
  origin: 'https://your-site.example.com',
})
// Returns: { sessionId, verificationUrl, expiresAt }

// 3. Listen for the postMessage event from the iframe (or poll
//    getVerificationSessionStatus) to learn when verification completes.
//    A `transfer.completed` webhook will be delivered to your configured
//    webhook URL once the transfer settles. For browser-crash recovery,
//    your backend should also reconcile against
//    GET /v1/partner/webhooks/events/sync using a dashboard
//    client_credentials token from POST /connect/oauth/token.

// Get transfer status
const status = await client.getTransfer(transfer.transferId)

// List transfers
const { transfers, total } = await client.listTransfers({
  status: 'completed',
  limit: 10,
  offset: 0,
})

Consent

// Revoke consent (disconnect user)
await client.revokeConsent()

// Clean up stored tokens
await db.edgeConnections.delete(userId)

Webhooks

EDGE Connect delivers webhook events to your server when transfers complete, fail, expire, or when a user revokes consent. The SDK provides two helpers: signature verification for the live HTTP delivery channel, and a reconciliation sync method for the cron-driven safety net.

Parse and verify webhook deliveries

Every webhook arrives with an X-Edge-Signature header in t=...,v1=... format — HMAC-SHA256 over ${timestamp}.${rawBody} keyed by your webhook secret. Use parseAndVerifyWebhook to verify the signature, parse JSON, and runtime-validate the typed event envelope in one step. The helper rejects events older than 5 minutes by default to prevent replay attacks.

⚠️ The body argument MUST be the raw request bytes, not a parsed object that you re-stringified. JSON.stringify(req.body) reorders keys and silently breaks verification with no diagnostic. Always capture the raw body via your framework's raw-body middleware.

import { parseAndVerifyWebhook } from '@edge-markets/connect-node'
import express from 'express'

const app = express()

app.post(
  '/webhooks/edge',
  // 👇 Critical: this gives you req.body as a Buffer, not a parsed object
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const { event, eventId, signatureTimestamp } = parseAndVerifyWebhook({
      rawBody: req.body,
      signatureHeader: req.headers['x-edge-signature'],
      secret: process.env.EDGE_WEBHOOK_SECRET!,
    })

    // Store eventId + signatureTimestamp in your durable inbox if you use one.
    // Process asynchronously and return 200 immediately — see "Best Practices"
    void processEdgeEvent(event)
    res.status(200).send('ok')
  },
)

NestJS

// main.ts
const app = await NestFactory.create(AppModule, { rawBody: true })

// edge-webhook.controller.ts
import { Controller, Post, Req, HttpCode, UnauthorizedException, type RawBodyRequest } from '@nestjs/common'
import type { Request } from 'express'
import { EdgeAuthenticationError, parseAndVerifyWebhook } from '@edge-markets/connect-node'

@Controller('webhooks')
export class EdgeWebhookController {
  @Post('edge')
  @HttpCode(200)
  handle(@Req() req: RawBodyRequest<Request>) {
    try {
      const { event } = parseAndVerifyWebhook({
        rawBody: req.rawBody ?? Buffer.from(''),
        signatureHeader: req.headers['x-edge-signature'],
        secret: process.env.EDGE_WEBHOOK_SECRET!,
      })
      void processEdgeEvent(event)
    } catch (error) {
      if (error instanceof EdgeAuthenticationError) {
        throw new UnauthorizedException('invalid signature')
      }
      throw error
    }
  }
}

verifyWebhookSignature remains exported for advanced integrations, but parseAndVerifyWebhook is the recommended default because it fails closed on unknown event types, malformed payloads, and parsed-body mistakes.

Type-safe event handling with EdgeWebhookEvent

EdgeWebhookEvent is a discriminated union over event.type. Switching narrows event.data automatically — no casts required.

import type { EdgeWebhookEvent } from '@edge-markets/connect-node'

function processEdgeEvent(event: EdgeWebhookEvent) {
  switch (event.type) {
    case 'transfer.completed':
      // event.data is { transferId, status: 'completed', type, amount }
      creditWallet(event.data.transferId, event.data.amount)
      break
    case 'transfer.failed':
      // event.data.reason is `string | undefined`
      logFailure(event.data.transferId, event.data.reason ?? 'unknown')
      break
    case 'transfer.expired':
      cancelPending(event.data.transferId)
      break
    case 'transfer.processing':
      // @experimental — reserved for ledger dual-write hand-off
      break
    case 'consent.revoked':
      deactivateLink(event.data.userId, event.data.clientId)
      break
    default: {
      const _exhaustive: never = event
      return _exhaustive
    }
  }
}

event.data.amount is intentionally a string (e.g. '100.00'), not a number, to preserve decimal precision — parse it with a decimal-aware library on your side.

Reconcile missed events with createWebhookReconciler

Even with retries and a dead-letter queue, network or partner outages can drop webhook events. Run syncWebhookEvents from a cron (every ~5 minutes is typical) to catch anything your primary receiver missed.

This method requires the partner-level credentials (partnerClientId / partnerClientSecret) on EdgeConnectServer. It uses the OAuth client_credentials grant, caches the partner token internally, and handles a 401 by refreshing the token once and retrying.

import { createEdgeConnectServerFromEnv } from '@edge-markets/connect-node'

const { server: edge } = createEdgeConnectServerFromEnv()

const reconciler = edge.createWebhookReconciler({
  cursorStore: {
    load: () => db.cursors.get('edge_webhook_sync'),
    save: (cursor) => db.cursors.set('edge_webhook_sync', cursor),
  },
  processEvent: processEdgeEvent, // same idempotent handler as live webhooks
  limit: 100,
  maxPages: 10,
})

async function reconcile() {
  const result = await reconciler.run()
  logger.info('EDGE webhook reconciliation complete', result)
}

setInterval(reconcile, 5 * 60 * 1000)

The reconciler saves the cursor only after processEvent(event) succeeds and skips overlapping runs on the same instance. You provide cursor storage and the idempotent event handler; the SDK owns pagination and cursor advancement.

If you need full control, edge.syncWebhookEvents() remains available.

The server enforces a 30-day lookback. If your cursor is older than 30 days, you will receive events from 30 days ago — re-bootstrap from a known good state via getTransfer if you have been offline for longer.

Best practices

  1. Verify signatures first — reject mis-signed events with 401 before doing any work.
  2. Return 200 immediately — process events asynchronously. EDGE retries on non-2xx and on responses slower than 10s.
  3. Be idempotent — the same event may be delivered more than once (at-least-once delivery). Key your wallet credit / status update on event.id or event.data.transferId.
  4. Run reconciliation as a backstop — webhooks are the fast path, syncWebhookEvents is the safety net. They share the same handler.

syncWebhookEvents uses the same SDK mTLS transport as user-scoped API calls, fetches and caches the partner client_credentials token internally, retries transient sync failures with the SDK retry policy, and redacts token material from observability hooks.

Durable webhook inbox with createWebhookInbox

For production integrations, accept a verified webhook into durable storage before returning 200, then process it asynchronously. The SDK can own the state machine while your app owns the database callbacks and wallet mutation.

import {
  createWebhookInbox,
  parseWebhookHttpRequest,
} from '@edge-markets/connect-node'

const inbox = createWebhookInbox({
  store: {
    insert: db.webhookInbox.insertIdempotently,
    find: db.webhookInbox.find,
    markProcessing: db.webhookInbox.markProcessing,
    markProcessed: db.webhookInbox.markProcessed,
    markFailed: db.webhookInbox.markFailed,
    markPending: db.webhookInbox.markPending,
    list: db.webhookInbox.list,
  },
  processEvent: async (event) => {
    // Partner-owned business logic. Keep wallet mutation idempotent.
    await processEdgeEvent(event)
  },
})

app.post('/webhooks/edge', express.raw({ type: 'application/json' }), async (req, res) => {
  const parsed = parseWebhookHttpRequest({
    request: { headers: req.headers, rawBody: req.body },
    secret: process.env.EDGE_WEBHOOK_SECRET!,
  })

  const accepted = await inbox.acceptRaw(
    parsed,
    Buffer.from(req.body).toString('utf8'),
    String(req.headers['x-edge-signature'] ?? ''),
  )

  res.status(200).json({
    received: true,
    duplicate: accepted.duplicate,
    status: accepted.status,
  })

  void inbox.process(accepted.eventId).catch((error) => {
    logger.error('EDGE webhook processing failed', { eventId: accepted.eventId, error })
  })
})

The inbox statuses are pending, processing, processed, and failed. Duplicate event.id deliveries are idempotent if the payload fingerprint matches. A duplicate event ID with different payload fails closed.

Sync reconciliation can use the same inbox path:

const reconciler = edge.createWebhookReconciler({
  cursorStore,
  inbox,
  limit: 100,
})

The reconciler accepts each synced event into the inbox, processes it, and saves the cursor only after processing succeeds or the event is already processed. Use inbox.replay(eventId) for explicit admin replays and inbox.recoverStaleProcessing() to make crashed processing rows replayable.

Transfer safety helpers

Use transfer helpers to avoid decimal and direction mistakes:

import {
  assertWebhookMatchesTransfer,
  createTransferIdempotencyKey,
  getConnectTransferDirection,
  mapTransferStatusToPartnerState,
  normalizeMoneyAmount,
  validateTransferAmount,
} from '@edge-markets/connect-node'

const amount = validateTransferAmount(input.amount, { min: '1.00', max: '10000.00' }, { allowNumber: false })
const idempotencyKey = createTransferIdempotencyKey({
  partnerUserId: user.id,
  type: 'debit',
  amount,
  category: 'sportsbook',
  externalId: bet.id,
})

// debit = EDGE user to partner; credit = partner to EDGE user
const direction = getConnectTransferDirection('debit')

assertWebhookMatchesTransfer(event, {
  transferId: localTransfer.edgeTransferId,
  type: 'debit',
  amount: localTransfer.amount,
})

Error Handling

import {
  EdgeError,
  EdgeAuthenticationError,
  EdgeTokenExchangeError,
  EdgeConsentRequiredError,
  isEdgeError,
} from '@edge-markets/connect-node'

try {
  const client = edge.forUser(accessToken)
  const balance = await client.getBalance()
} catch (error) {
  if (error instanceof EdgeAuthenticationError) {
    // Token expired - try refresh or reconnect
    return { error: 'session_expired' }
  }
  
  if (error instanceof EdgeConsentRequiredError) {
    // User revoked consent - need to reconnect
    return { error: 'reconnect_required' }
  }
  
  if (isEdgeError(error)) {
    // Some other SDK error
    console.error(`Edge Error [${error.code}]: ${error.message}`)
    return { error: error.code }
  }
  
  // Unknown error
  throw error
}

Error Types

| Error | When | What to do | |-------|------|------------| | EdgeAuthenticationError | Token invalid/expired | Refresh token or reconnect | | EdgeTokenExchangeError | Code exchange failed | Ask user to try again | | EdgeConsentRequiredError | User hasn't granted consent | Open EdgeLink | | EdgeInsufficientScopeError | Missing required scopes | Request more scopes | | EdgeNotFoundError | Resource not found | Check ID | | EdgeApiError | Other API error | Check error.code | | EdgeNetworkError | Network failure | Retry request |

NestJS Example

import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { EdgeConnectServer, EdgeConsentRequiredError } from '@edge-markets/connect-node'

@Injectable()
export class EdgeService {
  private readonly edge: EdgeConnectServer
  private readonly logger = new Logger(EdgeService.name)

  constructor(private config: ConfigService) {
    this.edge = new EdgeConnectServer({
      clientId: this.config.getOrThrow('EDGE_CLIENT_ID'),
      clientSecret: this.config.getOrThrow('EDGE_CLIENT_SECRET'),
      environment: this.config.get('EDGE_ENVIRONMENT', 'sandbox'),
    })
  }

  async exchangeCode(code: string, codeVerifier: string) {
    return this.edge.exchangeCode(code, codeVerifier)
  }

  async getBalance(accessToken: string) {
    try {
      const client = this.edge.forUser(accessToken)
      return await client.getBalance()
    } catch (error) {
      if (error instanceof EdgeConsentRequiredError) {
        this.logger.warn('User consent required')
        throw error
      }
      this.logger.error('Failed to get balance', error)
      throw error
    }
  }
}

Express Example

import express from 'express'
import { EdgeConnectServer, isEdgeError } from '@edge-markets/connect-node'

const edge = new EdgeConnectServer({
  clientId: process.env.EDGE_CLIENT_ID!,
  clientSecret: process.env.EDGE_CLIENT_SECRET!,
  environment: 'sandbox',
})

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

// Exchange code for tokens
app.post('/api/edge/exchange', async (req, res) => {
  try {
    const { code, codeVerifier } = req.body
    const tokens = await edge.exchangeCode(code, codeVerifier)
    
    // Store tokens for user...
    
    res.json({ success: true })
  } catch (error) {
    if (isEdgeError(error)) {
      res.status(400).json({ error: error.code, message: error.message })
    } else {
      res.status(500).json({ error: 'internal_error' })
    }
  }
})

// Get balance
app.get('/api/edge/balance', async (req, res) => {
  try {
    const accessToken = await getAccessTokenForUser(req.user.id)
    const client = edge.forUser(accessToken)
    const balance = await client.getBalance()
    res.json(balance)
  } catch (error) {
    // Handle errors...
  }
})

Related Packages

  • @edge-markets/connect - Core types and utilities
  • @edge-markets/connect-link - Browser SDK for popup authentication

License

MIT