Skip to content

fix: dedupe pharmacy-admin auth, fail-fast pharmacy-api boot, re-chec… - #1174

Open
JONAH-6 wants to merge 17 commits into
harystyleseze:mainfrom
JONAH-6:fix/issues-1080-1081-1082-1083
Open

fix: dedupe pharmacy-admin auth, fail-fast pharmacy-api boot, re-chec…#1174
JONAH-6 wants to merge 17 commits into
harystyleseze:mainfrom
JONAH-6:fix/issues-1080-1081-1082-1083

Conversation

@JONAH-6

@JONAH-6 JONAH-6 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes four independent correctness and security issues.

Testing

  • Added/updated unit tests for rate freshness.
  • Added authentication tests for shared pharmacy admin middleware.
  • Verified new and modified tests pass.
  • Confirmed no new TypeScript errors were introduced by these changes.

Notes

There are existing unrelated test and TypeScript issues already present on the main branch (pricing-catalog, Stellar checksum, and build configuration issues). These were verified to be pre-existing and are not introduced by this PR.

Closes #1080
Closes #1081
Closes #1082
Closes #1083

…k rates freshness, hoist severity order

- shared/pharmacy-admin-auth.ts: single createPharmacyAdminAuth() factory
  (safeCompare-based) used by both server.ts and
  services/pharmacy-api/server.ts, replacing two independently-written
  Bearer-token checks that each used non-constant-time !==.
- services/pharmacy-api/server.ts now throws at module load when
  PHARMACY_1_PUBLIC_KEY is unset, matching its sibling services, instead
  of silently leaving defaultPharmacyApp undefined.
- services/bill-audit-api/server.ts re-runs checkRatesFreshness() on an
  unref'd daily interval (in addition to the existing boot-time check) so
  a long-running process notices the fair-market rates going stale.
- services/drug-interaction-api/logic.ts hoists the severityOrder object
  literal to a module-level constant instead of reallocating it on every
  sortPairsBySeverity() call.

Closes harystyleseze#1081, harystyleseze#1082, harystyleseze#1080, harystyleseze#1083
@JONAH-6
JONAH-6 requested a review from harystyleseze as a code owner July 30, 2026 16:51
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@JONAH-6 is attempting to deploy a commit to the Harrison Eze's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@JONAH-6 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…cceed

Root cause of every failing check on PR harystyleseze#1174 (ci, check-env-vars, npm
audit, CodeQL JS/TS, Playwright E2E): package-lock.json (root and
dashboard) was out of sync with package.json, so `npm ci` failed
immediately in every workflow with EUSAGE.

- package-lock.json / dashboard/package-lock.json: regenerated via
  `npm install --package-lock-only` to include @vitest/coverage-v8 and
  its transitive deps (root) and the dashboard's test toolchain
  (vitest, jsdom, testing-library) that were missing from the lock
  file, unblocking `npm ci` everywhere.
- tsconfig.json: removed the dead project-reference graph
  (shared/dashboard), which made bare `npx tsc --noEmit` hard-fail with
  TS6310 ("Referenced project may not disable emit") before checking
  a single file.
- types/vitest-globals.d.ts: added the missing `vitest/globals`
  ambient type reference so `vi`/`describe`/`it`/etc. resolve under
  the root tsconfig, instead of erroring as undefined names.
- .github/workflows/ci.yml: added the missing "Install dashboard
  dependencies" step before the dashboard typecheck/test steps, which
  otherwise ran without dashboard's node_modules ever being installed.
JONAH-6 added a commit to JONAH-6/careguard that referenced this pull request Jul 30, 2026
…cceed

Root cause of every failing check on PR harystyleseze#1174 (ci, check-env-vars, npm
audit, CodeQL JS/TS, Playwright E2E): package-lock.json (root and
dashboard) was out of sync with package.json, so `npm ci` failed
immediately in every workflow with EUSAGE.

- package-lock.json / dashboard/package-lock.json: regenerated via
  `npm install --package-lock-only` to include @vitest/coverage-v8 and
  its transitive deps (root) and the dashboard's test toolchain
  (vitest, jsdom, testing-library) that were missing from the lock
  file, unblocking `npm ci` everywhere.
- tsconfig.json: removed the dead project-reference graph
  (shared/dashboard), which made bare `npx tsc --noEmit` hard-fail with
  TS6310 ("Referenced project may not disable emit") before checking
  a single file.
- types/vitest-globals.d.ts: added the missing `vitest/globals`
  ambient type reference so `vi`/`describe`/`it`/etc. resolve under
  the root tsconfig, instead of erroring as undefined names.
- .github/workflows/ci.yml: added the missing "Install dashboard
  dependencies" step before the dashboard typecheck/test steps, which
  otherwise ran without dashboard's node_modules ever being installed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
JONAH-6 added 15 commits July 30, 2026 18:44
…vars

check-env-vars.ts flagged 9 vars as unused (STELLAR_RPC_URL,
AGENT_PUBLIC_KEY, CAREGIVER_SECRET_KEY, CAREGIVER_PUBLIC_KEY,
PHARMACY_{1,2,3}_SECRET_KEY, PHARMACY_3_PUBLIC_KEY,
BILL_PROVIDER_SECRET_KEY). These are legitimately never read via
process.env — they're the other half of wallet key pairs that
`npm run setup` (scripts/setup-wallets.ts) prints for every wallet in
WALLET_NAMES, documented in .env.example so operators can paste the
full pair, even though only one half of each pair (e.g. a recipient's
PUBLIC_KEY) is ever loaded at runtime.

Also fixed the script's glob ignore patterns ('node_modules/**' etc.),
which only matched at the repo root and crashed with EISDIR once
dashboard/node_modules exists, since glob would try to read directories
like dashboard/node_modules/decimal.js as files.
`pnpm --dir dashboard playwright install --with-deps chromium` failed
with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL because "playwright" is neither
a pnpm subcommand nor a package.json script name in dashboard/, so pnpm
fell back to its recursive-exec resolution and treated "dashboard" as
the command to run. `pnpm exec` is the correct way to invoke a binary
from node_modules/.bin (verified locally: the broken form reproduces
the exact CI error, `pnpm --dir dashboard exec playwright install`
works).
SpendingPolicyInput was typed via z.infer<typeof SpendingPolicySchema>
(the parsed *output* type), which makes holdTimeSeconds required even
though the schema gives it .default(0). Every caller that omits
holdTimeSeconds — which setSpendingPolicy is explicitly designed to
support via its DEFAULT_POLICY merge — failed to typecheck. Using
z.input<typeof SpendingPolicySchema> instead correctly reflects that
defaulted fields are optional on input, fixing ~40 spurious TS2345
errors across agent/__tests__ and tests/ with no behavior change.

Also removed a duplicate `import { existsSync, mkdirSync } from "fs"`
line in agent/server.ts (already imported on the line above).
- getPharmacyPrices -> comparePharmacyPrices (the actual exported name;
  the old name was never a real export, so the import itself failed
  to compile).
- getSpendingTracker() takes no arguments (recipient scoping happens
  via the preceding setSpendingPolicy(recipientId, ...) call) — five
  call sites were still passing a stale 'test-recipient' argument.
- getX402Fetch()/extractX402TxHash() are functions local to tools.ts,
  not exports of the @x402/fetch package, so mocking
  '@x402/fetch'.getX402Fetch had no effect on the code under test.
  Replaced with the same wrapFetchWithPayment-stubbing approach already
  used successfully in agent/__tests__/tools-x402.test.ts, so the
  malformed-JSON test actually exercises the code path it claims to.
…leseze#196)

docs/runbooks/switch-network.md documents that the MPP client is
"lazy-constructed on first use and cached for 60 seconds" so operators
can switch STELLAR_NETWORK at runtime via SIGHUP without a full
restart — but tools.ts actually constructed mppClient once, eagerly,
at module load, with no cache, no TTL, and no invalidateMppClientCache
export. This left both the documented runbook procedure and
agent/__tests__/mpp-client-lazy.test.ts's 5 tests broken (the test
import itself failed to compile).

Implemented getMppClient()/invalidateMppClientCache()/setMppClient()
following the exact same lazy + 60s TTL + SIGHUP-invalidation pattern
already used for getX402Fetch() in this same file, and updated the one
call site to go through getMppClient() instead of the old module-level
variable.

Also fixed the test's env setup: it never disabled MOCK_NETWORK, so
getMppClient() always took the mock branch and the spy on the real
Mppx.create() path was never exercised.
The global fallback AGENT_SECRET_KEY ("SAKYNUBM36...") fails Stellar's
StrKey checksum validation, so any test file that imports agent/tools.ts
without overriding AGENT_SECRET_KEY itself crashed with "invalid
checksum" the moment tools.ts called Keypair.fromSecret() at module
load — a real, valid-looking key was needed, not just a same-length
string. Replaced it with a freshly generated, checksum-valid testnet
keypair's secret.
…ngful

requireApiKey() (shared/auth.ts) only enforces the Bearer token when
AGENT_API_KEY is configured (or NODE_ENV=production) — otherwise it
calls next() unconditionally. Neither test file set AGENT_API_KEY, so
every /agent/* request — with a missing, wrong, or correct token —
silently passed through unauthenticated, making the "missing/wrong
token returns 401" assertions fail against a real 200/400 response.
extract-x402-tx-hash.test.ts: extractX402TxHash() has a three-way
return contract (undefined / TX_HASH_EXTRACTION_FAILED sentinel / real
hash) — 5 assertions still expected the old two-way (undefined-only)
contract for cases where decode fails but the header isn't a valid
64-char hex fallback, so they now get the sentinel instead of undefined.

x402-settle-contract.test.ts: all 5 "transaction hash" fixture
constants were 60-61 character strings, not real 64-char hex, so every
assertion checking `.toMatch(/^[a-f0-9]{64}$/)` or comparing extraction
output against them failed. Replaced with genuine 64-char hex values.
…nfig

The checksum-valid testnet keypair added to tests/setup.ts (to replace
the previous invalid-checksum fallback) matches .gitleaks.toml's
stellar-secret-seed rule (S[A-Z2-7]{55}), since that pattern is
inherent to the Stellar secret-key encoding format itself — any
validly-formatted key, real or test fixture, will match it. Added a
per-rule allowlist entry for this specific known-safe test value,
verified locally against the exact commit range CI scans
(gitleaks detect --log-opts="--no-merges --first-parent <base>..HEAD"),
confirming zero leaks.
…mock-network tests

persistence.test.ts hardcoded DATA_DIR from import.meta.url, ignoring
that tests/setup.ts always sets process.env.DATA_DIR to a worker-
specific temp dir — which is what tools.ts's own getDataDir() actually
reads first. Every fsState.files lookup was checking the wrong key,
so all writes appeared to silently vanish. Also updated the corrupt
spending.json test to expect logger.error (not .warn — that's what the
legacy-file recovery path actually calls) and to expect the current
default tracker shape, which now includes monthTotals.

mock-network.test.ts: AGENT_SECRET_KEY was "test-agent-secret", which
doesn't start with "S" and tripped validateSignerKeyForNetwork()'s own
prefix check before Keypair.fromSecret is ever reached (that part is
mocked, but the prefix check isn't). Also added a beforeEach directory
recreation, since tests/setup.ts's global afterEach wipes DATA_DIR
after every test but tools.ts only creates the recipient dir once at
module load, breaking the file's second test.
…tures

getPaybillSeqRetryTotal()/resetPaybillSeqRetryTotal() existed but
nothing ever incremented the underlying counter, so every assertion
against it saw 0. Incremented it alongside the existing
stellarTxBadSeqRetriesTotal metric at the one place tx_bad_seq retries
actually happen (submitTransactionWithRetry's reload-and-rebuild loop).

Also fixed paybill-seq-retry.test.ts's own bugs: its DEFAULT_POLICY had
medicationMonthlyBudget + billMonthlyBudget (800) exceeding monthlyLimit
(500), failing SpendingPolicySchema's cross-field refine before any
payBill call could even run; its fs mock was missing appendFileSync/
renameSync (added by the harystyleseze#205 append-only log and harystyleseze#44 atomic-write
work); and its "retry also fails" case queued only 2 rejections while
the real retry loop always runs its full 3 reload/retry attempts,
so it asserted a stale expected count.
buildAndSubmitUsdcPayment contained a signer-hint check (verifying the
signed envelope's signature hint matches the expected signer's before
ever calling submitTransaction) but was dead code — payBill actually
calls submitTransactionWithFeeBump, a different code path introduced
later for fee-bump/tx_bad_seq retry support, which never got this
safety check. Moved the check into submitTransactionWithFeeBump's
buildInner() (shared by payBill and payForMedication) and deleted the
now-fully-unused buildAndSubmitUsdcPayment.

This is a real safety check for a live-money code path — it refuses to
submit a Stellar transaction whose signature doesn't match the
expected signer — so agent/__tests__/stellar-signature.test.ts's 3
assertions on it now actually exercise real behavior instead of dead
code.

Since submitTransactionWithFeeBump is also unit-tested directly with
hand-rolled signer stubs, added the previously-unnecessary
signatureHint() mock to those stubs in fee-bump-doubling.test.ts and
fee-bump-retry-budget.test.ts — real Keypair objects always have this
method, only these ad-hoc test doubles didn't.
…=0 in pay-for-medication.test.ts

Two "daily/monthly cap" test cases overrode individual SpendingPolicy
fields without keeping the schema's cross-field invariants satisfied
(approvalThreshold <= dailyLimit, dailyLimit <= monthlyLimit), so
setSpendingPolicy() threw before the test logic under test ever ran.

The MPP-failure and success-path tests never set MOCK_NETWORK=0, so
payForMedication always took the built-in mock-network branch (which
unconditionally fakes success) instead of exercising the real
createMppClient() -> Mppx.create() path the test's mockMppFetch spy is
wired to intercept — every assertion checked the hardcoded mock
response instead of the configured mockMppFetch behavior.
…e timeout

STELLAR_TIMEBOUNDS_SECONDS (env-configurable, defaults to 60) was
parsed at module load but buildInner() hardcoded .setTimeout(30)
instead of using it, so the env var had no effect on the actual
transaction timebounds.
Two bugs combined to make the per-policy-timezone daily-limit check
untestable:

1. The fs mock's existsSync/readFileSync only recognized spending*.json
   paths by pattern-matching the filename; for policy.json it always
   returned existsSync=false, so loadPolicy() always fell back to the
   module's internal default (timezone-less) policy no matter what
   setSpendingPolicy() "saved" — the Phoenix timezone this test sets
   was silently dropped, so checkSpendingPolicy() always used the
   global SPENDING_TIMEZONE=UTC day boundary instead. Replaced the
   ad-hoc pattern-matched mock with a real in-memory file map (same
   approach as agent/__tests__/pay-for-medication.test.ts) that
   correctly persists whatever tools.ts actually writes.

2. Both tests injected a transaction via
   `tools.loadSpending("rosa").transactions = [...]` — loadSpending()
   returns a fresh, disconnected read from disk, not the live
   module-internal tracker checkSpendingPolicy() reads from. Switched
   to getSpendingTracker().transactions.push(...), whose shallow copy
   preserves the same array reference as the real tracker.
@JONAH-6
JONAH-6 force-pushed the fix/issues-1080-1081-1082-1083 branch from ee7779e to d403d13 Compare August 1, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment