---
phase: 10-pax8-client-auth-foundation
plan: 03
type: execute
wave: 2
depends_on: ["10-01"]
files_modified:
  - scripts/verify-pax8-auth.ts
  - CLAUDE.md
  - INTEGRATIONS.md
autonomous: false
requirements: [PAX8-01, PAX8-02]
user_setup:
  - service: pax8
    why: "Live OAuth2 token exchange + read-only companies call (Phase 10 SC#2) cannot run until the developer-provisioned PAX8 credentials are present"
    env_vars:
      - name: PAX8_CLIENT_ID
        source: "PAX8 developer portal (devx.pax8.com) — provisioned client ID; add to .env.local (gitignored)"
      - name: PAX8_CLIENT_SECRET
        source: "PAX8 developer portal (devx.pax8.com) — provisioned client secret; add to .env.local (gitignored)"

must_haves:
  truths:
    - "A live token exchange against api.pax8.com/v1/token succeeds and a read-only /companies call returns a content array (Phase 10 SC#2, VERIFIED not just mocked)"
    - "PAX8 appears in the CLAUDE.md External integrations table with the PAX8_* env-var prefix"
    - "INTEGRATIONS.md documents PAX8: base URL, OAuth2 client-credentials + audience, the two env vars, and the isPax8Configured()/getPax8Client() entry points"
    - "The verify script prints only company counts/status — never the token or the client secret"
  artifacts:
    - path: "scripts/verify-pax8-auth.ts"
      provides: "One-off live auth-proof script loading .env.local and calling getPax8Client().listCompanies()"
    - path: "CLAUDE.md"
      provides: "PAX8 row in the External integrations table"
    - path: "INTEGRATIONS.md"
      provides: "PAX8 integration reference section"
  key_links:
    - from: "scripts/verify-pax8-auth.ts"
      to: "lib/services/pax8-factory.ts"
      via: "import { getPax8Client }"
      pattern: "getPax8Client"
---

<objective>
Close out Phase 10 by (a) documenting the new PAX8 integration in the two
canonical docs the codebase keeps for integrations, and (b) proving the auth
handshake LIVE against the real PAX8 API — the one thing the mocked unit tests
in Plan 01 cannot prove. The live proof is gated on the developer adding their
provisioned PAX8 credentials, so this plan pauses for a human verification step.

Purpose: Satisfies Phase 10 Success Criterion #2 end-to-end (real token exchange
+ real read-only endpoint call) and records the PAX8_* env-var convention per
CONTEXT.md's canonical-refs instruction to add PAX8 to CLAUDE.md and INTEGRATIONS.md
once the client exists.

Output: `scripts/verify-pax8-auth.ts`, updated `CLAUDE.md` + `INTEGRATIONS.md`,
and a developer-confirmed live auth-proof.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-pax8-client-auth-foundation/10-CONTEXT.md
@.planning/phases/10-pax8-client-auth-foundation/10-RESEARCH.md

# Client built in Plan 01 (this plan depends on it existing)
@lib/services/pax8-factory.ts
@lib/services/pax8-client.ts

# Doc targets + script env-loading analog
@CLAUDE.md
@scripts/list-rmm-sites.ts

<interfaces>
<!-- Script env-loading pattern (scripts/list-rmm-sites.ts): scripts are plain .ts that
     load the gitignored .env.local themselves via dotenv:
       import { config } from 'dotenv';
       import { resolve } from 'path';
       config({ path: resolve('/opt/stacks/pulse/.env.local') });
     THEN import the factory. Run the same way other scripts/*.ts are run in this repo.
     SECURITY FACT (verified): .gitignore line 34 is `.env*` and `git ls-files` shows NO
     env file tracked — PAX8 secrets added to .env.local do NOT enter git history.
     This supersedes RESEARCH's committed-.env concern. -->
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write the live auth-proof script + document PAX8 in CLAUDE.md and INTEGRATIONS.md</name>
  <files>scripts/verify-pax8-auth.ts, CLAUDE.md, INTEGRATIONS.md</files>
  <read_first>
    - scripts/list-rmm-sites.ts (exact env-loading + main().catch(process.exit) script shape to mirror)
    - lib/services/pax8-factory.ts (getPax8Client entry point — created in Plan 01)
    - lib/services/pax8-client.ts (listCompanies signature — created in Plan 01)
    - CLAUDE.md (the "External integrations" markdown table listing each service and its env prefix — add a PAX8 row matching that exact column format)
    - INTEGRATIONS.md IF IT EXISTS at repo root (match its existing per-integration section format); if absent, skip the INTEGRATIONS.md edit and note that in the SUMMARY (do not create a stub)
  </read_first>
  <action>
    Create `scripts/verify-pax8-auth.ts` mirroring scripts/list-rmm-sites.ts: first
    `import { config } from 'dotenv'` and `config({ path: resolve('/opt/stacks/pulse/.env.local') })`,
    then import `getPax8Client` from `../lib/services/pax8-factory`. In an async main(), call
    `getPax8Client().listCompanies(0, 1)`, then log ONLY a success summary — e.g. the count of
    the returned `content` array and the `page.totalElements` value — and NEVER log the access
    token, the client secret, or full company records. Wrap in `main().catch((err) => { console.error(err); process.exit(1); })`. Add a top-of-file comment: run with the same TS runner
    used for other scripts/*.ts (e.g. `npx tsx scripts/verify-pax8-auth.ts`), requires
    PAX8_CLIENT_ID and PAX8_CLIENT_SECRET in .env.local.
    Then edit `CLAUDE.md`: add a row to the External integrations table for `PAX8` with env
    prefix `PAX8_*`, matching the existing table's column layout. If `INTEGRATIONS.md` exists,
    add a PAX8 section documenting: base URL `https://api.pax8.com/v1`, OAuth2 client-credentials
    auth with `audience: https://api.pax8.com`, the two env vars (`PAX8_CLIENT_ID`,
    `PAX8_CLIENT_SECRET`, stored in `.env.local`), and the `isPax8Configured()` / `getPax8Client()`
    entry points in `lib/services/pax8-factory.ts` — matching the section format of the other
    integrations already documented there.
  </action>
  <verify>
    <automated>test -f scripts/verify-pax8-auth.ts && grep -q "getPax8Client" scripts/verify-pax8-auth.ts && grep -q "PAX8" CLAUDE.md && npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `scripts/verify-pax8-auth.ts` exists, loads `.env.local` via dotenv, and calls `getPax8Client().listCompanies(...)`
    - `grep -n "access_token\|accessToken\|clientSecret\|CLIENT_SECRET" scripts/verify-pax8-auth.ts` returns no line that logs a secret/token (the script logs only counts/status)
    - `CLAUDE.md` External integrations table contains a `PAX8` row with the `PAX8_*` prefix
    - If INTEGRATIONS.md exists: it contains a PAX8 section naming both env vars and the `audience` value; if it does not exist, the SUMMARY records that it was skipped
    - `npx tsc --noEmit --pretty` exits 0
  </acceptance_criteria>
  <done>Verify script written (secret-safe), CLAUDE.md has the PAX8 row, INTEGRATIONS.md documents PAX8 (or skip noted), tsc clean.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Live PAX8 auth-proof (developer adds credentials)</name>
  <action>PAUSE for the developer. This is a human-verify checkpoint: the developer adds PAX8_CLIENT_ID/PAX8_CLIENT_SECRET to the gitignored .env.local and runs scripts/verify-pax8-auth.ts to prove the live token exchange + read-only /companies call succeed (Phase 10 SC#2). Do not auto-approve — resume only on the developer signal below.</action>
  <what-built>
    A live auth-proof script (`scripts/verify-pax8-auth.ts`) that runs the real PAX8 OAuth2
    token exchange and a read-only `GET /companies` call through the Plan 01 client. Mocked
    unit tests already prove the code shape; this confirms the real PAX8 API contract matches.
  </what-built>
  <how-to-verify>
    1. Add your provisioned PAX8 credentials to `.env.local` (gitignored — verified `.env*` is
       in .gitignore and no env file is tracked, so this does NOT enter git history):
         PAX8_CLIENT_ID=...   PAX8_CLIENT_SECRET=...
    2. Run the script the same way you run other `scripts/*.ts` in this repo, e.g.:
         npx tsx scripts/verify-pax8-auth.ts
    3. Expected: it prints a success summary (a company count / totalElements) and exits 0 —
       proving the token exchange returned a usable bearer token AND the `/companies` read
       succeeded with it.
    4. Failure signals to report back: a 200 token followed by a 403 on `/companies` means the
       `audience` is wrong (RESEARCH Pitfall 2); a 400/415 on the token POST means the JSON
       body/Content-Type is wrong (Pitfall 3); "PAX8 is not configured" means the env vars are
       not being loaded from `.env.local`.
    Security note: your PAX8_CLIENT_SECRET lives only in the gitignored `.env.local`; the script
    never prints it or the token.
  </how-to-verify>
  <resume-signal>Type "approved" once the script prints a company count and exits 0, or paste the error output to triage.</resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| operator -> .env.local | Developer places real PAX8 credentials into the gitignored `.env.local` |
| Pulse script -> PAX8 API (outbound) | Live OAuth2 handshake + read-only companies call over HTTPS |

This plan installs no external packages (dotenv already present, used by existing scripts). Supply-chain (`T-*-SC`) checkpoint not triggered.

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-10-09 | Information Disclosure | PAX8 secret in git history | mitigate | Credentials go into `.env.local`, which `.gitignore` covers via `.env*` (line 34); `git ls-files` confirms no env file is tracked. Checkpoint instructions explicitly direct the secret to `.env.local`, NOT the (untracked) `.env`. Supersedes RESEARCH's committed-.env concern |
| T-10-10 | Information Disclosure | verify script stdout | mitigate | Script logs only company counts / `page.totalElements` and success status — never the access token, never the client secret. Enforced by grep acceptance criterion in Task 1 |
| T-10-11 | Elevation of Privilege | wrong OAuth2 audience surfaces at runtime | mitigate | The live call is the detector: a 200 token then 403 on `/companies` flags a wrong `audience` before Phase 11 builds sync on top of it. Checkpoint step 4 documents this signal |

No HIGH-severity threat blocks this plan. The one credential-handling threat (T-10-09) is mitigated by the verified gitignore coverage, so no blocking security halt is required.
</threat_model>

<verification>
- Task 1 automated gate passes (script exists, imports getPax8Client, CLAUDE.md updated, tsc clean)
- `grep -n "access_token\|clientSecret\|CLIENT_SECRET" scripts/verify-pax8-auth.ts` shows no secret/token logging
- Human checkpoint: `npx tsx scripts/verify-pax8-auth.ts` prints a company count and exits 0 (Phase 10 SC#2 LIVE)
</verification>

<success_criteria>
- PAX8-01 (SC#2, live): `getPax8Client()` performs a real OAuth2 client-credentials token exchange against `api.pax8.com/v1` and successfully calls a read-only endpoint (list companies) with the resulting bearer token — confirmed by the developer running the verify script
- PAX8 is documented in CLAUDE.md's External integrations table and (if present) INTEGRATIONS.md, per CONTEXT.md canonical-refs
</success_criteria>

<output>
Create `.planning/phases/10-pax8-client-auth-foundation/10-03-SUMMARY.md` when done.
</output>
