# pancloud-nodejs Administrator & Developer Guide

This guide covers the operational, architectural, and contributor-facing
surface of `pancloud-nodejs` — the Palo Alto Networks Application
Framework SDK for Node.js. The User Guide handles getting code talking to
the SDK; this document handles deciding whether to ship it, hardening it,
running it in production, and extending it.

The audience is the architect choosing an SDK, the platform engineer
deploying a service that uses it, the security engineer reviewing the
OAuth2 flow, or the contributor opening a pull request against the
`lib/` tree. Cross-references to specific lines in `lib/*.ts` are
anchored to source commit `e0d9e6b`.

## 1. Architecture Overview

`pancloud-nodejs` is an HTTPS client library. There is no daemon, no
sidecar, no background service. Every API call is initiated by your
application code; the SDK adds OAuth2 token management, retries, JSON
schema validation, and (optionally) a Node `Readable` stream wrapper.

### Data flow

```
+----------------------+     +----------------------+     +-----------------------------+
|  Your Node.js app    |     |  pancloud-nodejs SDK |     |  Cortex / PAN identity      |
|                      |     |                      |     |                             |
|  - business logic    |     |  Credentials object  |     |  api.paloaltonetworks.com   |
|  - calls SDK methods +---->+  CoreClass.fetchX*   +---->+    /api/oauth2/RequestToken |
|  - consumes results  |     |  retrier (3 / 100ms) |     |    /api/oauth2/RevokeToken  |
|                      |     |  fetch() (Node https)|     |                             |
|                      |     |                      |     |  api.us.paloaltonetworks.com|
|                      |     |  LoggingService      +---->+    logging-service/v1/*     |
|                      |     |  EventService        |     |                             |
|                      |     |  DirectorySyncService|     |  api.eu.paloaltonetworks.com|
|                      |     |                      |     |    event-service/v1/*       |
|                      |     |  Optional:           |     |    directory-sync-service/v1|
|                      |     |  CortexHubHelper     |     |                             |
|                      |     |  L2 correlator       |     |                             |
+----------------------+     +----------------------+     +-----------------------------+
```

> [!NOTE]
> Every outbound request is HTTPS. There is no plaintext fallback, no
> opt-out for TLS verification, and no proxy configuration in the SDK
> itself — proxies must be set at the Node runtime level (e.g. via
> `HTTPS_PROXY` and an agent), because `lib/fetch.ts` calls
> `https.request` directly without honoring the standard proxy env vars.

### Component inventory

| Component | Path | Role |
|---|---|---|
| Entry point | `lib/index.ts` | Re-exports the public API. The 19 names listed here are the SDK contract. |
| Core HTTP client | `lib/fetch.ts` | Thin wrapper over `https.request` returning `FetchResponse {ok, status, statusText, size, text(), json()}`. |
| Base class | `lib/core.ts` | `CoreClass` — implements auto-refresh, retry, fetch wrappers (GET/POST/PUT/DELETE). All service clients extend this. |
| Common utilities | `lib/common.ts` | `LogLevel`, `EntryPoint` type, `LogType` literals, `retrier`, `commonLogger`, JWT helpers. |
| Credentials base | `lib/credentials.ts` | Abstract `Credentials` class + `StaticCredentials` + `defaultCredentialsFactory`. |
| Auto-credentials | `lib/autocredentials.ts` | The env-driven `autoCredentials()` selector. |
| Memory provider | `lib/credentialprovider.ts` | `CortexCredentialProvider` abstract + `DefaultCredentialsProvider` concrete. Implements OAuth2 refresh flow. |
| Filesystem provider | `lib/fscredentialprovider.ts` | `FsCredProvider` — same as the memory provider plus AES-128 encrypted `PANCLOUD_CONFIG.json` persistence. |
| Developer-token credentials | `lib/devtokencredentials.ts` | `DevTokenCredentials` — exchanges a developer token for an access token via the `apiexplorer.rocks` provider. |
| Hub helper | `lib/hubhelper.ts` | `CortexHubHelper` — abstract OAuth2 authorization-code flow, multi-tenant. |
| Logging Service client | `lib/loggingservice.ts` | `LoggingService` + `LoggingServiceClient`. |
| Streaming wrapper | `lib/loggingserviceclient.ts` | `LoggingServiceClient` extends Node `Readable`. |
| Event Service client | `lib/eventservice.ts` | `EventService` extends `Emitter`. |
| Directory Sync client | `lib/directorysyncservice.ts` | `DirectorySyncService` extends `CoreClass`. |
| Async event base | `lib/emitter.ts` | `Emitter` extends `CoreClass`. Provides three event topics: `EVENT_EVENT`, `PCAP_EVENT`, `CORR_EVENT`. |
| Correlation engine | `lib/l2correlator.ts` | `MacCorrelator` — in-memory L2/L3 join by `sessionid`. |
| Errors | `lib/error.ts` | `SdkErr`, `PanCloudError`, `ApplicationFrameworkError`, `isSdkError`. |
| Stale `PanCloudError` | `lib/exceptions.ts` | A second 14-line `PanCloudError` class — see Section 8. |
| Utility | `lib/util.ts` | `Util.dnsDecode()`, `Util.pcaptize()`. |

> [!NOTE]
> `lib/exceptions.ts` defines a separate, unrelated `PanCloudError` class
> that is imported by `lib/directorysyncservice.ts` (line 1350) — a
> minor inconsistency with the main error in `lib/error.ts`. See Section 8
> for details.

### Class hierarchy at a glance

```
CoreClass (lib/core.ts)
├── Emitter (lib/emitter.ts)
│   ├── EventService (lib/eventservice.ts)  implements Iterable<Promise<EsEvent[]>>
│   └── LoggingService (lib/loggingservice.ts)
└── DirectorySyncService (lib/directorysyncservice.ts)

Credentials (abstract, lib/credentials.ts)
├── StaticCredentials (no refresh)
├── DevTokenCredentials (apiexplorer.rocks flow)
└── DefaultCredentials (created by CortexCredentialProvider subclasses)

CortexCredentialProvider<T, K> (abstract, lib/credentialprovider.ts)
├── DefaultCredentialsProvider (in-memory)
└── FsCredProvider<T, K> (encrypted JSON file)

CortexHubHelper<T, U, K, M> (abstract, lib/hubhelper.ts)
└── (implementer's storage subclass)

stream.Readable
└── LoggingServiceClient (lib/loggingserviceclient.ts)

Error (built-in)
└── SdkErr (lib/error.ts)
    ├── PanCloudError
    └── ApplicationFrameworkError
```

### Technology stack

| Layer | Technology | Notes |
|---|---|---|
| Language | TypeScript | Strict mode (`noImplicitAny`, `strictNullChecks`); compiled to ES2017 CommonJS. |
| Runtime | Node.js 8+ | Async/await, `Symbol.iterator`, `Buffer`, `URL`. |
| HTTP client | Node `https` | Direct `https.request`; no `node-fetch`, no `axios`, no `got`. |
| Auth | OAuth2 client-credentials and authorization-code | RFC-compliant token refresh + revocation. |
| Crypto | Node `crypto` | SHA-1 for `uid()`, AES-128-CBC for the FS provider's at-rest cache. |
| Streaming | Node `stream.Readable` | Object-mode for the `LoggingServiceClient` wrapper. |
| Eventing | Node `events.EventEmitter` | Used inside `Emitter` to dispatch to user callbacks. |
| Tests | TypeScript scripts under `test/` | Each `test/<scenario>.ts` is a runnable launcher. No Mocha/Jest. |
| Build | `tsc` | Two scripts: `build:library` (publishes `dist/`), `build:test` (compiles `test/*.ts`). |

> [!NOTE]
> The SDK's `package.json` has empty `dependencies` and empty
> `devDependencies` blocks. Every external API the SDK calls is
> satisfied by the Node standard library; the `tsc` toolchain is
> expected to be installed in your environment, not pulled in as a dev
> dependency.

### Internal data flow: a Logging Service query

```
app                   LoggingService            CoreClass             Credentials      Cortex API
 |                         |                         |                     |               |
 | factory(cred)           |                         |                     |               |
 |------------------------>|                         |                     |               |
 | query(cfg)              |                         |                     |               |
 |------------------------>|                         |                     |               |
 |                         | fetchPostWrap('/queries', body)               |               |
 |                         |------------------------>|                     |               |
 |                         |                         | checkAutoRefresh()  |               |
 |                         |                         |-------------------->|               |
 |                         |                         |   (refresh if <5min)|               |
 |                         |                         |                     |               |
 |                         |                         | retrier(fetch)      |               |
 |                         |                         |---------------------+-------------->|
 |                         |                         |   POST /queries     |   200 + JSON  |
 |                         |                         |<--------------------+---------------|
 |                         |  isJobResult(rJson)?    |                                     |
 |                         |  jobQueue[queryId]={..} |                                     |
 |                         |  schedule autoPoll      |                                     |
 |  jobPromise             |                                                               |
 |<------------------------|                                                               |
 |                         |  ... auto-poll loop ...                                       |
 |                         |  emitMessage -> user callBack.event                           |
 |                         |  resolve(jobPromise) on JOB_FINISHED                          |
 | (resolved)              |                                                               |
 |<------------------------|                                                               |
```

## 2. System Requirements

### Runtime

| Item | Minimum | Recommended | Notes |
|---|---|---|---|
| Node.js | 8.0 | 14+ | The SDK's TypeScript target is ES2017. The streaming wrapper requires `stream.Readable` from Node ≥ 8. |
| Memory | 64 MB headroom | 256 MB | The L2 correlation engine buffers events in memory; sustained high-rate streaming may need more. |
| File descriptors | default | default | One TCP/TLS connection per active service instance plus any in-flight retries. |
| Outbound network | TCP 443 to PAN regional API | TCP 443 + low latency | Long-running Event Service auto-poll opens connections continuously; latency directly affects effective event rate. |
| Disk | none required | < 1 MB | Only the FS credential provider writes anything (`PANCLOUD_CONFIG.json`). |

### Build-time

| Item | Version |
|---|---|
| TypeScript | 2.7+ (the codebase uses `strictNullChecks` and tagged unions). |
| `tsc` available on `PATH` | required by `npm run build:library` and `npm run build:test`. |

### Network endpoints

The SDK speaks to two classes of endpoints:

| Endpoint | Purpose | Code reference |
|---|---|---|
| `https://api.paloaltonetworks.com/api/oauth2/RequestToken` | OAuth2 token refresh / authorization-code exchange | `lib/credentialprovider.ts:547` (`IDP_TOKEN_URL`) |
| `https://api.paloaltonetworks.com/api/oauth2/RevokeToken` | OAuth2 refresh-token revocation | `lib/credentialprovider.ts:548` (`IDP_REVOKE_URL`) |
| `https://identity.paloaltonetworks.com/as/authorization.oauth2` | OAuth2 authorization-code request URL (from `CortexHubHelper`) | `lib/hubhelper.ts:3092` (`IDP_AUTH_URL`) |
| `https://app.apiexplorer.rocks/request_token` | Default developer-token consumer URL | `lib/devtokencredentials.ts:1249` (`DEV_TOKEN_PROVIDER`) |
| `https://api.us.paloaltonetworks.com` | Cortex Data Lake API — Americas | `lib/common.ts` `EntryPoint` |
| `https://api.eu.paloaltonetworks.com` | Cortex Data Lake API — Europe | `lib/common.ts` `EntryPoint` |

> [!IMPORTANT]
> Egress firewalls must allow all four `*.paloaltonetworks.com` hosts plus
> `app.apiexplorer.rocks` (only if you use `DevTokenCredentials`). The
> SDK does not honor `HTTP_PROXY` / `HTTPS_PROXY` env vars at the library
> level; if your environment requires a proxy, configure it via
> `https.globalAgent` or by overriding the `fetch()` export.

## 3. Configuration Reference

This section catalogues every constructor option, factory parameter, and
environment variable the SDK reads.

### 3.1 Environment variables

| Variable | Read by | Type | Default | Required? |
|---|---|---|---|---|
| `PAN_CLIENT_ID` | `defaultCredentialsProviderFactory`, `fsCredentialsFactory`, `autoCredentials` | string | none | Yes for refresh-token flow |
| `PAN_CLIENT_SECRET` | same | string | none | Yes for refresh-token flow |
| `PAN_REFRESH_TOKEN` | `defaultCredentialsProviderFactory`, `autoCredentials` | string | none | Yes for memory provider |
| `PAN_ACCESS_TOKEN` | `autoCredentials` | string (JWT) | none | Yes for static fallback |
| `PAN_ENTRYPOINT` | all | `EntryPoint` literal | `https://api.us.paloaltonetworks.com` (assumed by `autoCredentials` when unset) | Recommended |
| `PAN_DEVELOPER_TOKEN` | `DevTokenCredentials` | string | none | Yes for dev-token flow |
| `PAN_DEVELOPER_TOKEN_PROVIDER` | `DevTokenCredentials` | URL | `https://app.apiexplorer.rocks/request_token` | No |
| `${envPrefix}_CLIENT_ID` etc. | `defaultCredentialsProviderFactory` with `envPrefix` | string | as above with custom prefix | No |

The `envPrefix` option lets two providers in the same process read from
different env vars (e.g. `PROD_CLIENT_ID`, `STAGING_CLIENT_ID`).

### 3.2 `CredentialProviderOptions` (memory and filesystem providers)

Defined in `lib/credentialprovider.ts:608`:

| Field | Type | Default | Description |
|---|---|---|---|
| `idpTokenUrl` | string | `https://api.paloaltonetworks.com/api/oauth2/RequestToken` | Override the IDP token endpoint (e.g. for testing). |
| `idpRevokeUrl` | string | `https://api.paloaltonetworks.com/api/oauth2/RevokeToken` | Override the IDP revoke endpoint. |
| `accTokenGuardTime` | number (seconds) | `300` | Refresh window before access token expires. Hard-capped at `3300`. |
| `retrierAttempts` | number | `3` | HTTP retry attempts during IDP refresh / revoke. |
| `retrierDelay` | number (ms) | `100` | Delay between retries. |

`fsCredentialsFactory` adds `secret: string` (mandatory; AES-128 key
material) plus `envPrefix?: string`.

### 3.3 `CoreOptions` and `EmitterOptions`

`CoreOptions` (lib/core.ts:352) — shared by every service factory:

| Field | Type | Default | Description |
|---|---|---|---|
| `level` | `LogLevel` | `LogLevel.INFO` | Verbosity for `commonLogger`. Setting it on a single instance overrides the global. |
| `retrierCount` | number | `3` | HTTP retries per service call. |
| `retrierDelay` | number (ms) | `100` | Delay between retries. |
| `fetchTimeout` | number (ms) | unset | Per-request timeout passed to `https.request`. |

`EmitterOptions` (lib/emitter.ts:1867) extends `CoreOptions` with:

| Field | Type | Default | Description |
|---|---|---|---|
| `allowDup` | boolean | `false` | Allow duplicate listeners on a topic. |
| `l2Corr.timeWindow` | number (s) | `120` | Correlation buffer age-out window. |
| `l2Corr.absoluteTime` | boolean | `false` | Use wall-clock instead of stream timestamps for age-out. |
| `l2Corr.gcMultiplier` | number | `0` | GC every N events. `0` runs the collector every event. |

### 3.4 `LsOptions` (LoggingService)

| Field | Type | Default | Description |
|---|---|---|---|
| `autoPollSleep` | number (ms) | `200` | Delay between auto-poll iterations. |
| `controlListener` | `(m: LsControlMessage) => void` | unset | Receive `{queryId, lastKnownStatus, totalHits}` after every batch. |

### 3.5 `EsOptions` (EventService)

| Field | Type | Default | Description |
|---|---|---|---|
| `channelId` | string | `"EventFilter"` | Server-side channel ID. Multiple `EventService` instances against the same channel will compete. |
| `autoPollSleep` | number (ms) | `200` | Delay between auto-poll iterations. |
| (inherits `EmitterOptions`) | | | |

### 3.6 `LsQueryCfg` (a single Logging Service query)

| Field | Type | Required | Description |
|---|---|---|---|
| `query` | string | Yes | SQL `SELECT ... FROM panw.<table> [WHERE ...]`. |
| `startTime` | number (s, Unix epoch) | Yes | Inclusive lower bound. |
| `endTime` | number (s, Unix epoch) | Yes | Inclusive upper bound. |
| `maxWaitTime` | number (ms) | No | Server-side wait for first response (max 30000). |
| `client` | string | No | Free-form app identifier. |
| `clientParameters` | any | No | Echoed back in every result; useful for trace correlation. |
| `logType` | `LogType` | Recommended | Spares the SDK from guessing the log type during emit. |
| `callBack.event` | function | No | Trigger auto-poll, deliver records here. |
| `callBack.pcap` | function | No | Trigger auto-poll, deliver decoded PCAP buffers. |
| `callBack.corr` | function | No | Trigger auto-poll, deliver L2/L3 correlations. |

### 3.7 `EsFilterBuilderCfg` (Event Service filter)

| Field | Type | Required | Description |
|---|---|---|---|
| `filter[].table` | `LogType` | Yes | One of the `panw.*` tables (or `tms.traps`). |
| `filter[].where` | string | No | WHERE predicate appended to the SELECT. |
| `filter[].timeout` | number (ms) | No | Per-filter poll wait (max 60000, default 60000). |
| `filter[].batchSize` | number | No | Max events per poll (min 1, default 1). |
| `flush` | boolean | No | Discard buffered events when applying filter. |
| `filterOptions.callBack` | object | No | Same shape as the LS callback. |
| `filterOptions.poolOptions.ack` | boolean | No | Auto-ack each batch. |
| `filterOptions.poolOptions.pollTimeout` | number (ms) | No | Wait when channel empty (default 1000). |

### 3.8 The `Credentials` constructor

```typescript
constructor(entryPoint: EntryPoint, guardTime?: number)
```

| Parameter | Type | Default | Notes |
|---|---|---|---|
| `entryPoint` | `EntryPoint` | required | One of the two regional URLs. |
| `guardTime` | number (s) | `300` | Refresh window. Capped at `3300`. |

### 3.9 The `LoggingServiceClient` constructor

```typescript
new LoggingServiceClient(ls, cfg & { retries?: number, delay?: number }, opts?)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `ls` | `LoggingService` | required | The service instance to issue the query against. |
| `cfg` | `LsQueryCfg` | required | Same as `ls.query()` config. |
| `cfg.retries` | number | `20` | Polls allowed in `RUNNING` before giving up. |
| `cfg.delay` | number (ms) | `200` | Delay between RUNNING-state polls. |
| `opts` | `ReadableOptions` | objectMode forced | Forwarded to `Readable` super-constructor. |

> [!IMPORTANT]
> `cfg.callBack` is silently deleted by the constructor — the streaming
> wrapper drives polling itself. Do not pass a callback through this
> path; it is dead code from the consumer's perspective.

## 4. Deployment & Scaling

### Single-process deployment

The default shape: one Node process, one `Credentials` object, one or
more service-client instances. This works up to whatever rate a single
event loop can sustain — typically thousands of records per second for
the streaming `LoggingServiceClient`, limited by network round-trip to
the regional API.

Deployment surface:

- One process, one `Credentials` instance per data lake.
- Reuse the `Credentials` across `LoggingService`, `EventService`,
  `DirectorySyncService` instances; refreshes are deduped via
  `validUntil`.
- Use `LoggingServiceClient` rather than the raw query API for sustained
  reads; it backpressures naturally through the Node stream protocol.

> [!TIP]
> Run with `NODE_OPTIONS="--max-old-space-size=512"` (or higher) if you
> turn on the L2 correlation engine with a long `timeWindow` — the
> correlator buffer can grow to thousands of entries before the GC
> fires.

### Multi-process scaling

Because each `EventService` instance binds to a server-side channel,
horizontal scale-out is constrained: two processes against the same
channel ID compete for events. Two patterns work:

1. **Sharded channel IDs**: pass `channelId: 'shard-A'`, `shard-B`, ... to
   each instance and configure the upstream filter list per shard.
2. **One process owns the channel**: a single consumer reads events and
   re-publishes them to a queue (Redis Streams, NATS, Kafka). Workers
   pick from the queue.

Logging Service queries are stateless from the SDK side — a second
process can issue a parallel query without disturbing the first. The
server-side limit is the per-tenant concurrent query quota imposed by
PAN.

### Multi-tenant / multi-datalake deployment

When one process serves many Cortex Data Lake tenants, use
`fsCredentialsFactory()` (or a custom `CortexCredentialProvider`
subclass) and call `registerManualDatalake()` once per tenant. Each
tenant gets its own `Credentials` object, its own refresh schedule, and
its own row in the encrypted `PANCLOUD_CONFIG.json`.

The `CortexHubHelper` abstract class is the recommended path for
production multi-tenant apps because it covers:

- Authorization-code exchange via the user's browser.
- Tenant-id validation against `req.user[tenantKey]` in an Express-like
  request.
- Storage of pending authorization states.
- Datalake metadata (a free-form `T` parameter) attached to each
  registered datalake.

You implement six abstract methods (`_listDatalake`, `_getDatalake`,
`_upsertDatalake`, `_deleteDatalake`, `requestAuthState`,
`restoreAuthState`, `deleteAuthState`) against your storage of choice
(Postgres, DynamoDB, etc.).

> [!WARNING]
> The `forceCallbackTenantValidation` option in `CortexHelperOptions`
> defaults to `false`. In a multi-tenant deployment, set it to `true`
> and ensure your auth middleware populates `req.user[tenantKey]` so the
> callback handler can verify the requester actually owns the datalake
> they are activating.

### Container deployment notes

A minimal `Dockerfile` for a service that uses this SDK:

```dockerfile
FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
USER node
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]
```

Inject secrets via env vars at runtime:

```bash
docker run --rm \
  -e PAN_CLIENT_ID=... \
  -e PAN_CLIENT_SECRET=... \
  -e PAN_REFRESH_TOKEN=... \
  -e PAN_ENTRYPOINT=https://api.us.paloaltonetworks.com \
  my-pancloud-app:latest
```

For Kubernetes, mount these from a `Secret`, not a `ConfigMap`:

```yaml
envFrom:
  - secretRef:
      name: pancloud-credentials
```

## 5. Operations

### Monitoring

Every service client exposes a stats getter. Scrape these on a timer:

| Service | Method | Counters |
|---|---|---|
| `LoggingService` | `getLsStats()` | `apiTransactions`, `eventsEmitted`, `pcapsEmitted`, `correlationEmitted`, `records`, `queries`, `polls`, `deletes`, `writes`. |
| `EventService` | `getEsStats()` | All of `LsStats` plus `acks`, `nacks`, `flushes`, `polls`, `filtergets`, `filtersets`. |
| `DirectorySyncService` | `getDssStats()` | `apiTransactions`, `queryCalls`, `domainCalls`, `attributeCalls`, `countCalls`. |

Tie each to a Prometheus gauge or your APM of choice. Alert thresholds
that have proven useful:

- `nacks > 0` over 5 minutes → consumer is rejecting events; investigate.
- `queries > 0` but `records = 0` over an hour → upstream data flow has
  stopped; check the firewall side.
- `apiTransactions` grows but `polls` stays flat → process is paused or
  blocked.

> [!TIP]
> The L2 correlator emits its own stats inside `EmitterStats.correlationStats`
> (`agedOut`, `dbWaterMark`, `dbInserts`, `discardedEvents`). A growing
> `dbWaterMark` paired with `agedOut > 0` suggests the time window is
> too short for the actual flow lifetimes.

### Logging

The SDK uses a single `commonLogger` exported from `lib/common.ts`. By
default it writes JSON-ish lines prefixed with `PANCLOUD: ` to stdout
(level INFO/ALERT) and stderr (level ERROR). To pipe into a structured
logger:

```typescript
import { setLogger, LogLevel } from 'pancloud-nodejs'
import pino from 'pino'

const log = pino()
setLogger({
    level: LogLevel.INFO,
    error: e =>  log.error({ src: e.getSourceClass(), code: e.getErrorCode() }, e.getErrorMessage()),
    alert: (s, m, n) => log.warn({ src: s.className, name: n }, m),
    info:  (s, m, n) => log.info({ src: s.className, name: n }, m),
    debug: (s, m, n, p) => log.debug({ src: s.className, name: n, payload: p }, m)
})
```

> [!NOTE]
> The logger truncates the `payload` field to 300 characters before
> emitting. If you need full payload visibility for debugging, replace
> the logger and avoid the SdkLogger's truncation.

### Health checks

There is no built-in `/health` endpoint — the SDK is a library, not a
server. A working liveness check for a service that uses the SDK:

```typescript
async function healthCheck(cred: Credentials): Promise<boolean> {
    try {
        await cred.autoRefresh()
        return true
    } catch {
        return false
    }
}
```

This exercises the OAuth2 refresh flow (it is a no-op if the token has
plenty of life left) and forces a real IDP round-trip when the cached
token is stale.

> [!TIP]
> For a "deeper" check, run a one-record `LoggingService` query against
> a small known time window. If you do, set `maxWaitTime: 5000` so the
> liveness probe times out fast.

### Runbook: refresh token revoked

**Symptoms:** `IDENTITY` errors from `idpRefresh`, every service call
fails with HTTP 400 from `/api/oauth2/RequestToken`.

**Recovery:**

1. Confirm with `setLogLevel(LogLevel.DEBUG)` that the failure is
   IDP-side (`source: "CortexCredentialProvider"`).
2. Re-issue a refresh token from the Cortex hub for the affected
   `client_id`.
3. Update the running env var (or the `PANCLOUD_CONFIG.json` for
   FS-based deployments).
4. Restart the process.

**Prevention:** rotate refresh tokens deliberately. The SDK auto-stores
new refresh tokens that come back in the IDP response (see
`CortexCredentialProvider.retrieveCortexAccessToken`, line ~887), so
graceful PAN-side rotations should propagate without operator action.

### Runbook: Event Service backlog

**Symptoms:** `polls` keeps climbing but `records` per minute is dropping;
PAN-side backlog warning.

**Recovery:**

1. `await es.flush()` to drop everything currently buffered (data loss
   is acceptable if you can re-query).
2. Or, increase `batchSize` on each filter entry to receive more events
   per poll round-trip.
3. Or, shard: split the filter list across multiple `EventService`
   instances on different `channelId` values.

### Runbook: stuck `LoggingServiceClient`

**Symptoms:** Stream never ends, no `data` events, no `error`.

**Recovery:**

1. Check `setLogLevel(LogLevel.DEBUG)` output for repeated `RUNNING`
   responses.
2. If the query has been running > 60 s and the SDK has hit its
   `retries` ceiling, the stream emits an `error`; if not, raise
   `retries` and `delay` in the constructor.
3. Manually `stream.destroy()` to abort and trigger `deleteQuery`.

## 6. Performance

### HTTP-level performance

Every service client routes calls through `CoreClass.fetchXWrap`, which:

1. Increments `apiTransactions`.
2. Calls `checkAutoRefresh()` (potentially adding one IDP round-trip if
   the token is stale).
3. Calls `retrier(this, retrierCount, retrierDelay, fetch, url, init)`.
4. Parses the response as JSON and validates.

The retrier (`lib/common.ts:276`) catches every exception and retries —
including HTTP 5xx responses, but only because the underlying `fetch`
implementation throws on those. This means a single SDK call can take
up to `retrierCount * (request_time + retrierDelay)` seconds in the
worst case.

> [!TIP]
> If your service must respond within strict deadlines (e.g. a webhook
> that needs to ack within 5 seconds), set `retrierCount: 1` and
> `fetchTimeout` to a value comfortably under your deadline. The SDK
> will then surface failures immediately rather than retrying through
> the deadline.

### Auto-poll cost

Both `LoggingService` and `EventService` schedule auto-poll iterations
every `autoPollSleep` ms (default 200 ms). For a service that always
has data, the SDK uses `setImmediate` instead of `setTimeout` to keep
the loop tight (`EventService.autoPoll`, line ~2715). Idle channels
fall back to the 200 ms interval.

Per-poll cost is one HTTPS request + one JSON parse + a type-guard
walk over the response. On modern Node it is well under 1 ms of CPU
plus the network round-trip.

### Memory profile of the L2 correlator

The correlator stores one entry per uncorrelated event in `db: DbItem[]`.
Each entry is roughly 200 B (a wrapper plus the underlying event
reference). Worst-case buffer size is bounded by `dbWaterMark` × event
size. Tune `timeWindow` down (e.g. to 60 s) when correlations are
expected to occur quickly.

### Streaming throughput

`LoggingServiceClient` is object-mode — each chunk is a JavaScript
array, not bytes. The natural backpressure is the Node stream
high-water mark on the consumer side; pause the source and the
`pusher` function will stop calling `process.nextTick`.

For high-record-count queries, prefer `LoggingServiceClient` to the
auto-poll callback pattern: the stream interface gives consumers
explicit backpressure, while the callback pattern fires events as fast
as the SDK polls them.

> [!NOTE]
> The streaming wrapper has its own retry constants (`RETRIES = 20`,
> `DELAY = 200`) that are independent of the parent `LoggingService`'s
> retrier. Increase these for queries that take longer than 4 s of
> wall-clock to start producing results.

## 7. Security

This section is a security review for the architect or AppSec engineer
evaluating the SDK.

### OAuth2 secret handling

The SDK accepts three categories of secrets:

| Secret | Lifetime | Where stored |
|---|---|---|
| `client_secret` | Long-lived (issued in the Cortex hub) | In memory only by default. `fsCredentialsFactory` does not persist it — the file holds only `accessToken` + `refreshToken`, encrypted. |
| `refresh_token` | Long-lived (until revoked) | In memory by default. AES-128-CBC encrypted in `PANCLOUD_CONFIG.json` if you use the FS provider. |
| `access_token` | Short-lived (≤ 1 hour) | In memory; encrypted in the FS file alongside the refresh token. |

> [!CAUTION]
> Never log secrets. The SDK's debug logger truncates payloads to 300
> chars to reduce accidental leakage, but this is not a substitute for
> reviewing your own code: the SDK's payload printer in
> `commonLogger.debug` will print the request body (which contains the
> refresh token) when it sends a refresh request. Set
> `LogLevel.INFO` or higher in production to avoid this.

### Encryption at rest (FS provider)

`fsCredentialsFactory` encrypts the cached tokens in `PANCLOUD_CONFIG.json` with AES-128-CBC, using key material
derived from the caller-provided `secret` (`passIvGenerator` in `lib/fscredentialprovider.ts`). Keep that `secret`
in your secret manager, not next to the file, and restrict the file to the service account.

> [!TIP]
> For production services that already run a KMS or vault (AWS KMS, GCP KMS, HashiCorp Vault), implement a
> `CortexCredentialProvider` subclass that stores credentials there: override `loadCredentialsDb`,
> `createCredentialsItem`, `updateCredentialsItem` and `deleteCredentialsItem` to talk to your secret store
> directly.

### TLS verification

`lib/fetch.ts` calls Node's `https.request` with no overrides for
`rejectUnauthorized`, no custom CA bundle, and no TLS pinning. The
default Node behaviour applies: certificates are validated against the
system root store. There is no way to disable verification through the
SDK's public API.

If you need a custom CA (e.g. for a corporate MITM proxy), set
`NODE_EXTRA_CA_CERTS` in the environment.

> [!CAUTION]
> Do not patch `tls.checkServerIdentity` to bypass verification "for
> testing" in code that ships to production. The SDK speaks to public
> PAN endpoints with public CA-signed certs; there is no legitimate
> reason for the verification to fail.

### Request payload review

The SDK sends these payloads. Audit them against your DLP / network
review:

| Endpoint | Method | Body | Sensitive fields |
|---|---|---|---|
| `IDP_TOKEN_URL` | POST | `{client_id, client_secret, refresh_token, grant_type}` | `client_secret`, `refresh_token` |
| `IDP_TOKEN_URL` (auth-code) | POST | `{client_id, client_secret, redirect_uri, grant_type, code}` | `client_secret`, `code` |
| `IDP_REVOKE_URL` | POST | `{client_id, client_secret, token, token_type_hint}` | `client_secret`, `token` |
| `logging-service/v1/queries` | POST | `LsQueryCfg` (without `logType` and `callBack`) | `query` (could leak schema info) |
| `event-service/v1/channels/{id}/filters` | PUT | `EsFilter` | `filter` (could leak schema info) |
| `directory-sync-service/v1/{class}` | POST | `DssQueryFilter` | `name.attributeValue` (user data) |

Logs of any of these payloads at debug level expose secrets. Production
log policy: never enable `LogLevel.DEBUG` long-term.

### Tenant isolation

`CortexCredentialProvider` is parameterised by a metadata type `T` and
a tenant-key field `K`. When you implement a multi-tenant subclass:

- Always set `tenantKey` so that `selectDatalakeByTenant()` can filter.
- Validate the tenant ID *before* calling `getCredentialsObject()`.
  `CortexHubHelper.getCredentialsObject` does this:
  ```typescript
  let activeList = await this.credProvider.selectDatalakeByTenant(tenantId as any)
  if (!activeList.includes(datalakeId)) {
      throw new PanCloudError(CortexHubHelper, 'CONFIG', `datalake ${datalakeId} not found`)
  }
  ```
- Set `forceCallbackTenantValidation: true` to require the requester's
  tenant ID to match the state-stored tenant ID during OAuth2 callback.

> [!WARNING]
> A subclass that returns a `Credentials` object without verifying the
> requesting tenant owns it has cross-tenant data leakage. Treat
> `getCredentialsObject(datalakeId)` as a privileged operation; never
> expose it to untrusted input.

### Refresh-token rotation

PAN's IDP may return a new `refresh_token` in any refresh response.
The SDK detects this:

```typescript
if (idpResponse.refresh_token) {
    credentials.refreshToken = idpResponse.refresh_token
    commonLogger.info(CortexCredentialProvider, 'Received new Cortex Refresh Token')
}
await this.updateCredentialsItem(datalakeId, credentials)
```

It then calls the storage subclass's `updateCredentialsItem`. **Make
sure your subclass actually persists the new value** — a no-op
implementation will lose the new refresh token on the next process
restart and you will be back to the revoked token.

Recommended cadence:

| Asset | Rotation cadence | Mechanism |
|---|---|---|
| `client_secret` | Yearly minimum | Re-issue in Cortex hub; redeploy with new secret. |
| `refresh_token` | Quarterly | Revoke old, generate new from hub. |
| `access_token` | Per request | Automatic via `autoRefresh`. |
| FS provider AES secret | When personnel leave | Decrypt with old, re-encrypt with new on rotation. |

### What NOT to log

- Raw `access_token` JWTs.
- Raw `refresh_token` strings.
- The `client_secret`.
- Customer log payloads — `panw.threat`, `panw.dpi`, etc. contain
  packet captures and URL data subject to your customer's privacy
  policy.
- Directory Sync entries — usernames, email addresses, group
  memberships.

> [!CAUTION]
> The SDK's debug logger will happily print every HTTP body as it goes
> over the wire. In production, set `LogLevel.INFO` (the default) and
> never override the `commonLogger.debug` method to write to long-term
> log storage.

### Sensitive-data sinks

| File | Risk | Mitigation |
|---|---|---|
| stdout/stderr at `LogLevel.DEBUG` | Refresh tokens, access tokens, query payloads | Only enable in short, supervised debugging sessions. |
| `PANCLOUD_CONFIG.json` (FS provider) | Encrypted refresh + access tokens | File ownership + permissions to the service user only; the SDK fails if read/write permission is wrong (`fscredentialprovider.ts:3036`). |
| Memory dumps | All of the above + JS heap | Disable core dumps in production; use process-isolated containers. |

## 8. Developer Internals

This section is for SDK contributors and integrators reading the source.

### Repository layout

```
pancloud-nodejs/
├── lib/                          TypeScript source (canonical)
│   ├── index.ts                  Re-exports the public API
│   ├── core.ts                   CoreClass: HTTP wrappers + retry + refresh
│   ├── common.ts                 Logger, retrier, types, constants
│   ├── credentials.ts            Credentials abstract + Static
│   ├── credentialprovider.ts     CortexCredentialProvider abstract + Default
│   ├── fscredentialprovider.ts   FsCredProvider (encrypted JSON file)
│   ├── devtokencredentials.ts    DevTokenCredentials
│   ├── autocredentials.ts        autoCredentials() selector
│   ├── hubhelper.ts              CortexHubHelper (auth-code flow)
│   ├── loggingservice.ts         LoggingService + LsControlMessage
│   ├── loggingserviceclient.ts   LoggingServiceClient (Readable stream)
│   ├── eventservice.ts           EventService (Iterable + auto-poll)
│   ├── directorysyncservice.ts   DirectorySyncService
│   ├── emitter.ts                Emitter base class (3 topics)
│   ├── l2correlator.ts           MacCorrelator (in-memory L2/L3 join)
│   ├── error.ts                  SdkErr + PanCloudError + ApplicationFrameworkError
│   ├── exceptions.ts             A second, unrelated PanCloudError class (legacy)
│   ├── fetch.ts                  https.request wrapper -> FetchResponse
│   └── util.ts                   Util.dnsDecode + Util.pcaptize
├── test/                         Hand-written TypeScript scenarios (not unit tests)
│   ├── credential.ts, credential_basic.ts, credential_fs.ts
│   ├── eventservice*.ts          11 EventService scenarios
│   ├── loggingservice*.ts        6 LoggingService scenarios
│   ├── dss*.ts                   8 DirectorySync scenarios
│   ├── dnsdecode.ts, pcap.ts     Utility scenarios
│   └── tsconfig.json             Test build config
├── package.json                  name=pancloud, version=1.0.0
├── tsconfig.json                 Library build config
├── README.md
└── .gitignore
```

### State machines

#### LoggingService query state machine

```
              query()                             poll() / autoPoll
   START -------------> [submitted]  <-----+--------+
                            |              |        |
                            |              |        |
              JOB_FAILED    |   FINISHED   |        |  RUNNING
                +-----------+--------------+--------+
                |                          |
                v                          v
            [failed]               sequenceNo++
                                          |
                                          | JOB_FINISHED
                                          v
                                     [finished] -- emitterCleanup -> resolve
```

Key invariants:

- `pendingQueries = Object.keys(jobQueue)` is rebuilt every transition.
- The auto-poll timer (`tout`) is scheduled iff `pendingQueries.length > 0`.
- `cancelPoll(qid, err?)` removes the entry, resolves or rejects the
  pending promise, and calls `deleteQuery` to release server resources.

#### EventService channel state machine

```
                  factory()
   START -------------------> [no filter]
                                  |
              filterBuilder() / setFilters()
                                  |
                                  v
                           [filter applied]
                            |       ^
                  poll()    |       | clearFilter()
                            v       |
                       [polling]   ...
                            |
                  filterOptions.callBack provided?
                       /            \
                     yes             no
                      |               |
                      v               v
                [auto-poll loop]  [manual poll only]
                      |
                pause() / resume()
```

- `popts` (poll options) is captured from `filterOptions.poolOptions` on
  every `setFilters` call; the auto-poll uses these on every iteration.
- `pause()` clears the timer and sets `polling = false`. The current
  in-flight `poll()` still resolves.
- `resume()` calls `EventService.autoPoll(this)` directly and starts a
  fresh loop.

#### Credentials refresh state machine

```
   getAccessToken()
        |
        v
   accessToken cached?  ---no--->  retrieveAccessToken()  --> stash {token, validUntil}
        |
        yes
        v
   autoRefresh()
        |
        v
   Date.now() + guardTime > validUntil ?  ---yes--->  retrieveAccessToken()
        |
        no
        v
   return cached
```

### Module reference

#### `lib/index.ts`

The single export surface. If a name is not in this file, treat it as
internal. The 19 public names:

```
defaultCredentialsFactory, Credentials, DevTokenCredentialsOptions,
DevTokenCredentials, autoCredentials, LoggingService, LsOptions,
LsQueryCfg, LsControlMessage, LoggingServiceClient, EventService,
EsOptions, EsFilterBuilderCfg, EsFilterCfg, DirectorySyncService,
DssOptions, DssQueryFilter, DssObjClass, EmitterInterface,
L2correlation, LogLevel, retrier, commonLogger, OAUTH2SCOPE, EntryPoint,
setLogLevel, setLogger, isSdkError, PanCloudError, Util,
CortexCredentialProvider, CredentialProviderOptions, CredentialsItem,
RefreshResult, defaultCredentialsProviderFactory, isCredentialItem,
CortexClientParams, CortexHelperOptions, CortexHubHelper,
HubIdpCallback, HubIdpStateData, isCortexClientParams,
fsCredentialsFactory
```

#### `lib/core.ts`

`CoreClass` is the foundation for every service client. Key fields:

| Field | Type | Visibility | Purpose |
|---|---|---|---|
| `cred` | `Credentials` | protected | Auth token source. |
| `validUntil` | `Number` | protected | Last seen token expiry; used to detect refresh. |
| `baseUrl` | `string` | protected | `cred.getEntryPoint() + basePath` (e.g. `.../logging-service/v1`). |
| `fetchHeaders` | object | protected | Cached `Authorization: Bearer ...` + `Content-Type: application/json`. |
| `lastResponse` | any | public | The most recent JSON response — useful for debugging. |
| `stats` | `CoreStats` | protected | `{apiTransactions: number}`. |

Methods of interest:

- `setFetchHeaders()` rebuilds the `Authorization` header from
  `cred.getAccessToken()`.
- `checkAutoRefresh()` calls `cred.autoRefresh()` and updates headers
  iff `validUntil` changed.
- `fetchXWrap` is the single internal call site for HTTP. Subclasses
  call `fetchGetWrap`, `fetchPostWrap`, `fetchPutWrap`, `fetchDeleteWrap`,
  or `voidXOperation` (when the response shape is not interesting).

#### `lib/common.ts`

Defines `LogLevel`, the `EntryPoint` literal type, and the `LogType`
literal type. The set of `LogType` values is exhaustively listed in the
private `LTYPES` map (lib/common.ts:110); `isKnownLogType()` is the
type-guard.

The `retrier` function is the canonical retry primitive. Every
service-class fetch is wrapped through it; the IDP refresh is wrapped
through it; the developer-token consumption is wrapped through it.

#### `lib/error.ts`

The error type hierarchy. `SdkErr` extends the built-in `Error` and adds
`getErrorCode`, `getErrorMessage`, `getSourceClass`. The two concrete
subclasses:

- `PanCloudError` — SDK-internal failures. Code is one of `PARSER`,
  `IDENTITY`, `CONFIG`, `UNKNOWN`.
- `ApplicationFrameworkError` — non-2xx HTTP responses. The
  `errorMessage` and `errorCode` are pulled from the response JSON.

`isSdkError(e)` is the duck-typing check the test scripts use to
decide between SDK errors and unrelated runtime errors.

#### `lib/exceptions.ts`

This file defines a separate, much simpler `PanCloudError` class with a
single `message` field. It is imported only by
`lib/directorysyncservice.ts` (line 1350: `import { PanCloudError } from
"./exceptions"`). The result is that DSS errors raise instances that do
not satisfy the `isSdkError` predicate. This is a known wart — see the
Improvements section.

### Code patterns

#### Type guards for every response

The SDK never trusts a response's shape. Every service handler calls a
hand-written type guard before exposing the result. Examples:

- `isJobResult(rJson)` (lib/loggingservice.ts:3982) — 20+ checks.
- `isEsFilter(obj)` (lib/eventservice.ts:2308) — recursive validation.
- `isDssDomain(obj)` (lib/directorysyncservice.ts:1387) — six fields.
- `isApexResponse(obj)` (lib/devtokencredentials.ts:1255) — two fields.

A response that fails a type guard raises `PanCloudError(PARSER, ...)`
with the original JSON included in the message. This is the SDK's
defence against API-side schema changes.

#### Factory methods, private constructors

Every service client has a private constructor and a static `factory`
method. This forces consumers through the documented entry point and
gives the SDK a place to apply future construction-time logic (e.g.
async initialization).

#### `CoreOptions` propagation

A pattern used consistently: subclasses extend `CoreOptions` and pass
the same options object to `super(...)`, then read their own
extensions. Example: `EsOptions extends EmitterOptions extends CoreOptions`.

#### The "Emitter" pattern

`Emitter` (lib/emitter.ts) uses Node's `EventEmitter` to dispatch on
three private topic strings: `EVENT_EVENT`, `PCAP_EVENT`, `CORR_EVENT`.
Subclasses (`LoggingService`, `EventService`) call `emitMessage(e)` once
per batch; `emitMessage` runs the L2 correlator (if enabled) and emits
to whichever topics have listeners.

Listeners are registered via `registerEventListener`,
`registerPcapListener`, `registerCorrListener`. Each returns `true` if
newly registered, `false` if the same function was already a listener
and `allowDup` is false.

### Testing strategy

The repository's `test/` directory contains 24 hand-written `.ts`
scenarios, each runnable via the corresponding `.js` entry point after
`npm run build:test`. They are not unit tests in the Jest/Mocha sense —
they are smoke tests that exercise a real Cortex Data Lake using
credentials from environment variables. Examples:

- `test/loggingservice_basic.ts` — synchronous query, decode results.
- `test/loggingservice_async_poll.ts` — auto-poll callback pattern.
- `test/eventservice_correlation.ts` — full L2/L3 correlation pipeline.
- `test/dss_query_users.ts` — DirectorySyncService user query with
  filter and pagination.

Each test prints to stdout. CI integration is left to the consumer.

> [!NOTE]
> A repository-level CI configuration (GitHub Actions, Travis,
> CircleCI) is not present at commit `e0d9e6b`.

### Improvements / known issues

| Item | Severity | Notes |
|---|---|---|
| Two `PanCloudError` classes (`error.ts` + `exceptions.ts`) | Low | DSS errors don't satisfy `isSdkError`. Migrate `directorysyncservice.ts:1350` import to `./error` and delete `exceptions.ts`. |
| Empty `dependencies` block | Low | Listed for explicitness so consumers know there is no transitive footprint. |
| `LoggingServiceClient` silently drops `cfg.callBack` | Low | At minimum, log a warning if a consumer passes one. |
| No published changelog or release notes | Low | Adds friction for security review. |
| Test scripts use a `pancloud-nodejs` import path that does not match `package.json:name` | Low | The tests import from `'pancloud-nodejs'` while `package.json:name` is `'pancloud'`. The repo expects the npm install path to be the GitHub repository name. |
| GitHub issue [#30 Community Health Assessment](https://github.com/PaloAltoNetworks/pancloud-nodejs/issues/30) | Tracked | Open community-health items; review before contributing. |

### Contributing

The repository is licensed Apache-2.0 (the per-file headers say so;
`package.json` says ISC — track issue #30 for discrepancies).

Contribution surface area:

1. Open a PR against `master` of `PaloAltoNetworks/pancloud-nodejs`.
2. The diff should keep `tsc -p tsconfig.json` clean (no new warnings).
3. Include a `test/<scenario>.ts` for any new behaviour, runnable
   against a real Cortex tenant.
4. Update `lib/index.ts` if your change adds a public symbol.
5. Update the per-file copyright header to include the current year.

> [!TIP]
> When you add a new HTTP-calling method, route it through
> `CoreClass.fetchXWrap` (or one of its `fetchGetWrap`/`fetchPostWrap`
> wrappers). Calling `fetch()` directly bypasses both the auto-refresh
> and the retry logic.

## 9. Integration Points

### Express / Fastify integration (Cortex Hub callback)

`CortexHubHelper.authCallbackHandler` is shaped like an Express
middleware:

```typescript
app.get('/oauth2/callback', async (req, res, next) => {
    await hubHelper.authCallbackHandler(req as any, res, next)
    if (req.callbackIdp?.error) {
        res.status(400).send(req.callbackIdp.error)
        return
    }
    res.json({ datalakeId: req.callbackIdp?.datalakeId })
})
```

The handler reads `req.query.code` and `req.query.state`, restores the
authorization state, exchanges the code for tokens, and stores the
result. It populates `req.callbackIdp` with `{error?, message?,
datalakeId?}`.

> [!NOTE]
> The handler calls `next()` itself. Do not call `next()` again from
> outside or you will trigger the next middleware twice. Pass
> `() => {}` as the `next` argument if you have already taken
> responsibility for the response.

### Node `Stream` ecosystem

`LoggingServiceClient` is a `Readable` in object mode. It composes with
any object-mode transform or sink:

```typescript
import { Transform } from 'stream'

const filter = new Transform({
    objectMode: true,
    transform(chunk: any[], _, cb) {
        cb(null, chunk.filter(r => r.severity === 'critical'))
    }
})

stream.pipe(filter).on('data', recs => console.log(recs.length))
```

### Logging frameworks

The `commonLogger` interface is intentionally minimal so any logger
fits. Worked examples for the three most common frameworks:

#### winston

```typescript
import winston from 'winston'
import { setLogger, LogLevel } from 'pancloud-nodejs'
const logger = winston.createLogger({ /* ... */ })
setLogger({
    level: LogLevel.INFO,
    error: e => logger.error(e.getErrorMessage(), { code: e.getErrorCode(), src: e.getSourceClass() }),
    alert: (s, m, n) => logger.warn(m, { src: s.className, name: n }),
    info:  (s, m, n) => logger.info(m, { src: s.className, name: n }),
    debug: (s, m, n, p) => logger.debug(m, { src: s.className, name: n, payload: p })
})
```

#### pino

See Section 5 (Operations) for the full pino example.

#### bunyan

Adapt the same shape — pass `{src: s.className}` as the second arg, the
message as the first.

### Metrics frameworks

Periodically harvest the per-instance stats:

```typescript
import { Counter } from 'prom-client'

const apiTransactions = new Counter({
    name: 'pancloud_api_transactions_total',
    help: 'Total API transactions performed by the SDK',
    labelNames: ['service']
})

setInterval(() => {
    apiTransactions.labels('logging').inc(ls.getLsStats().apiTransactions)
    apiTransactions.labels('event').inc(es.getEsStats().apiTransactions)
    apiTransactions.labels('directory').inc(dss.getDssStats().apiTransactions)
}, 30_000)
```

> [!WARNING]
> The stats counters are running totals on the SDK side, not deltas.
> If you `inc()` directly with the value, you double-count. Either
> compute deltas yourself or use a Prometheus `Gauge` and call `set()`.

### Custom credentials sources

To integrate with a non-PAN identity provider that fronts Cortex (for
example, an internal STS that issues short-lived Cortex access tokens),
subclass `Credentials`:

```typescript
import { Credentials, EntryPoint, expTokenExtractor } from 'pancloud-nodejs'

class MySTSCredentials extends Credentials {
    constructor(entryPoint: EntryPoint, private sts: MySTSClient) {
        super(entryPoint)
        this.className = 'MySTSCredentials'
    }
    async retrieveAccessToken(): Promise<void> {
        const token = await this.sts.exchangeForCortexToken()
        this.setAccessToken(token, expTokenExtractor(this, token))
    }
}
```

Pass an instance to any service factory.

### KMS-backed credential storage

To store refresh tokens in a KMS-encrypted backing store, subclass
`CortexCredentialProvider` and implement the abstract methods:

```typescript
class KmsCredProvider extends CortexCredentialProvider<MyMeta, 'tenantId'> {
    async createCredentialsItem(dlid, item, meta) {
        const ciphertext = await kms.encrypt(JSON.stringify(item.refreshToken))
        await db.put({ dlid, ciphertext, ...meta })
    }
    async loadCredentialsDb() { /* fetch from db, decrypt */ }
    async updateCredentialsItem(dlid, item) { /* re-encrypt and store */ }
    async deleteCredentialsItem(dlid) { /* delete from db */ }
    async selectDatalakeByTenant(tenantId) { /* query by tenant index */ }
    protected credentialsObjectFactory(dlid, ep, gt, prefetch) {
        return this.defaultCredentialsObjectFactory(dlid, ep, gt, prefetch)
    }
}
```

This is the recommended path for production multi-tenant deployments;
the shipped `FsCredProvider` is a development convenience.

### Webhook-style integrations

For "push log records into the Logging Service" pipelines, use
`LoggingService.write(vendorName, logType, data[])`. The vendor name
and log type must be pre-registered with PAN; the data array must
match the registered schema:

```typescript
const result = await ls.write('AcmeCorp', 'panw.system', [
    { uuid: '...', timestamp: '...', source: '...', severity: 'info', message: '...' }
])
console.log(`accepted ${result.uuids.length} records`)
```

The response carries the assigned UUIDs; persist them for end-to-end
trace correlation.

### Migrating from raw HTTPS calls

Code that already speaks to the Cortex API directly via `fetch()` or
`axios` can migrate incrementally:

1. Replace your token-refresh logic with `autoCredentials()` or
   `defaultCredentialsProviderFactory()`. Pass the resulting
   `Credentials` to a single `LoggingService` instance.
2. Replace your manual `https.request` to `/queries` with
   `ls.query(cfg)`.
3. For long-running streams, replace your hand-rolled poll loop with
   `LoggingServiceClient` (which gives you backpressure for free).
4. Keep your monitoring in place — the SDK's stats getters expose the
   same counters you were probably already tracking.

> [!NOTE]
> The SDK does not own retries or backoff above the request-level
> retrier. Long-term failures (e.g. PAN region down) bubble up as
> exceptions; your application is responsible for circuit-breaker
> behaviour, dead-letter queues, and operator-facing alerts.

---

_Generated April 2026 from PaloAltoNetworks/pancloud-nodejs at e0d9e6b. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial, AI-generated guide for a Palo Alto Networks open-source repository; not reviewed by Palo Alto Networks._
