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_PROXYand an agent), becauselib/fetch.tscallshttps.requestdirectly 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.tsdefines a separate, unrelatedPanCloudErrorclass that is imported bylib/directorysyncservice.ts(line 1350) — a minor inconsistency with the main error inlib/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.jsonhas emptydependenciesand emptydevDependenciesblocks. Every external API the SDK calls is satisfied by the Node standard library; thetsctoolchain 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.comhosts plusapp.apiexplorer.rocks(only if you useDevTokenCredentials). The SDK does not honorHTTP_PROXY/HTTPS_PROXYenv vars at the library level; if your environment requires a proxy, configure it viahttps.globalAgentor by overriding thefetch()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.callBackis 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
Credentialsinstance per data lake. - Reuse the
CredentialsacrossLoggingService,EventService,DirectorySyncServiceinstances; refreshes are deduped viavalidUntil. - Use
LoggingServiceClientrather 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 longtimeWindow— 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:
- Sharded channel IDs: pass
channelId: 'shard-A',shard-B, ... to each instance and configure the upstream filter list per shard. - 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
Tparameter) 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
forceCallbackTenantValidationoption inCortexHelperOptionsdefaults tofalse. In a multi-tenant deployment, set it totrueand ensure your auth middleware populatesreq.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 > 0over 5 minutes → consumer is rejecting events; investigate.queries > 0butrecords = 0over an hour → upstream data flow has stopped; check the firewall side.apiTransactionsgrows butpollsstays flat → process is paused or blocked.
Tip
The L2 correlator emits its own stats inside
EmitterStats.correlationStats(agedOut,dbWaterMark,dbInserts,discardedEvents). A growingdbWaterMarkpaired withagedOut > 0suggests 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
payloadfield 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
LoggingServicequery against a small known time window. If you do, setmaxWaitTime: 5000so 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:
- Confirm with
setLogLevel(LogLevel.DEBUG)that the failure is IDP-side (source: "CortexCredentialProvider"). - Re-issue a refresh token from the Cortex hub for the affected
client_id. - Update the running env var (or the
PANCLOUD_CONFIG.jsonfor FS-based deployments). - 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:
await es.flush()to drop everything currently buffered (data loss is acceptable if you can re-query).- Or, increase
batchSizeon each filter entry to receive more events per poll round-trip. - Or, shard: split the filter list across multiple
EventServiceinstances on differentchannelIdvalues.
Runbook: stuck LoggingServiceClient
Symptoms: Stream never ends, no data events, no error.
Recovery:
- Check
setLogLevel(LogLevel.DEBUG)output for repeatedRUNNINGresponses. - If the query has been running > 60 s and the SDK has hit its
retriesceiling, the stream emits anerror; if not, raiseretriesanddelayin the constructor. - Manually
stream.destroy()to abort and triggerdeleteQuery.
6. Performance
HTTP-level performance
Every service client routes calls through CoreClass.fetchXWrap, which:
- Increments
apiTransactions. - Calls
checkAutoRefresh()(potentially adding one IDP round-trip if the token is stale). - Calls
retrier(this, retrierCount, retrierDelay, fetch, url, init). - 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: 1andfetchTimeoutto 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 parentLoggingService'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.debugwill print the request body (which contains the refresh token) when it sends a refresh request. SetLogLevel.INFOor 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
CortexCredentialProvidersubclass that stores credentials there: overrideloadCredentialsDb,createCredentialsItem,updateCredentialsItemanddeleteCredentialsItemto 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.checkServerIdentityto 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
tenantKeyso thatselectDatalakeByTenant()can filter. - Validate the tenant ID before calling
getCredentialsObject().CortexHubHelper.getCredentialsObjectdoes 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: trueto require the requester's tenant ID to match the state-stored tenant ID during OAuth2 callback.
Warning
A subclass that returns a
Credentialsobject without verifying the requesting tenant owns it has cross-tenant data leakage. TreatgetCredentialsObject(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_tokenJWTs. - Raw
refresh_tokenstrings. - 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 thecommonLogger.debugmethod 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 iffpendingQueries.length > 0. cancelPoll(qid, err?)removes the entry, resolves or rejects the pending promise, and callsdeleteQueryto 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 fromfilterOptions.poolOptionson everysetFilterscall; the auto-poll uses these on every iteration.pause()clears the timer and setspolling = false. The current in-flightpoll()still resolves.resume()callsEventService.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 theAuthorizationheader fromcred.getAccessToken().checkAutoRefresh()callscred.autoRefresh()and updates headers iffvalidUntilchanged.fetchXWrapis the single internal call site for HTTP. Subclasses callfetchGetWrap,fetchPostWrap,fetchPutWrap,fetchDeleteWrap, orvoidXOperation(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 ofPARSER,IDENTITY,CONFIG,UNKNOWN.ApplicationFrameworkError— non-2xx HTTP responses. TheerrorMessageanderrorCodeare 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:
- Open a PR against
masterofPaloAltoNetworks/pancloud-nodejs. - The diff should keep
tsc -p tsconfig.jsonclean (no new warnings). - Include a
test/<scenario>.tsfor any new behaviour, runnable against a real Cortex tenant. - Update
lib/index.tsif your change adds a public symbol. - 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 itsfetchGetWrap/fetchPostWrapwrappers). Callingfetch()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 callnext()again from outside or you will trigger the next middleware twice. Pass() => {}as thenextargument 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 PrometheusGaugeand callset().
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:
- Replace your token-refresh logic with
autoCredentials()ordefaultCredentialsProviderFactory(). Pass the resultingCredentialsto a singleLoggingServiceinstance. - Replace your manual
https.requestto/querieswithls.query(cfg). - For long-running streams, replace your hand-rolled poll loop with
LoggingServiceClient(which gives you backpressure for free). - 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.