Skip to content

fix(sdk): make the Node SDK usable against platform credentials - #67

Merged
matthew-demidoff merged 7 commits into
masterfrom
fix/sdk-production-readiness
Aug 27, 2026
Merged

fix(sdk): make the Node SDK usable against platform credentials#67
matthew-demidoff merged 7 commits into
masterfrom
fix/sdk-production-readiness

Conversation

@matthew-demidoff

Copy link
Copy Markdown
Collaborator

An external SDK review found the default OAuth flow incompatible with credentials the platform actually issues: self-service apps are registered with token_endpoint_auth_method = client_secret_post, but the SDK always sent Basic auth, so the README's exchangeCode/refreshToken/introspect examples failed with invalid_client. This closes that and the rest of the review's actionable findings.

Node SDK (0.3.0, still unpublished):

  • Client auth defaults to client_secret_post; client_secret_basic and none selectable via tokenEndpointAuthMethod, validated at construction.
  • BottleneckAuthError.code now carries the activation envelope's stable code (rate_limited, invalid_credentials, ...) instead of the human message; OAuth error/error_description parsing unchanged; retryAfterSeconds parsed from Retry-After on 429s.
  • verifyWebhookSignature enforces a five-minute freshness window (toleranceSeconds to tune) as replay protection; malformed and far-future timestamps rejected.
  • API surface catches up with docs/external-apps.md: revokeToken, clientCredentialsGrant, revokeActivation, getAppConfig, listActivationRequests, listAuthorizations, idempotencyKey on create, revoked/deniedReason on the status type. Device flow, PAR, DCR, private_key_jwt, logout, and discovery are documented as intentionally not wrapped.
  • Request lifecycle: injectable fetch, client and per-request timeouts, per-request AbortSignal. Transport failures wrap into BottleneckAuthError (network_error/timeout) as the README always claimed; caller aborts rethrow the caller's reason. No automatic retries.

Tests and packaging:

  • New suite runs node:test against the built dist in both module formats; CI matrixes the sdk job over Node 18.17/20/22 and installs the packed tarball into ESM and CJS fixture consumers (verify-pack), so the export map and prepack build are exercised.
  • Build moved to prepack, CJS declarations wired into the require condition, tarball now ships a license. Note: package.json claimed MIT while the repo is Apache-2.0; since nothing was ever published under MIT I aligned it to Apache-2.0 - flag if MIT was deliberate.
  • sdk-release.yml publishes on sdk-node-v* tags via npm trusted publishing with provenance. Publishing itself is left to you: the org/package needs the trusted publisher configured on npmjs.com first, and the README now states plainly that the package is not yet on npm.

Docs and example clients:

  • README's callback example now stores state + PKCE verifier in the session and validates state before exchanging the code (the old example was a copy-pasteable login CSRF).
  • Go/Python clients labeled as unsupported examples, with their two real defects fixed so copying them works: client_secret_post instead of Basic, and webhook timestamp freshness.
  • A key-set tripwire in the activation integration suite pins the status payload to the SDK type so they cannot drift silently again.

Review attention: the webhook freshness default is a behavior change for any existing verifier caller (none known beyond our own tests, which now use fresh timestamps), and the 0.2.0 -> 0.3.0 breaking changes are catalogued in sdk/node/CHANGELOG.md.

The default OAuth client auth now sends client_secret_post, matching how
self-service apps are registered (the old always-Basic behavior was
rejected with invalid_client); client_secret_basic and none are
selectable via tokenEndpointAuthMethod. Error parsing prefers the
activation envelope's stable code over the human message while keeping
OAuth error/error_description handling, and 429s expose
retryAfterSeconds. verifyWebhookSignature enforces a five-minute
freshness window (configurable) as replay protection.

Also brings the SDK up to the documented API surface: revokeToken,
clientCredentialsGrant, revokeActivation, getAppConfig,
listActivationRequests, listAuthorizations, idempotencyKey on create,
revoked/deniedReason on the status response, and request lifecycle
controls (injectable fetch, timeouts, per-request AbortSignal, transport
failures wrapped as BottleneckAuthError).

Tests run against the built artifact in both module formats; the
integration suite gains a key-set tripwire so the status payload and the
SDK type cannot drift silently again.
…kflow

CI now runs the SDK suite on Node 18.17/20/22 (the claimed engines
range) and installs the packed tarball into throwaway ESM and CJS
consumers, so the export map and file list are exercised, not just the
source tree. The build moves to prepack so git installs and plain npm
pack produce a working package, the CJS declaration file is wired into
the require condition, and the tarball now ships the repository's
Apache-2.0 license (package.json previously claimed MIT; never
published under it).

Publishing is a tag-triggered workflow (sdk-node-v*) using npm trusted
publishing with provenance; the npm side still needs the trusted
publisher configured before the first release.
… labeling

The README's callback example now stores state and the PKCE verifier in
the session and validates state before exchanging the code; the old
example invited a copy-pasted login CSRF. The install section says
plainly that the package is not on npm yet, and the endpoints the SDK
deliberately does not wrap are listed.

The Go and Python clients are labeled as unsupported examples rather
than SDKs, and their two real defects are fixed so copying them works
against real credentials: introspection sends client_secret_post
instead of Basic auth, and webhook verification checks timestamp
freshness.
… bad input

- timeoutMs and the caller AbortSignal now apply to reading the response
  body, not just the headers; a stalled or dropped body times out or
  aborts instead of hanging
- 2xx bodies that fail to parse throw BottleneckAuthError with code
  invalid_response; mid-body transport failures wrap as network_error
- verifyWebhookSignature returns false on a missing or non-string
  signature instead of throwing (Node and Python; Go already handled it)
- createActivationRequest rejects idempotency keys outside the 8-255
  range the server otherwise ignores silently
- a configured clientSecret requires clientId at construction
- BottleneckAuthError instanceof holds across the parallel ESM and CJS
  builds via Symbol.hasInstance
- injected fetch is never invoked as a method of the transport object
- Go example introspect sends client_id alone for public clients
- verify-pack resolves the SDK dir with fileURLToPath so checkout paths
  with spaces work
- README: import randomUUID from node:crypto (global crypto is Node
  19+), document the list-endpoint caps and invalid_response
The route used the generic unauthorized() helper, whose envelope has no
code field, so the SDK surfaced the human sentence as
BottleneckAuthError.code on the one endpoint that broke the documented
branch-on-code contract. Emit the OAuth envelope instead:
invalid_request for a missing bearer token, invalid_token for an
invalid one.
The SDK rejects webhook timestamps older than 300s while retry delays
run to 24h, so deliveries stay verifiable only because deliverOne
stamps and signs at send time on every attempt. Nothing asserted that
invariant, and freezing the timestamp across attempts would pass every
existing test while breaking all late retries at SDK consumers.
Both list endpoints return at most the most recent rows (50 requests,
200 authorizations) with no pagination, and the docs suggested using
them to recover ids and reconcile grants without mentioning it.
@matthew-demidoff
matthew-demidoff merged commit 7087785 into master Aug 27, 2026
4 checks passed
@matthew-demidoff
matthew-demidoff deleted the fix/sdk-production-readiness branch August 27, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant