pancloud-nodejs Administrator & Developer Guide

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.

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

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

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:

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:

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:

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:

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:

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

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

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:

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

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:

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:

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:

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:

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.