# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Read-only MCP server + Catalog Data Layer for Znet's Magento 2 store, used from ChatGPT. Node 22, TypeScript (strict, ESM, `NodeNext`), pnpm 11 workspaces, PostgreSQL 16. Full design and decisions: [ARCHITECTURE.md](ARCHITECTURE.md); run/deploy notes: [README.md](README.md); Magento Integration setup: [docs/magento-setup.md](docs/magento-setup.md).

> The `.claude/rules/*.md` (ScandiPWA/React), `.claude/QUICK_START.md` and `.claude/ARCHITECTURE_MAP.md` describe the separate Magento + ScandiPWA storefront project, **not this repo**. Don't apply them here.

## Commands

```bash
pnpm install
pnpm build                    # tsc -b over project references (packages/* then apps/*)
pnpm test                     # vitest: unit + e2e + db-roles (needs Postgres)
pnpm exec vitest run apps/sync/src/mappers.test.ts   # single file
pnpm exec vitest run -t "visibility"                 # by test name

pnpm db:generate --name <change>   # after editing packages/catalog-db/src/schema.ts
pnpm db:migrate                    # also re-applies role grants

pnpm dev:api     # catalog-api  :4100
pnpm dev:mcp     # mcp-server   :4200/mcp  (AUTH_MODE=static for local; token from MCP_STATIC_TOKENS)
pnpm dev:sync    # pg-boss scheduler
pnpm sync:once <sync-reference|sync-products-full|sync-products-incremental|sync-inventory>
pnpm sync:once sku <SKU>
pnpm magento:check                 # verify Magento connectivity/ACLs, estimate full-sync time
pnpm auth:sessions / pnpm auth:revoke <username>
pnpm perf 50000                    # synthetic large-catalog benchmark
pnpm exec tsx test/run-mock.ts 4900   # fake Magento (token magento-test-token) for local runs without staging
```

There is no lint script; `pnpm build` (type-check) and `pnpm test` are the gates.

- All `tsx`/dev scripts run with `--conditions=znet-source`: each workspace package's `exports` has a `znet-source` condition pointing at `src/*.ts`, so nothing needs building for dev or tests (vitest sets the same condition). Plain `node` uses `dist/`, so run `pnpm build` before that.
- Tests: `NODE_ENV=test` stops `.env` from loading (hermetic). The e2e test drops/recreates `mcp_znet_test`; db-roles uses `mcp_znet_roles_test`; perf uses `mcp_znet_perf`. Defaults are `postgres://localhost/...`; override with `TEST_DATABASE_URL`, `TEST_ROLES_DATABASE_URL`, `PERF_DATABASE_URL`. Test files run serially (`fileParallelism: false`) because they share cluster-wide roles.
- Local Postgres is provided by DDEV here (`.ddev/config.yaml`, Postgres 16; `.env`'s `DATABASE_URL` uses host `db`, which resolves only inside DDEV containers).

## Architecture

```
Magento ──► apps/sync ──► Postgres (catalog.*, ops.*) ──► apps/catalog-api ──► apps/mcp-server ──► ChatGPT
   ▲                                                                              │
   └──────────────────── live orders / salable stock / live price ────────────────┘
```

- **`packages/shared`** — `loadEnv(schema)` plus composable zod env fragments (`baseEnv`, `databaseEnv`, `magentoEnv`, …). Each app builds its own schema in `src/config.ts` by `.extend()`-ing fragments; config errors fail fast at startup. Also pino logger (redacts tokens/PII) and `PublicError` (the only error text shown to MCP clients).
- **`packages/magento`** — `MagentoClient` (OAuth 1.0a signing or bearer, retry/backoff, pagination, `searchCriteria` builder) and `MagentoApi` (typed endpoints). All four OAuth1 values must be set together or none.
- **`packages/catalog-db`** — Drizzle `schema.ts`, SQL migrations in `migrations/`, `writes.ts` (sync upserts), `reads.ts` + `quality.ts` (API queries), `pricing.ts` (effective price), `oauth.ts` (hashed OAuth codes/tokens), `grants.ts` (per-service DB roles).
- **`apps/sync`** — `jobs.ts` holds the job functions (`JOBS` map); `index.ts` registers them as pg-boss queues with the `singleton` policy and cron schedules; `cli.ts` runs one job directly. Incremental sync uses a per-stream `updated_at` cursor in `ops.sync_state` with a 2-minute overlap (upserts are idempotent); the full sweep soft-deletes products Magento no longer returns. Inventory sync is disabled when `CATALOG_STOCK_ENABLED=false` (Znet's setting). `mappers.ts` turns Magento DTOs into rows.
- **`apps/catalog-api`** — Fastify app (`buildApp` in `app.ts`, imported directly by e2e tests); the only reader of `catalog.*`; service-token auth (`CATALOG_API_TOKEN`); `visibility.ts` diagnoses why a product isn't visible on the storefront.
- **`apps/mcp-server`** — Express + MCP SDK, Streamable HTTP, **stateless** (a new `McpServer` per request). Tools live in `src/tools/` (`catalog.ts`, `orders.ts`), each declared with `defineTool({ name, scope, input, run })`. `registry.ts` wraps every tool: only tools within the caller's scopes are listed, every call goes to `ops.audit_log` with PII-redacted args, errors map to safe messages, output is truncated. Catalog tools call the Catalog API through `catalog-client.ts`; order tools call Magento live. `pii.ts` masks customer data unless the caller has `orders:pii`.
- **Auth modes** (`AUTH_MODE`): `magento` (default) is a built-in OAuth 2.1 server (`src/magento-auth/`) where staff sign in with Magento admin user + password + Google Authenticator code, checked against Magento and never stored, with scopes from `MCP_USER_SCOPES`; `oauth` takes external IdP JWTs via JWKS; `static` uses fixed tokens and is refused in production.

### Invariants to preserve

- **Read-only toward Magento.** No tool or job writes to Magento; all tools are annotated `readOnlyHint: true`.
- **Per-service DB roles** (`grants.ts`, verified by `test/db-roles.test.ts`): `mcp_sync` writes `catalog.*`; `mcp_api` only reads; `mcp_server` can insert into the audit log and manage `ops.oauth_*`, but **cannot read the catalog** or read/modify the audit log. If you add a table or change which service touches what, update `grants.ts` and the roles test. Migrations run as the owner role `mcp_znet`.
- Scopes: `catalog:read`, `orders:read` (PII masked), `orders:pii`. PII is masked by default; full PII is a scope grant, not a code change.
- Store-scoped data: `store_id = 0` is admin/default scope; reads fall back to store 0.

## Code style

Match the existing TypeScript: 2-space indent, single quotes, no semicolons, `.js` extensions on relative imports (NodeNext), `import type` where applicable (`verbatimModuleSyntax`). Workspace packages are imported as `@znet/shared`, `@znet/magento`, `@znet/catalog-db`. Tests sit next to source as `*.test.ts`; cross-app tests go in `test/`.
