@edge-markets/connect-node

中文文档
4.0.0发布于 2026/10/27MITTypeScriptESM + CJS近 30 天下载 1.7万

Server SDK for EDGE Connect token exchange and API calls

原始使用文档
文档目录31 节
  1. Features
  2. Installation
  3. Quick Start
  4. ⚠️ Security
  5. Configuration
  6. From environment variables
  7. Manual configuration
  8. Message Level Encryption (MLE)
  9. Mutual TLS (mTLS)
  10. Next.js and Vercel Route Handlers
  11. Token Exchange
  12. Encrypting stored tokens
  13. Token Refresh And User Sessions
  14. API Methods
  15. User & Balance
  16. Transfers
  17. Consent
  18. Webhooks
  19. Parse and verify webhook deliveries
  20. Type-safe event handling with EdgeWebhookEvent
  21. Reconcile missed events with createWebhookReconciler
  22. Best practices
  23. Durable webhook inbox with createWebhookInbox
  24. Transfer safety helpers
  25. Error Handling
  26. Error Types
  27. NestJS Example
  28. Express Example
  29. Related Packages
  30. License
  31. Read-only webhooks v2 (2026-09-06)

@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:

VariableDescription
EDGE_CLIENT_IDUser-facing OAuth client ID
EDGE_CLIENT_SECRETUser-facing OAuth client secret
EDGE_ENVIRONMENTproduction or sandbox
EDGE_API_BASE_URLOptional Connect API override
EDGE_OAUTH_BASE_URLOptional OAuth/token URL override
EDGE_PARTNER_API_BASE_URLOptional partner dashboard API override
EDGE_PARTNER_CLIENT_IDPartner machine-to-machine client ID for webhook sync
EDGE_PARTNER_CLIENT_SECRETPartner machine-to-machine client secret for webhook sync
EDGE_MTLS_CERT / EDGE_MTLS_CERT_PATHClient certificate PEM or file path
EDGE_MTLS_KEY / EDGE_MTLS_KEY_PATHClient private key PEM or file path
EDGE_SERVER_CA_PEM / EDGE_SERVER_CA_PEM_PATHOptional extra server trust root PEM or file path
EDGE_MTLS_CA / EDGE_MTLS_CA_PATHLegacy alias for optional extra server trust root PEM or file path
EDGE_REQUIRE_MTLSSet to true to fail startup unless client cert and key are configured
EDGE_TIMEOUT_MSOptional 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: 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)

MLE is not available yet. The Edge Connect API doesn't accept or return encrypted payloads, so passing mle: { enabled: true } throws EdgeFeatureUnavailableError. Connect API traffic is protected by mTLS.

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, add a per-link database lease and an optimistic token version around refresh. Persist the complete returned token set atomically because durable opaque refresh credentials rotate on success.

Refresh failures throw EdgeRefreshError. Use actionRequired (retry, partner_admin, or relink) and retryable for connection state decisions; do not translate every token-endpoint failure into a user reconnect prompt.

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, address?, city?, state?, zip?, phone?, dateOfBirth?, verified, createdAt }

const balance = await client.getBalance()
// Returns: { userId, currentBalance, 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,
})
// 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

ErrorWhenWhat to do
EdgeAuthenticationErrorToken invalid/expiredRefresh token or reconnect
EdgeTokenExchangeErrorCode exchange failedAsk user to try again
EdgeConsentRequiredErrorUser hasn't granted consentOpen EdgeLink
EdgeInsufficientScopeErrorMissing required scopesRequest more scopes
EdgeNotFoundErrorResource not foundCheck ID
EdgeApiErrorOther API errorCheck error.code
EdgeNetworkErrorNetwork failureRetry 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...
  }
})
  • @edge-markets/connect - Core types and utilities
  • @edge-markets/connect-link - Browser SDK for popup authentication

License

MIT

Read-only webhooks v2 (2026-09-06)

Release gated: these exports ship with the next release containing this change; existing 3.0.0 installations do not yet provide them. Release-please owns versions. parseAndVerifyWebhookV2 verifies exact raw bytes with a five-minute timestamp window, an explicit cell and an active signing key (or bounded key ring). It distinguishes authenticated future types from invalid known types. The only business event is consent.revoked; preserve its actual grantId from OAuth and match cell/client/user/grant before changing a link. Ignore test: true.

WebhookInboxV2 requires an application-owned durable adapter with unique acceptance, expiring leases and fencing. Commit acceptance before 2xx; run a separate restart-safe worker. reconcileWebhooksV2 requires a distributed lease and atomic insertion of every event in a page together with its cursor. Never advance after partial failure. WebhookCursorExpiredError requires grant snapshot recovery and replay from the snapshot's retained watermark.

EdgeConnectServer.syncWebhookEventsV2, getWebhookGrantSnapshot and getWebhookCapabilities use separate partner machine credentials with mTLS. partnerApiBaseUrl must be the canonical cell gateway plus /v1 (the SDK appends /partner/webhooks); cells are staging, sandbox-staging, sandbox-production and production. No user-token or plaintext transport fallback.

Normal rotation verifies any active v1 signature during 24-hour overlap. Expire previous receiver keys explicitly; never log raw signatures, credentials or keys. The offline connect-webhook-inspect --cell sandbox-staging --signature-file /protected/signature.txt < /protected/body.json command reads the secret from EDGE_WEBHOOK_SECRET and prints only verified safe metadata. It makes no HTTP request. Use the dashboard for real sandbox tests, attempts and confirmed resends.

See the docs site's Webhooks, Signature Verification and Recovery guides and Alexandria's PostgreSQL receiver for a complete application adapter. Legacy exports remain available; legacy reconciliation now stops at unprocessed work.

版本与发布记录

@edge-markets/connect-node 共发布 23 个版本,首次发布于 2026/01/21;过去 12 个月发布了 23 个版本。

版本发布日期距今
4.0.0latest2026/10/2717 天前
3.1.02026/09/281 个月前
3.0.02026/08/033 个月前
2.0.02026/07/293 个月前
1.12.12026/07/273 个月前
1.12.02026/07/273 个月前

关于 @edge-markets/connect-node 的常见问题

@edge-markets/connect-node 是什么?

Server SDK for EDGE Connect token exchange and API calls

如何安装 @edge-markets/connect-node?

在项目目录运行 npm install @edge-markets/connect-node,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 connect-webhook-inspect,可以用 npx connect-webhook-inspect 直接运行。

@edge-markets/connect-node 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@edge-markets/connect-node 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@edge-markets/connect-node 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.23.4。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@edge-markets/connect-node 可以商用吗?

@edge-markets/connect-node 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@edge-markets/connect-node 的下载量有多少?

根据 npm 官方下载统计,@edge-markets/connect-node 在 2026/10/13 – 2026/11/11 这 30 天内被下载 16,633 次,日均约 554 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@edge-markets/connect-node 还在维护吗?

最新版本 4.0.0 发布于 2026/10/27(17 天前);至今共发布 23 个版本,其中过去 12 个月发布了 23 个;首次发布于 2026/01/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。