chore(ts-migration): migrate API transport to TypeScript

Phase 4a: `demoUser` and `httpClient`. Type-only. The production bundle is
byte-identical - built from this commit's parent and from this commit, all six
chunk hashes match.

`httpClient` is the contract boundary, so the annotations are deliberately
conservative:

  - `KrowApiError` is a CAST, not a class. The three error sites still build a
    plain `Error` and assign `.name`, `.status`, `.code` and `.details` onto it
    exactly as before. `class KrowApiError extends Error` would have read
    better and changed three things that callers depend on: the prototype
    chain, `instanceof`, and how `name` comes to be set.

  - `RESOURCE_PATHS` becomes `Record<EntityName, EntityResourcePath>`. That is
    the first thing in the repo to check the eighteen entity names against the
    eighteen in `ENTITY_NAMES`; until now the two lists agreed only by habit,
    and a divergence would have surfaced as `base44.entities.Whatever` being
    undefined with nothing to say why.

  - `createEntity`'s parameter stays `string` and its return stays inferred.
    Annotating the return `EntityClient<any>` was tried and reverted: it makes
    `list()` return `Promise<any[]>` where inference gives `Promise<any>`, and
    `agentStore.js` does `qc.getQueryData(KEY) || (await ...list(...))`. React
    Query types `getQueryData` as `unknown`; `unknown || any` collapses to
    `any`, `unknown || any[]` stays `unknown`, and `.find` on the next line
    stopped compiling. Two new errors in a file this phase does not migrate,
    for no gain. `EntityClientFor<K>` is ready for the phase that migrates
    those consumers.

Unchanged and verified in the emitted output: `credentials: 'include'`, both
header branches on `body === undefined`, the URLSearchParams query encoding with
its repeated-array and undefined-omission rules, the `payload.data` unwrap that
drops `meta`, the verbatim server message, `status: 0` / `code: 'unreachable'`
for a transport failure, the `-created_date` and `limit` defaults, path
construction through `encodeURIComponent`, and `bulkCreate`'s sequential
`this.create` loop.

`DEMO_USER` is annotated `User`, which does real work: unannotated,
`role: 'admin'` widens to `string` and the default shape did not satisfy the
type the app uses for the thing it defaults.

Verified in isolation from the parallel feature work (d1425f9 plus these two
files): tsc 64 -> 53, the eleven removed being exactly this file's, and a
set-difference against the baseline showing none added. skill-check 1641/1642
with only the known stale-fixture failure, Owliver baseline 59/59, lint 0
errors, build succeeds with the API origin inlined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-17 12:16:16 +05:30
parent d1425f974c
commit f96f128839
2 changed files with 53 additions and 13 deletions

41
src/api/demoUser.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { User } from '@/types/user';
/**
* The shape of a signed-in user, before the server has answered.
*
* This is a **default shape, not a record**. The user lives in PostgreSQL and
* arrives from `GET /me`; what is here is the set of keys that must resolve
* during the first render, in the fraction of a second before that response
* lands.
*
* It exists as its own module for one reason: `base44Client.js` needs exactly
* this, and it used to reach into `api/seed.js` to get it — a 1,900-line
* fixture of demo positions, candidates, interviews, staff and shift records,
* every byte of which was then in the production bundle so that three booleans
* could be defaulted. The fixture is still the right thing for the test scripts
* that read it (`scripts/skill-check.mjs`, `scripts/owliver-capture.mjs`); it
* was never the right thing for the running app. `seed.js` re-exports this
* constant, so those scripts are unchanged and the production import chain no
* longer reaches them.
*
* Nothing here is a source of truth for anything. `preferences` is the one part
* that is read: `auth.preferences()` is synchronous — `AssistantPanelContext`
* decides whether Owliver starts open in a `useState` initialiser — so a key
* the server has never stored still has to resolve to something rather than to
* `undefined`.
*/
export const DEMO_USER: User = {
id: 'user_demo',
full_name: 'Alex Rivera',
email: 'demo@krow.app',
role: 'admin',
account_type: 'employer',
created_date: '2026-06-01T09:00:00.000Z',
/* Product preferences travel with the account rather than in a store of their
own, so there is one record to persist and one thing to read. */
preferences: {
owliverDefault: true,
compactDensity: false,
emailDigest: true,
},
};