AGENTS.md@packages/api-mock · git:20261014.13042d8 · 2026-10-14 · sha256 580531825f2acbcb

AGENTS.md@packages/api-mock git:20261014.13042d8A

Immutable. This exact content never changes and is served at /api/v1/blob/580531825f2acbcb.

# api-mock Agent Notes

Scoped instructions for `packages/api-mock/`. Read together with the
[root `AGENTS.md`](../../AGENTS.md).

## What's in here

`@zitadel/api-mock` is a workspace-internal MSW handler library
for the typed `@zitadel/api` Flow API. Consumers:

- `apps/storybook` — the `Orchestrator/Login` stories boot the worker
  through `msw-storybook-addon`.
- `packages/components/src/orchestrator/zitadel-login.spec.ts` — feeds
  the handlers into `msw/node`'s `setupServer`.
- The widget SDKs (`packages/sdk-{angular,qwik,react,solid,svelte,vue}`) —
  their `test-setup.ts` calls `serveMockFlowApi()` from the test-only
  `./vitest` (`msw/node`) or `./vitest-browser` (`msw/browser`) entry point.
- `apps/demo-next/` and `apps/demo-nuxt/` — hit the standalone TCP server
  started by `pnpm --filter @zitadel/api-mock start` (not an
  in-browser worker).

It is **not** published. It still builds to `dist/` like every other
workspace package, so consumers resolve it through its export map (the
`@zitadel/source` condition points in-repo tooling at the source) and depend
on `api-mock:build` in moon like any other package.

## The flow shape must mirror the real default flow

This mock is the substrate for the orchestrator's own test suite, the Storybook
stories, and the demo e2e suites. When its flow shape diverges from the server's,
every one of those consumers silently proves the wrong thing.

The authority is
[`packages/config/defaults/default-login.json`](../config/defaults/default-login.json),
embedded into the server via `configdefaults.DefaultLoginFlowDefinition()`. Two
properties it fixes, both of which this mock got wrong until they were corrected:

- **Sign-in is split**: `identifier` (email) → `password` → `done`. It is not a
  combined email+password card. The mock served a combined card for a long time,
  so the orchestrator's tests, the Storybook story, and both demo e2e suites were
  all walking a screen the server never emits.
- **Credential fields are keyed by the schema pointer** `x-auth-methods#password`
  (exported as `PASSWORD_FIELD`), on both `password` and `register-password`. The
  server answers `req.invalid` to a submit keyed on plain `password`, so the short
  name let the orchestrator send a key the real backend refuses — invisible to
  every mock-backed test.

Steps that `zitadel sso enable` adds (`register-sso`, `sso-conflict`) are not in
`default-login.json`. Their authority is
[`docs/design/idp/schemas/default-login.scaffold.json`](../../docs/design/idp/schemas/default-login.scaffold.json),
which the CLI's `applySsoToFlow` test keeps in step with the generator. The
`sso-conflict` password field is keyed by `PASSWORD_FIELD` as well.

Three knowing, documented exceptions: the `identifier` step keeps a `recover`
navigate action (the default flow defines no recovery step yet) so that screen
stays reachable; the `passkey-upsell`/`passkey-setup` pair is retained for
direct actor injection though the default flow no longer routes through it; and
`register-sso` serves `given_name`/`family_name` plus a `sign_in` navigate
where the scaffold collects `email` with a single `submit`.
(`register` on identifier and `sign_in` on register used to be listed here;
they are real now — the default declares them as navigate actions with
purposed transitions.)

**Before changing a step fixture, diff it against that JSON.** The per-step
action surface is enforced automatically by
`src/fixtures/default-conformance.spec.ts` — a fixture can neither miss a
declared action nor grow an extra beyond its documented allowlist. If a design
or test needs a shape the server does not emit, the flow definition changes
first.

## Architecture

```
xstate flowMachine (src/flow-machine.ts)
   ↓ snapshot.value picks the next step
fixtures (src/fixtures/login.ts)        — orval-typed CreateFlow201 builders
   ↓ withBranding(...) overlay
handlers (src/handlers.ts)              — orval `getCreateFlowMockHandler` etc.
   ↓
setupMock(worker) / setupMockHandlers() (src/index.ts)
```

Every step fixture returns the orval `CreateFlow201` shape. There are
no shadow step types — fixtures import from
`@zitadel/api/generated/model` and the structural identity of
the three orval response aliases (`CreateFlow201`, `GetFlowStep200`,
`SubmitFlowStep200`) lets a single fixture set drive all three handlers.

## Adding a new flow step

State names in the xstate machine **are** the canonical wire step names
(`identifier`, `register`, `recover`, `password`, `passkey-login`, `passkey-upsell`, `passkey-setup`, `sso-redirect`, `done`) — no
mapping function. To add a step:

1. Add a state to the machine in `src/flow-machine.ts` named exactly as
   it appears on the wire, and wire it into the relevant `SUBMIT`
   transition. Add the name to the `FlowStepName` union.
2. Add a fixture builder in `src/fixtures/login.ts` (or a sibling file
   for non-login flows). Builder must return `CreateFlow201` typed from
   orval; use `wrap(...)` for the standard envelope.
3. Add a `case` in `currentResponse()` in `src/handlers.ts` that calls
   the new fixture.
4. Add a walk test in `src/index.spec.ts` that exercises the typed
   orval client through the new branch.

## Dev fixture emails

`src/handlers.ts` recognizes special emails for the `Orchestrator/Login`
Storybook stories (`moon run storybook:dev`). The story docs note the same
table.

| When | Email | Result |
|------|-------|--------|
| Sign in submit | `wrong@example.com` | Stays on identifier; `error.invalid_credentials` on password |
| Sign in submit | `server@example.com` | Stays on identifier; `error.sign_in_server` form alert |
| Sign up submit | `exists@example.com` | Stays on register; `error.email_exists` on email |

Happy path: any other email → identifier/register → `done`. The
`passkey-upsell` / `passkey-setup` states still exist in the machine but are
no longer routed to by the default paths (passkey registration is offered up
front instead); their fixture emails (`passkey-cancel@…`, `passkey-unsupported@…`,
`passkey-fail@…`) only matter for tests that inject those states directly.

## Branding

`applyBranding(branding)` writes a module-level overlay merged into
every response. The overlay type is `CreateFlow201Branding &
Record<string, unknown>`: the wire baseline gets full TS guidance, and
the open record allows orchestrator-side v2 extension fields
(`palette`, `typography`, `shape`, `assets`, `theme`) without making
this package depend on the components type surface. The orchestrator's
branding-validator strips anything the OpenAPI doesn't model. Tests
should clear the overlay between cases via `clearBranding()`.

`src/server.ts` exposes two functions. `createMockApp({ issuer })` builds
the configured Express app and returns it without binding a port; it
calls `applyBranding(defaultDevBranding)` on boot so demo apps receive a
baseline `font_url` (Google Fonts Arimo) without extra setup.
`startMockServer(port)` wraps it for the standalone TCP server (deriving a
localhost issuer from `port`). The bare app is published via the
`./server` export so a serverless host can use it directly as a request
handler — see `apps/mock-zitadel`, which serves it as per-PR Vercel
previews. The branding payload lives in `src/default-dev-branding.ts` and
mirrors `docs/design/branding/branding.example.json`. MSW-only consumers
(`setupMockHandlers` / `setupMock`) do **not** get this overlay unless
they call `applyBranding` themselves — same as before.

## Testing

Two Vitest projects, mirroring `packages/components`:

- `unit` — node mode against `msw/node`'s `setupServer`. Picks up
  `*.spec.ts`. The end-to-end walk in `src/index.spec.ts` is the
  canonical contract test for the handlers, run locally by `pnpm test`.
- `browser` — real Chromium via Playwright against `msw/browser`'s
  `setupWorker`. Picks up `*.browser.spec.ts`. `index.browser.spec.ts`
  smoke-tests the `setupMock(worker)` browser entry point. Run alone
  via `pnpm test:browser`; it needs a Playwright browser install
  (`pnpm exec playwright install`).

`pnpm test:all` runs both projects, and is what CI runs (the `api-mock:test`
moon task invokes it with an `install-browsers` dep). When fixing a regression in step
routing, add a case to the unit spec first — it is the canonical contract for the
handlers.

## Don't

- Don't introduce shadow types for step / field / branding shapes. Use
  orval's `CreateFlow201*` aliases directly.
- Don't depend on `msw/browser` from anything other than the
  `setupMock(worker)` entry point and the test-only `./vitest-browser`
  harness — the same handlers must work in node.
- Don't ship a build artifact. Consumers import source via the workspace
  export map; the Moon build task is intentionally a no-op.
- Don't invent a step shape to make a test or a design convenient. The flow
  definition is the authority — see the section above.
- Don't hard-code `"password"` as a field name. Import `PASSWORD_FIELD`.