npm package: @protegeyinc/sdk-node
Description: Node.js SDK for Protegey signal ingestion
Latest version: 0.2.1

# @protegeyinc/sdk-node

Node.js SDK for [Protegey](https://protegey.com) — continental fraud intelligence for Africa.

Handles signal ingestion, outcome reporting, UFES v2 normalization, validation, batching, and retry — so your integration stays clean.

---

## Requirements

- Node.js >= 17.3.0
- A Protegey API key (`cen_live_sk_...` or `cen_sandbox_sk_...`)

---

## Installation

```bash
npm install @protegeyinc/sdk-node
# or
pnpm add @protegeyinc/sdk-node
```

---

## Quick start — Simple API

Best for server-side apps where a single identity is active per request context.

```ts
import { protegey } from '@protegeyinc/sdk-node'

// Once at startup
protegey.init({ apiKey: 'cen_live_sk_...' })

// On each request — set the actor
protegey.identify('user_hashed_id', 'individual', {
  phone: '+233241234567',
})

// Track an event
const result = await protegey.track('LOGIN_FAILURE', {
  auth_method: 'otp',
  failure_reason: 'wrong_pin',
})

console.log(result.data.risk_level) // 'low' | 'medium' | 'high' | 'critical' | 'minimal'
```

---

## Advanced API — `ProtegeyClient`

Use when you need per-request identity control, multiple tenants, or custom batching.

```ts
import { ProtegeyClient } from '@protegeyinc/sdk-node'

const client = new ProtegeyClient({
  apiKey: 'cen_live_sk_...',
  maxRetries: 3,
  batchSize: 10,
  flushIntervalMs: 2000,
  onWarning: (w) => console.warn('[protegey]', w.message),
})

const result = await client.track(
  'PAYMENT',
  { amount: 500, currency: 'GHS' },
  { phone: '+233241234567' },
  {
    eventCategory: 'transaction',
    sessionContext: {
      session_id: 'sess_abc123xyz',
      sequence: 3,
    },
  },
)

// Report outcome once the transaction resolves
await client.outcome(result.data.signal_id, {
  fraud_outcome: 'legitimate',
  event_outcome: 'success',
})

// Flush any buffered signals before process exit
await client.shutdown()
```

---

## Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | required | Your Protegey API key |
| `baseUrl` | `string` | production | Override for sandbox or self-hosted |
| `timeoutMs` | `number` | `10000` | Per-request timeout in ms |
| `maxRetries` | `number` | `2` | Retry attempts on 5xx / network errors |
| `idempotency` | `boolean` | `true` | Attach `Idempotency-Key` header on retries |
| `batchSize` | `number` | `10` | Flush when queue reaches this size |
| `flushIntervalMs` | `number` | `2000` | Auto-flush interval in ms (0 = disabled) |
| `flushConcurrency` | `number` | `5` | Max concurrent requests per flush |
| `maxPayloadBytes` | `number` | `524288` | Max signal size in bytes (512 KB) |
| `partnerUserId` | `string` | — | Partner-side user ID forwarded in `source` |
| `integrationChannel` | `'api' \| 'batch' \| 'stream'` | `'api'` | How signals are submitted |
| `sourceIpAddress` | `string` | — | Originating IP forwarded in `source` |
| `sourceConfidenceScore` | `number` | — | 0–1 confidence in the source data |
| `onWarning` | `(w: SdkWarning) => void` | — | Called when unknown fields are pruned |

---

## Tracking events

### `track(eventType, payload?, identify?, options?)`

```ts
await client.track(
  'ACCOUNT_TAKEOVER',
  { method: 'sim_swap' },
  {
    phone: '+234801234567',
    deviceIntelligence: {
      is_emulator: false,
      sdk_integrity: 'valid',
    },
  },
  {
    eventCategory: 'auth',
    traceId: 'trace_xyz',
    sessionContext: {
      session_id: 'sess_abc123xyz789',
      sequence: 1,
    },
    relatedEntities: [
      {
        entity_id: 'acct_hashed_receiver',
        relationship_type: 'receiver',
        source: 'declared',
      },
    ],
  },
)
```

### Outcome reporting

Call `outcome()` once you know the real-world result of an event:

```ts
await client.outcome(signalId, {
  fraud_outcome: 'fraud',       // 'fraud' | 'legitimate' | 'suspected' | 'unknown'
  fraud_type: 'scam',           // 'account_takeover' | 'scam' | 'laundering' | 'mule' | 'unknown'
  event_outcome: 'blocked',     // 'success' | 'failure' | 'blocked' | 'reversed'
  event_outcome_source: 'partner',
  outcome_timestamp: new Date(),  // Date objects are coerced to ISO strings
})
```

---

## Sandbox

```ts
protegey.init({
  apiKey: 'cen_sandbox_sk_...',
  baseUrl: 'https://api.protegey.com/api/v1',
})
```

Or against a local backend:

```bash
PROTEGEY_BASE_URL=http://localhost:8000/api/v1 node your-script.js
```

---

## Validation

The SDK validates every signal against the UFES v2 JSON schema **before sending**. Unknown fields are pruned and surfaced via `onWarning` rather than causing a hard failure — this keeps integrations compatible across SDK versions.

```ts
const client = new ProtegeyClient({
  apiKey: '...',
  onWarning: ({ path, message }) => {
    // e.g. path: "primary_actor.legacy_field", code: "unknown_field_pruned"
    logger.warn('Protegey SDK pruned unknown field', { path, message })
  },
})
```

A `UfesValidationError` is thrown (not retried) when:
- A required field is missing
- An enum value is invalid
- The signal exceeds `maxPayloadBytes`

---

## Error handling

```ts
import { ProtegeyRequestError, UfesValidationError } from '@protegeyinc/sdk-node'

try {
  await client.track('PAYMENT', payload, identity)
} catch (e) {
  if (e instanceof UfesValidationError) {
    // Schema problem — fix the payload, don't retry
    console.error('Validation failed:', e.details)
  } else if (e instanceof ProtegeyRequestError) {
    // HTTP error after all retries exhausted
    console.error(`HTTP ${e.statusCode}:`, e.body)
  }
}
```

---

## Lifecycle

```ts
// Flush all buffered signals and reject new track() calls
await client.shutdown()

// Or flush without shutting down (e.g. end of a batch job tick)
await client.flush()
```

---

## Smoke test

Run against a live backend to verify end-to-end connectivity:

```bash
PROTEGEY_API_KEY=cen_sandbox_sk_... npx tsx scripts/smoke.ts
```