Skip to content

feat: add authorizationToken config option - #136

Merged
andre-j3sus merged 6 commits into
mainfrom
ajesus/authorization-token-support
Jul 31, 2026
Merged

feat: add authorizationToken config option#136
andre-j3sus merged 6 commits into
mainfrom
ajesus/authorization-token-support

Conversation

@andre-j3sus

@andre-j3sus andre-j3sus commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Adds an optional authorizationToken that attributes a test to a registered customer. It's opaque to the engine — consumers get it from their own backend:

const engine = new SpeedTest({ authorizationToken: 'token-from-your-backend' });

Defaults to null, so existing behaviour is unchanged.

Sent as a jwt query-string param on the download, upload, TURN-credential and results-logging requests (listed in AUTHORIZABLE_URLS). Never sent over plain HTTP. Reachability/RPKI probes are excluded — different hosts.

Query string rather than an Authorization header: a header adds a CORS preflight, and the warmed connection stops BandwidthEngine's server-time calibration from seeing a fresh TCP handshake.

Named jwt rather than token because the measurement log POST body already has a token field carrying a server-issued per-measurement value, and both go to logMeasurementApiUrl. Requires the matching server-side param name.

The token is masked before it reaches any log or callback. The failing URL is passed to the consumer's onError, and consumers routinely forward those payloads to third-party log sinks.

Assumes the download/upload endpoints don't reject on the token — an expired one should fall back to unattributed. A 4xx wouldn't halt a test, but it degrades badly: BandwidthEngine treats every non-OK response as a transient fault and retries 20 times with no backoff, so each affected phase burns 21 requests and reports a misleading "Connection failed" before moving on. Older versions that never send a token would be affected the same way. Worth fixing separately.

The red build job is a pre-existing e2e failure on main (567aead), unrelated to this change.

Adds an optional `authorizationToken` that attributes a test to a
registered customer, so usage can be accounted for rather than being
anonymous.

The token is opaque to the engine: consumers obtain it from their own
backend and pass it in. It is attached as a `token` query-string
parameter to the endpoints that bill against a customer — the download,
upload and load-generator requests, the TURN credentials request, and
the results-logging requests. The reachability, RPKI and NXDOMAIN probes
are excluded, since they target unrelated hosts.

Sent in the query string rather than a header to avoid a CORS preflight
on the measurement endpoints. Beyond the extra round trip, a preflight
reuses the TCP connection for the real request, which would suppress the
server-time calibration in BandwidthEngine that depends on observing a
fresh handshake.

The token is never attached over plain HTTP, so that a token cannot be
exposed in cleartext.

Defaults to null, leaving existing behaviour unchanged.
@andre-j3sus andre-j3sus self-assigned this Jul 29, 2026
Comment thread src/index.ts Outdated
Comment thread src/utils/authorization.ts Outdated
@ask-bonk

ask-bonk Bot commented Jul 29, 2026

Copy link
Copy Markdown

Both inline comments posted. Now the summary.

Review summary

This PR adds an optional authorizationToken config option that attaches an opaque token query-string parameter to the billed measurement endpoints (download, upload, load-generator, TURN creds, and both logging URLs), defaulting to null so existing behavior is unchanged.

What it changes

  • src/utils/authorization.ts (new) — withAuthorizationToken appends the token to a single URL (HTTPS-only, preserves existing params); applyAuthorizationToken bakes it into every billed URL of a merged config.
  • src/config/defaultConfig.ts — adds the authorizationToken: string | null field (default null) with docs.
  • src/index.ts — both MeasurementEngine and SpeedTestEngine constructors wrap their merged config in applyAuthorizationToken.
  • Docs (README.md, AGENTS.md) and 3 test files (+19 tests).

Assessment

The core design is sound. I verified the claim that sub-engines inherit the token: BandwidthEngine (BandwidthEngine.ts:373) and LoadNetworkEngine (index.ts:123) both build new URL(apiUrl, ...) and .set() their own params on top, so the baked-in token survives. The HTTPS-only guard and query-string-over-header rationale are well justified.

Findings (both posted inline, non-blocking)

  1. src/index.ts:749SpeedTestEngine redundantly re-merges and re-tokenizes the whole config just to read logAimApiUrl/sessionId, duplicating work already done in super(). A maintenance/drift hazard, not a correctness bug.
  2. src/utils/authorization.ts:23new URL(apiUrl, window.location.origin) throws a ReferenceError in any non-DOM context when a token is set. Fine for the documented browser-only usage (the null default path is safe), but a typeof window guard or doc note would make it fail gracefully.

Correctness/security notes (no change requested)

  • The !token guard also treats an empty-string token as "no token" (tested at authorization.test.ts:83) — reasonable.
  • The PR body itself flags the important operational assumption: the billed endpoints must not 4xx on an unrecognized/expired token, or BandwidthEngine's retry-then-fail path would break the test. That's a server-side contract requirement worth tracking as follow-up, but it's outside this diff.

No blocking issues. The two inline comments are quality/robustness suggestions.

github run

Absolute URLs are now parsed without a base, so only genuinely relative
ones consult window.location.origin. Every default API URL is absolute,
so constructing an engine with a token no longer needs a DOM.

Also trims the comments added in the previous commit.
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: authorizationToken config option

Adds an optional, opaque authorizationToken that is baked into every billed API URL (downloadApiUrl, uploadApiUrl, turnServerCredsApiUrl, logAimApiUrl, logMeasurementApiUrl) as a token query-string param, defaulting to null so existing behavior is unchanged.

Verification

I traced the token through to the actual network calls and confirm the sub-engines inherit it:

  • Download/upload + load-generator: BandwidthEngine.ts:373 and LoadNetworkEngine/index.ts:123 both do new URL(apiUrl, ...) then searchParams.set(...) their own params on top, so the baked-in token survives.
  • Per-measurement log: LoggingBandwidthEngine.ts:113 POSTs to the tokenized logMeasurementApiUrl. (Note the unrelated token field in the log body at line 104 is the server-issued measurement token, not the auth token — no collision since they use different transports.)
  • AIM results log: logFinalResults.ts:134 fetches the tokenized apiUrl directly.
  • TURN creds: PacketLossEngine.ts:77 fetches the tokenized URL directly.

The HTTPS-only guard, the query-string-over-header rationale, and the fresh-object mutation invariant (tested at authorizationToken.test.ts:75) are all sound and well justified. Ran the added unit tests (25 passing), tsc --noEmit (clean). The design is solid.

Findings

One non-blocking maintainability observation posted inline. The two earlier inline comments (redundant merge at index.ts:749, window dependency) were already addressed — the window crash is fixed in ce4c00b (absolute URLs parse without a DOM, covered by a test), and the redundant merge is a pre-existing pattern the author reasonably declined to widen this PR to fix.

Operational contract (out of diff, worth tracking)

The PR body correctly flags it: the billed endpoints must not return 4xx on an unrecognized/expired token. BandwidthEngine throws on any non-OK response and retries before surfacing Connection failed, so a rejecting endpoint would break the whole test — including for older library versions that never send a token. This is a server-side requirement, not fixable here, but should be tracked as follow-up.

No blocking issues.

Comment thread src/utils/authorization.ts Outdated
@ask-bonk

ask-bonk Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review posted.

Summary

I reviewed PR #136 (feat: add authorizationToken config option) and posted a review with one inline comment.

What it changes: Adds an optional, opaque authorizationToken config option (default null) that gets baked into every billed API URL as a token query-string parameter — download, upload, load-generator, TURN creds, and both logging endpoints. Reachability/RPKI/NXDOMAIN probes are deliberately excluded.

Verification I did (not just claims):

  • Traced the token to the actual fetch calls in all four sub-engine paths and confirmed it survives — BandwidthEngine.ts:373 and LoadNetworkEngine/index.ts:123 rebuild the URL and .set() their own params on top of the baked-in token; the AIM, per-measurement, and TURN paths fetch the tokenized URL directly.
  • Confirmed no collision between the auth token query param and the unrelated token field in the log body (LoggingBandwidthEngine.ts:104).
  • Ran the 25 added unit tests (all pass) and tsc --noEmit (clean).

Findings:

  • Non-blocking (new, inline): the billed-URL set is enumerated three times (AuthorizableUrls interface, the function body, and docs). A future new billed endpoint would silently ship without the token — no compile/test failure catches it since the generic only requires presence, not exhaustiveness.
  • The two earlier inline comments (window dependency, redundant merge at index.ts:749) were already addressed — the window crash is genuinely fixed in ce4c00b and covered by a test; the redundant merge is a pre-existing pattern the author reasonably declined to widen scope for.
  • Operational contract (out of diff): the PR body correctly flags that billed endpoints must not 4xx on a bad token, or BandwidthEngine's retry-then-fail path breaks the whole test. Server-side follow-up, not fixable here.

No blocking issues; the design is sound.

github run

The URLs carrying the token were enumerated in three places, so a new
measurement endpoint could silently ship unattributed. TOKEN_URL_KEYS is
now the only list, iterated by the helper and asserted exhaustively by
the tests, which import it rather than keeping their own copy.
Address review feedback on the authorizationToken option:

- Rename the query param from `token` to `auth`. The measurement log POST
  body already carries a `token` field holding a server-issued per-measurement
  value, and both are sent to logMeasurementApiUrl.
- Mask the token in the fetch error paths. The failing URL reaches the
  consumer's onError callback, which is commonly forwarded to third-party
  log sinks. Also catch the log POST so a rejection cannot print the URL.
- Rename TOKEN_URL_KEYS to AUTHORIZABLE_URLS and TokenizableUrls to
  AuthorizableUrls to convey eligibility rather than parsing.
- Expose the merged config to subclasses via a protected getter, so
  SpeedTestEngine no longer rebuilds it just to read two fields.
@andre-j3sus
andre-j3sus marked this pull request as ready for review July 30, 2026 15:10
carlosefr
carlosefr previously approved these changes Jul 30, 2026

@carlosefr carlosefr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Agreed with the API side. Still avoids colliding with the `token` field in
the measurement log POST body.
@andre-j3sus

Copy link
Copy Markdown
Collaborator Author

/bonk

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: authorizationToken config option

Optional, opaque authorizationToken (default null) baked into every billed API URL as a jwt query-string param — download, upload, TURN creds, and both logging endpoints. Reachability/RPKI/NXDOMAIN probes and turnServerUri are excluded.

Verification I ran (not just re-reading prior claims)

  • Tests: installed deps, pnpm test:unit → 121/121 pass. pnpm lint → clean.
  • Token survives to the wire: BandwidthEngine.ts:374 and LoadNetworkEngine/index.ts rebuild the URL and .set() their own params on top of the baked-in jwt, so it isn't dropped. AIM (logFinalResults.ts:134) and TURN (PacketLossEngine.ts:77) fetch the tokenized URL directly.
  • No param collision: the jwt query param is distinct from the server-issued token field in the log POST body (LoggingBandwidthEngine.ts:104) — which is exactly why the rename to jwt in 5853f9f was necessary. Good call.
  • Redaction: BandwidthEngine's console.warn/onConnectionError now redact via redactAuthorizationToken (BandwidthEngine.ts:557,566); LoggingBandwidthEngine swallows the best-effort log rejection so the tokenized URL can't surface in an unhandled rejection. logFinalResults and PacketLossEngine credential-failure paths pass the error, not the URL, so no leak there.
  • Prior review items are genuinely resolved in-branch: the window-at-merge crash is fixed (ce4c00b, absolute URLs parse without a DOM, covered by a test asserting typeof window === 'undefined'); the earlier "redundant second merge" in SpeedTestEngine is now gone — it reads this.config via a new protected get config() getter (index.ts:143) instead of re-merging.

Findings

One real issue: the user-facing docs still say the param is named token, but the code sends jwt (authorization.ts:8). A consumer coordinating the server-side param name from the README/JSDoc would wire up the wrong name. Two inline comments below (defaultConfig.ts and README.md). Non-blocking but should be fixed before merge since the whole point of the jwt rename is the server-side contract.

Operational contract (out of diff, worth tracking)

As the PR body flags: billed endpoints must not 4xx on an unrecognized/expired token, or BandwidthEngine's retry-then-fail path (MAX_RETRIES = 20, no backoff) breaks the whole phase and reports a misleading "Connection failed". Server-side follow-up.

Design is sound; only the stale doc naming is worth a change here.

Comment thread src/config/defaultConfig.ts Outdated
Comment thread README.md Outdated
@ask-bonk

ask-bonk Bot commented Jul 31, 2026

Copy link
Copy Markdown

Reviewed PR #136 and posted a review with two inline comments.

What it changes

Adds an optional, opaque authorizationToken config option (default null, so existing behavior is unchanged). When set, it's baked into every billed API URL as a jwt query-string param (download, upload, TURN creds, and both logging endpoints) via a new src/utils/authorization.ts. Reachability/RPKI/NXDOMAIN probes and turnServerUri are excluded. The token is redacted from BandwidthEngine error/console paths, and the best-effort measurement-log fetch is now .catch()-swallowed so it can't leak via an unhandled rejection. +383/−27 across 10 files, including 3 test files (+21 tests).

Verification I ran

  • pnpm test:unit → 121/121 pass; pnpm lint → clean.
  • Traced the token to the actual fetch calls in all sub-engines — it survives the URL rebuilds in BandwidthEngine/LoadNetworkEngine and is fetched directly on the AIM/TURN paths.
  • Confirmed the jwt query param does not collide with the server-issued token field in the log POST body (LoggingBandwidthEngine.ts:104) — the rationale for the 5853f9f rename.
  • Confirmed the two earlier review items (the window-at-merge crash and the redundant second config merge) are genuinely fixed in-branch.

Finding (non-blocking, but fix before merge)

The code sends the param as jwt (authorization.ts:8), but the user-facing docs still say token:

  • src/config/defaultConfig.ts:72 (JSDoc)
  • README.md:51

Since the PR explicitly requires the server-side param name to match, a consumer following these docs would wire up the wrong name. Both flagged inline.

No correctness/security bugs in the diff. The 4xx-on-bad-token operational risk the PR body flags is a genuine server-side contract requirement but outside this change.

github run

The JSDoc and README still documented the param as `token`, which a consumer
would wire up server-side. Both predate the rename to `jwt` in 5853f9f.
@andre-j3sus
andre-j3sus merged commit f3474b1 into main Jul 31, 2026
8 checks passed
@andre-j3sus
andre-j3sus deleted the ajesus/authorization-token-support branch July 31, 2026 09:44
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.

2 participants