Skip to content

feat(auth): optional WAVE_INSTALL_CHANNEL -> X-Wave-Install-Channel header (E2 usage-attribution) - #93

Open
yakimoto wants to merge 1 commit into
mainfrom
feat/install-channel-header
Open

feat(auth): optional WAVE_INSTALL_CHANNEL -> X-Wave-Install-Channel header (E2 usage-attribution)#93
yakimoto wants to merge 1 commit into
mainfrom
feat/install-channel-header

Conversation

@yakimoto

@yakimoto yakimoto commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Client-side half of E2 usage-attribution — the measurement mechanism the economist
synthesis named to resolve the WATCH on funding a public WAVE Skills showcase repo
(not built here). Companion PR on wave-av/wave-gateway:
feat/usage-attribution-e2 (server-side allowlist + ledger threading). Full
grounding + honest limitations: E2-USAGE-ATTRIBUTION.md (companion PR on
wave-av/claude-workstation, governance/plans/wave-skills-distribution/).

getAuthHeaders() now adds X-Wave-Install-Channel when WAVE_INSTALL_CHANNEL is
set in the environment. This lets a Skill/manifest-generated onboarding config
self-declare its install channel (e.g. skill-manifest) apart from a hand-written
docs install (docs-manual) — wave-gateway allowlists the value and folds it into a
non-billing usage-ledger attribution dimension for quarterly reporting.

Unset by default — byte-identical to today for anyone who doesn't set it.

$ WAVE_API_KEY=... WAVE_INSTALL_CHANNEL=skill-manifest node -e "..."
{ Authorization: '...', 'Content-Type': '...', 'User-Agent': '...', 'X-Wave-Install-Channel': 'skill-manifest' }

$ WAVE_API_KEY=... node -e "..."   # unset
{ Authorization: '...', 'Content-Type': '...', 'User-Agent': '...' }   # header absent, everything else unchanged

Why this exists (grounding, not a guess)

No MCP client (Claude Code, Cursor, or otherwise) surfaces a genuine install-time
referrer — confirmed via WebSearch against current MCP spec docs. This package's own
getAuthHeaders() currently sends a static User-Agent: wave-mcp-server/0.1.0
identical regardless of how the customer found WAVE, so today there is literally no
way to distinguish "discovered via the manifest" from "hand-installed after reading
docs" at the request layer. WAVE_INSTALL_CHANNEL is a new, WAVE-controlled,
self-declared tag (like a UTM parameter) — not a network-verified signal, and the
phase file says so plainly.

Scope note

The other half of this tag — WAVE's manifest-generated onboarding snippet actually
setting WAVE_INSTALL_CHANNEL=skill-manifest in the config it hands customers —
belongs in wave-docs-www#67 (already shipped this session per
docs/wave-skills-distribution-e1-onboarding). Not touched here: that's a fourth repo
not inspected in this pass; flagged as a fast-follow in the phase file instead of
built blind.

Branching note

Branched from origin/main, not PR #92 (fix/gateway-base-url-91), to avoid taking a
dependency on unreviewed, unmerged code touching the same file (src/auth.ts). A small
rebase conflict when #92 lands is preferable to stacking on an unreviewed PR.

Tests

This repo has no test runner on main (confirmed — no test script, no test
files). Verified via:

  • npm run type-check — clean
  • npm run lint — clean (0 warnings)
  • Manual tsx execution of getAuthHeaders() with and without WAVE_INSTALL_CHANNEL
    set (output above) — proves the additive, byte-identical-when-unset behavior.

Also updated README.md's environment-variable table and CHANGELOG.md.

Deploy

No deploy — this is an npm package change; publishing a new version to npm is a
separate, explicit step not taken here. GitHub Actions is in a platform-wide outage
right now (per this task's brief) — not waiting on CI for that reason.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com


Note

Low Risk
Additive optional header on auth utilities with no change when unset; no auth or billing logic modified in this repo.

Overview
Adds optional install-channel attribution on outbound WAVE API calls: when WAVE_INSTALL_CHANNEL is set, getAuthHeaders() includes X-Wave-Install-Channel on every request (via waveFetchWithRateLimit and direct getAuthHeaders() usage). Unset env leaves headers byte-identical to current behavior.

README documents the new variable; CHANGELOG records it under Unreleased → Added. Server-side allowlisting and ledger threading live in the companion wave-gateway work; this PR is the MCP client half for manifest vs manual install tagging in quarterly usage reporting.

Reviewed by Cursor Bugbot for commit f26adbe. Configure here.

Review in cubic

…hannel header

Client-side half of E2 usage-attribution (see the companion wave-gateway PR
feat/usage-attribution-e2, and governance/plans/wave-skills-distribution/
E2-USAGE-ATTRIBUTION.md on wave-av/claude-workstation).

getAuthHeaders() now adds X-Wave-Install-Channel when WAVE_INSTALL_CHANNEL is set in
the environment, so a Skill/manifest-generated onboarding config can self-declare its
install channel (e.g. skill-manifest) apart from a hand-written docs install
(docs-manual), which wave-gateway allowlists and folds into a non-billing usage-ledger
attribution dimension for quarterly reporting. Unset by default -- byte-identical to
today for anyone who does not set it (verified manually: with the env var set the
header is added, unset it is absent, everything else unchanged).

No test runner exists on this repo main (confirmed) -- verified via type-check + lint
(both clean) and a direct tsx smoke-run of getAuthHeaders() with/without the env var.

Branched from origin/main rather than PR #92 (fix/gateway-base-url-91) to avoid taking
a dependency on unreviewed, unmerged code -- a rebase conflict there is preferable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ab394472-db62-40e9-8fe4-25cc7b7097e3)

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 49 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 858cc206-c510-4dd1-90cf-b754e873a6de

📥 Commits

Reviewing files that changed from the base of the PR and between 8411936 and f26adbe.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • README.md
  • src/auth.ts

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add optional install-channel header for usage attribution

✨ Enhancement 📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Add optional WAVE_INSTALL_CHANNEL env var forwarded as X-Wave-Install-Channel.
• Keep request behavior byte-identical when the env var is unset.
• Document the new env var in README and changelog for onboarding clarity.
Diagram

graph TD
  A["Client process"] --> B["getAuthHeaders()"] --> C{"WAVE_INSTALL_CHANNEL set?"}
  C -->|"Yes"| D["Add X-Wave-Install-Channel"] --> E["API request"]
  C -->|"No"| E
  E --> F["wave-gateway"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Encode channel in User-Agent
  • ➕ Avoids introducing a new header name
  • ➕ Some observability stacks already index User-Agent
  • ➖ Semantically incorrect: mixes attribution with product/version identity
  • ➖ Harder to validate/allowlist cleanly on the server
  • ➖ More likely to be modified by intermediaries or client overrides
2. Use a query parameter (e.g., ?install_channel=...)
  • ➕ Easy to inspect in logs without header tooling
  • ➕ No need to modify header-building utilities
  • ➖ Leaks into caches, URLs, and analytics unexpectedly
  • ➖ Encourages propagation into stored links; higher privacy/retention risk
  • ➖ Less standard for cross-cutting metadata than headers
3. Read from a structured config file instead of env var
  • ➕ More explicit provenance; can be generated by onboarding tools
  • ➕ Potentially supports richer attribution metadata later
  • ➖ Adds parsing/IO and a new config surface area
  • ➖ Harder to set in ephemeral environments vs env vars
  • ➖ Doesn’t eliminate the need to forward via headers anyway

Recommendation: The PR’s approach (optional env var forwarded as a dedicated header) is the best fit: it’s explicit, allowlist-friendly, and remains fully backward-compatible when unset. Alternatives either blur semantics (User-Agent), increase leakage surface (query params), or add unnecessary complexity (config file) for the same end result.

Files changed (3) +22 / -1

Enhancement (1) +11 / -1
auth.tsConditionally add X-Wave-Install-Channel in getAuthHeaders() +11/-1

Conditionally add X-Wave-Install-Channel in getAuthHeaders()

• Introduces a constant for the install-channel header name and updates 'getAuthHeaders()' to append it when 'process.env.WAVE_INSTALL_CHANNEL' is set. Keeps header output unchanged when the env var is absent.

src/auth.ts

Documentation (2) +11 / -0
CHANGELOG.mdDocument optional install-channel env var and header forwarding +10/-0

Document optional install-channel env var and header forwarding

• Adds an Unreleased/Added entry describing 'WAVE_INSTALL_CHANNEL' and that it is forwarded as 'X-Wave-Install-Channel'. Emphasizes it is unset by default with no behavior change for existing users.

CHANGELOG.md

README.mdAdd WAVE_INSTALL_CHANNEL to environment variable table +1/-0

Add WAVE_INSTALL_CHANNEL to environment variable table

• Extends the configuration table to include 'WAVE_INSTALL_CHANNEL' and explains it is a self-declared attribution label sent as 'X-Wave-Install-Channel' and safe to leave unset.

README.md

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread README.md
| --- | --- | --- | --- |
| `WAVE_API_KEY` | Yes | - | Your WAVE API key |
| `WAVE_BASE_URL` | No | `https://wave.online` | API base URL |
| `WAVE_INSTALL_CHANNEL` | No | - | Self-declared install-channel label sent as `X-Wave-Install-Channel` (WAVE-internal usage-attribution reporting; safe to leave unset) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 README env-var table edited by hand but generated from .wave/repo.json

README.md states it is machine-generated from the grounded SSOT and verified by npm run verify. The new WAVE_INSTALL_CHANNEL row was added directly to README.md, but the corresponding "Environment variables" table in .wave/repo.json:429-448 still lists only WAVE_API_KEY and WAVE_BASE_URL. The next regeneration/verification pass will either drop this row or fail the check. Consider updating the SSOT entry as well.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/auth.ts
Comment on lines +40 to +41
const installChannel = process.env["WAVE_INSTALL_CHANNEL"];
if (installChannel) headers[INSTALL_CHANNEL_HEADER] = installChannel;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Unvalidated environment value forwarded verbatim as an outbound HTTP header

WAVE_INSTALL_CHANNEL is copied straight into the X-Wave-Install-Channel header (src/auth.ts:40-41) with no format validation. Values containing CR/LF or other illegal header characters cause fetch to throw a TypeError, breaking every API call; arbitrary values are also sent upstream unfiltered (server-side allowlisting is only documented, not enforced here).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@cursor cursor 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.

Risk: medium. Left a non-blocking comment (not approved): Cursor Bugbot and Cursor Security Agent both completed as skipped, so required automated-review signals are incomplete. Human review is needed; no additional reviewers could be assigned (only the PR author is assignable).

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Invalid header value crash 🐞 Bug ☼ Reliability
Description
getAuthHeaders() copies WAVE_INSTALL_CHANNEL verbatim into X-Wave-Install-Channel; if the value
contains characters invalid for HTTP headers (e.g., CR/LF), fetch() header construction can
throw/reject and break API calls. The code paths that call fetch() with these headers don’t locally
guard against this, so the error will propagate to callers at runtime.
Code

src/auth.ts[R40-42]

+  const installChannel = process.env["WAVE_INSTALL_CHANNEL"];
+  if (installChannel) headers[INSTALL_CHANNEL_HEADER] = installChannel;
+  return headers;
Evidence
The new code path reads WAVE_INSTALL_CHANNEL and writes it directly into the headers object returned
by getAuthHeaders(). Those headers are then passed directly into fetch() across multiple request
paths without local try/catch around fetch construction, so any header-construction failure will
propagate as a rejected promise/error at runtime.

src/auth.ts[34-43]
src/auth.ts[49-64]
src/tools/shared.ts[33-47]
src/resources/streams.ts[20-24]
src/resources/productions.ts[19-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`WAVE_INSTALL_CHANNEL` is injected into an outbound HTTP header without validation. If it contains illegal header characters (notably `\r` / `\n`), the runtime’s `fetch()`/`Headers` construction can throw/reject, causing request failures.

## Issue Context
`getAuthHeaders()` is used broadly as the default auth header source for API calls. Adding a quick validation here prevents configuration-induced runtime failures and gives operators a clear error message.

## Fix Focus Areas
- src/auth.ts[34-42]

## Suggested fix
- Before setting `headers["X-Wave-Install-Channel"]`, validate the env value:
 - Reject (throw a descriptive Error) or ignore (treat as unset) when it contains `\r` or `\n` (and optionally other control characters).
 - Optionally cap length to a reasonable size to avoid oversized headers.
- Keep behavior unchanged when the variable is unset.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 2 rules
Review mode: ⚖️ Balanced: This changes runtime authentication headers and environment-driven request behavior; although localized, it has real API/attribution and security-adjacent implications requiring a careful single-pass review.

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/auth.ts
Comment on lines +40 to +42
const installChannel = process.env["WAVE_INSTALL_CHANNEL"];
if (installChannel) headers[INSTALL_CHANNEL_HEADER] = installChannel;
return headers;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Invalid header value crash 🐞 Bug ☼ Reliability

getAuthHeaders() copies WAVE_INSTALL_CHANNEL verbatim into X-Wave-Install-Channel; if the value
contains characters invalid for HTTP headers (e.g., CR/LF), fetch() header construction can
throw/reject and break API calls. The code paths that call fetch() with these headers don’t locally
guard against this, so the error will propagate to callers at runtime.
Agent Prompt
## Issue description
`WAVE_INSTALL_CHANNEL` is injected into an outbound HTTP header without validation. If it contains illegal header characters (notably `\r` / `\n`), the runtime’s `fetch()`/`Headers` construction can throw/reject, causing request failures.

## Issue Context
`getAuthHeaders()` is used broadly as the default auth header source for API calls. Adding a quick validation here prevents configuration-induced runtime failures and gives operators a clear error message.

## Fix Focus Areas
- src/auth.ts[34-42]

## Suggested fix
- Before setting `headers["X-Wave-Install-Channel"]`, validate the env value:
  - Reject (throw a descriptive Error) or ignore (treat as unset) when it contains `\r` or `\n` (and optionally other control characters).
  - Optionally cap length to a reasonable size to avoid oversized headers.
- Keep behavior unchanged when the variable is unset.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

No findings are within the configured fix scope. To change which findings are fixed, adjust the setting on your Qodo configuration page.

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