@classic-homes/api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @classic-homes/api
2
+
3
+ Typed client for the CHAPI API. Every path, query param, request body and
4
+ response is typed from CHAPI's OpenAPI contract (`openapi.json` at the repo
5
+ root), so consuming repos get autocomplete and compile-time checks against the
6
+ real API — including the v2 (D1-backed) endpoints. v2 response bodies are typed
7
+ per resource (e.g. `LotV2`, `CommunityV2`) from the field registry.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @classic-homes/api
13
+ ```
14
+
15
+ This is a scoped, restricted package published under the `@classic-homes` org, so a
16
+ consuming repo must authenticate npm to that scope first. Add an `.npmrc` at the
17
+ repo root:
18
+
19
+ ```ini
20
+ # .npmrc (consuming repo)
21
+ @classic-homes:registry=https://registry.npmjs.org/
22
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
23
+ ```
24
+
25
+ Then export a token with read access to the org before installing / in CI:
26
+
27
+ ```bash
28
+ export NPM_TOKEN=xxxxxxxx # org read token; do NOT commit it
29
+ npm install @classic-homes/api
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```ts
35
+ import { createChapiClient } from '@classic-homes/api';
36
+
37
+ const chapi = createChapiClient({
38
+ baseUrl: 'https://api.example.com',
39
+ token: async () => getAccessToken(), // string | () => string | Promise<string>
40
+ });
41
+
42
+ // List v2 lots — params, query and response are fully typed.
43
+ const { data, error } = await chapi.GET('/v2/lots', {
44
+ params: { query: { page: 1, limit: 25, sort: '-lotNumber' } },
45
+ });
46
+ if (error) throw new Error('request failed');
47
+ for (const lot of data.data) {
48
+ // lot is typed from the OpenAPI schema
49
+ }
50
+
51
+ // PATCH v2 enrichment
52
+ await chapi.PATCH('/v2/filings/{id}', {
53
+ params: { path: { id: 390 } },
54
+ body: { webStatus: 'Production' },
55
+ });
56
+ ```
57
+
58
+ The client is a thin wrapper over
59
+ [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/); `GET`/`POST`/`PATCH`/…
60
+ methods and the `params`/`body` shapes come from it.
61
+
62
+ ## Authentication
63
+
64
+ Every request is sent as `Authorization: Bearer <token>`. The API accepts either:
65
+
66
+ - **A JWT** — obtained from CHAPI's auth flow. Use a `token` function to refresh
67
+ short-lived JWTs per request.
68
+ - **An API key** — pass the key string as `token`. The API checks the bearer value
69
+ as an API key first, then falls back to JWT verification.
70
+
71
+ ```ts
72
+ // static API key
73
+ const chapi = createChapiClient({ baseUrl, token: process.env.CHAPI_API_KEY });
74
+
75
+ // or a refreshing JWT
76
+ const chapi = createChapiClient({ baseUrl, token: () => auth.getAccessToken() });
77
+ ```
78
+
79
+ ### Auth helpers
80
+
81
+ The package also ships typed auth calls, error predicates, and an auto-refreshing
82
+ session that handles CHAPI's short-lived access tokens and **refresh-token rotation**
83
+ (every refresh returns a NEW refresh token that replaces the old one).
84
+
85
+ ```ts
86
+ import {
87
+ createChapiClient,
88
+ createAuthSession,
89
+ login,
90
+ isUnauthorized,
91
+ isForbidden,
92
+ isRateLimited,
93
+ } from '@cos/chapi-client';
94
+
95
+ // 1. Log in (typed wrapper over POST /v1/auth/login)
96
+ const bootstrap = createChapiClient({ baseUrl });
97
+ const { data, error } = await login(bootstrap, { email, password });
98
+ if (error) throw new Error('login failed');
99
+
100
+ // 2. Create a session that keeps the access token fresh and follows rotation.
101
+ // Persist BOTH tokens on change — the previous refresh token is now invalid.
102
+ const session = createAuthSession({
103
+ baseUrl,
104
+ tokens: data, // { accessToken, refreshToken, sessionToken }
105
+ onTokensChanged: (t) => saveToStorage(t),
106
+ });
107
+
108
+ // 3. Use the session's token provider — it refreshes transparently before expiry.
109
+ const chapi = createChapiClient({ baseUrl, token: session.token });
110
+
111
+ // 4. Branch on typed errors instead of string-matching codes
112
+ const res = await chapi.GET('/v2/homes', { params: { query: { page: 1 } } });
113
+ if (isUnauthorized(res.error)) redirectToLogin();
114
+ else if (isForbidden(res.error)) showNoAccess();
115
+ else if (isRateLimited(res.error)) backOff();
116
+ ```
117
+
118
+ `logout(client)` and `refresh(client, refreshToken)` are also exported. See
119
+ `docs/TOKEN_LIFECYCLE.md` in the API repo for the full lifecycle.
120
+
121
+ ## Errors, pagination & permissions
122
+
123
+ - **Errors** — `openapi-fetch` returns `{ data, error }` (it does not throw on
124
+ non-2xx). `error` is the typed error body (`{ error: { code, message, ... } }`).
125
+ Always branch on `error` before using `data`.
126
+ - **Pagination** — list responses carry `meta.pagination` (`page`, `limit`, `total`,
127
+ `totalPages`). `limit` max is 500. Page with `params.query.page` / `limit`.
128
+ - **Sources & staleness** — `meta.sources` names the backing edge DB; `meta.sync`
129
+ (`lastCompleted`, `isStale`) and the `X-Data-Stale` header report data freshness.
130
+ - **Permissions are exact-match** — v2 hides fields your token isn't explicitly
131
+ granted. A `*` wildcard does NOT unlock permission-scoped fields (e.g. financial
132
+ fields need `<resource>:read:financial`). Missing fields usually mean a missing
133
+ permission, not a bug.
134
+ - **PATCH write-through** — `meta.writeThrough` is `'ok'` when the edge DB was
135
+ updated in-request, or `'deferred'` when the write is pending the next sync
136
+ (the underlying record was still updated; the read model just lags briefly).
137
+
138
+ ## Incremental sync (`updated-since`)
139
+
140
+ Each resource exposes `GET /v2/<resource>/updated-since?since=<ISO>` returning rows
141
+ with `lastUpdated >= since`, ordered by `lastUpdated` then primary key. To pull a
142
+ stable incremental feed, page until empty, then advance `since` to the max
143
+ `lastUpdated` seen and dedupe by `id` (rows sharing a boundary timestamp can repeat
144
+ across polls):
145
+
146
+ ```ts
147
+ async function pullSince(chapi, since: string) {
148
+ const seen = new Set<string>();
149
+ let page = 1;
150
+ let maxUpdated = since;
151
+ for (;;) {
152
+ const { data, error } = await chapi.GET('/v2/lots/updated-since', {
153
+ params: { query: { since, page, limit: 500 } },
154
+ });
155
+ if (error) throw new Error('updated-since failed');
156
+ for (const row of data.data) {
157
+ if (seen.has(String(row.id))) continue;
158
+ seen.add(String(row.id));
159
+ if (row.lastUpdated && row.lastUpdated > maxUpdated) maxUpdated = row.lastUpdated;
160
+ // ...upsert row into your store...
161
+ }
162
+ if (data.data.length < 500) break;
163
+ page += 1;
164
+ }
165
+ return maxUpdated; // pass as `since` on the next poll
166
+ }
167
+ ```
168
+
169
+ ## How the types stay accurate
170
+
171
+ - `openapi.json` (repo root) is generated from the endpoint registry via
172
+ `npm run openapi:generate` and committed.
173
+ - CI runs `npm run openapi:check` to fail the build if the committed spec drifts
174
+ from the code.
175
+ - This package's `npm run generate` regenerates `src/types.ts` from that spec
176
+ (`src/types.ts` is git-ignored — always built, never hand-edited).
177
+
178
+ ## Scripts
179
+
180
+ | Script | Purpose |
181
+ | --- | --- |
182
+ | `npm run generate` | Regenerate `src/types.ts` from `../../openapi.json` |
183
+ | `npm run build` | Generate types, then compile to `dist/` |
184
+ | `npm run typecheck` | Type-check without emitting |
185
+
186
+ ## Publishing (maintainers)
187
+
188
+ The package is published by the `Publish SDK` workflow on an `sdk-v*` tag. To cut a
189
+ release:
190
+
191
+ 1. From the repo root, regenerate and verify the contract:
192
+ `npm run openapi:generate && npm run openapi:check`.
193
+ 2. Bump `version` in `packages/chapi-client/package.json` and add a `CHANGELOG.md`
194
+ entry.
195
+ 3. Commit, then tag: `git tag sdk-v<version> && git push origin sdk-v<version>`.
196
+
197
+ The workflow rebuilds `src/types.ts` from the committed `openapi.json` and runs
198
+ `npm publish --provenance`, so the published types always match the tagged
199
+ contract. Deploy tags (`v*`) are separate from SDK tags (`sdk-v*`).
200
+
201
+ ## Example consumer
202
+
203
+ A minimal runnable example lives at `examples/sdk-consumer/` in the CHAPI repo
204
+ (list lots + PATCH a filing against a local `wrangler dev`).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Auth helpers for the CHAPI client.
3
+ *
4
+ * The base client only gives you a `token` injection point. This module adds the pieces
5
+ * every consumer would otherwise hand-roll: typed login/refresh/logout calls, a typed error
6
+ * taxonomy, and an auto-refreshing session that transparently keeps a short-lived access
7
+ * token fresh and follows refresh-token ROTATION (every refresh returns a new refresh token
8
+ * that must replace the old one).
9
+ */
10
+ import { type ChapiClient } from './client';
11
+ /** Shape of a CHAPI error body: `{ error: { code, message } }`. */
12
+ export interface ChapiApiError {
13
+ error?: {
14
+ code?: string;
15
+ message?: string;
16
+ } | null;
17
+ }
18
+ /** True for 401-class failures (missing/invalid/expired/revoked credentials). */
19
+ export declare function isUnauthorized(err: unknown): boolean;
20
+ /** True when authenticated but not permitted (403). */
21
+ export declare function isForbidden(err: unknown): boolean;
22
+ /** True when rate limited (429). */
23
+ export declare function isRateLimited(err: unknown): boolean;
24
+ /** True when the account is locked (423). */
25
+ export declare function isAccountLocked(err: unknown): boolean;
26
+ export interface AuthTokens {
27
+ accessToken: string;
28
+ refreshToken?: string;
29
+ sessionToken?: string;
30
+ }
31
+ /** `POST /v1/auth/login`. Returns the openapi-fetch `{ data, error }` result. */
32
+ export declare function login(client: ChapiClient, body: {
33
+ username?: string;
34
+ email?: string;
35
+ password: string;
36
+ [k: string]: unknown;
37
+ }): Promise<import("openapi-fetch").FetchResponse<{
38
+ parameters: {
39
+ query?: never;
40
+ header?: never;
41
+ path?: never;
42
+ cookie?: never;
43
+ };
44
+ requestBody: {
45
+ content: {
46
+ "application/json": import("./types").components["schemas"]["LoginRequest"];
47
+ };
48
+ };
49
+ responses: {
50
+ 200: {
51
+ headers: {
52
+ [name: string]: unknown;
53
+ };
54
+ content: {
55
+ "application/json": import("./types").components["schemas"]["LoginResponse"];
56
+ };
57
+ };
58
+ 400: {
59
+ headers: {
60
+ [name: string]: unknown;
61
+ };
62
+ content: {
63
+ "application/json": import("./types").components["schemas"]["Error"];
64
+ };
65
+ };
66
+ 401: {
67
+ headers: {
68
+ [name: string]: unknown;
69
+ };
70
+ content: {
71
+ "application/json": import("./types").components["schemas"]["Error"];
72
+ };
73
+ };
74
+ 423: {
75
+ headers: {
76
+ [name: string]: unknown;
77
+ };
78
+ content: {
79
+ "application/json": import("./types").components["schemas"]["Error"];
80
+ };
81
+ };
82
+ 429: {
83
+ headers: {
84
+ [name: string]: unknown;
85
+ };
86
+ content: {
87
+ "application/json": import("./types").components["schemas"]["Error"];
88
+ };
89
+ };
90
+ };
91
+ }, {
92
+ body: never;
93
+ }, `${string}/${string}`>>;
94
+ /** `POST /v1/auth/refresh`. Returns a NEW access token AND a NEW (rotated) refresh token. */
95
+ export declare function refresh(client: ChapiClient, refreshToken: string): Promise<import("openapi-fetch").FetchResponse<{
96
+ parameters: {
97
+ query?: never;
98
+ header?: never;
99
+ path?: never;
100
+ cookie?: never;
101
+ };
102
+ requestBody: {
103
+ content: {
104
+ "application/json": {
105
+ refreshToken: string;
106
+ };
107
+ };
108
+ };
109
+ responses: {
110
+ 200: {
111
+ headers: {
112
+ [name: string]: unknown;
113
+ };
114
+ content: {
115
+ "application/json": {
116
+ accessToken?: string;
117
+ refreshToken?: string;
118
+ tokenType?: "Bearer";
119
+ expiresIn?: number;
120
+ };
121
+ };
122
+ };
123
+ 401: {
124
+ headers: {
125
+ [name: string]: unknown;
126
+ };
127
+ content: {
128
+ "application/json": import("./types").components["schemas"]["Error"];
129
+ };
130
+ };
131
+ };
132
+ }, {
133
+ body: never;
134
+ }, `${string}/${string}`>>;
135
+ /** `POST /v1/auth/logout`. Requires the client to be authenticated. */
136
+ export declare function logout(client: ChapiClient): Promise<import("openapi-fetch").FetchResponse<{
137
+ parameters: {
138
+ query?: never;
139
+ header?: never;
140
+ path?: never;
141
+ cookie?: never;
142
+ };
143
+ requestBody?: never;
144
+ responses: {
145
+ 200: {
146
+ headers: {
147
+ [name: string]: unknown;
148
+ };
149
+ content: {
150
+ "application/json": import("./types").components["schemas"]["MessageResponse"];
151
+ };
152
+ };
153
+ 401: {
154
+ headers: {
155
+ [name: string]: unknown;
156
+ };
157
+ content: {
158
+ "application/json": import("./types").components["schemas"]["Error"];
159
+ };
160
+ };
161
+ };
162
+ }, never, `${string}/${string}`>>;
163
+ export interface AuthSessionOptions {
164
+ /** API origin, e.g. https://api.example.com */
165
+ baseUrl: string;
166
+ /** Initial tokens (e.g. from a prior login or SSO callback). */
167
+ tokens: AuthTokens;
168
+ /**
169
+ * Called whenever the tokens change (after a refresh rotates them). Persist BOTH the new
170
+ * access token and the new refresh token — the previous refresh token is now invalid.
171
+ */
172
+ onTokensChanged?: (tokens: AuthTokens) => void;
173
+ /** Refresh this many seconds before the access token actually expires. Default 30. */
174
+ refreshSkewSeconds?: number;
175
+ }
176
+ export interface AuthSession {
177
+ /** A `token` provider to pass to `createChapiClient({ token })` — refreshes as needed. */
178
+ token: () => Promise<string>;
179
+ /** Current tokens snapshot. */
180
+ getTokens: () => AuthTokens;
181
+ /** Force a refresh now (rotates the refresh token). Returns the new access token. */
182
+ refreshNow: () => Promise<string>;
183
+ }
184
+ /**
185
+ * Create a session that keeps the access token fresh and follows refresh-token rotation.
186
+ *
187
+ * Pass `session.token` as the `token` option of `createChapiClient`. Before each request the
188
+ * provider checks the access token's `exp`; if it is expired (or within the skew window) it
189
+ * calls `/v1/auth/refresh`, stores the rotated tokens, and fires `onTokensChanged`.
190
+ *
191
+ * @example
192
+ * const session = createAuthSession({ baseUrl, tokens, onTokensChanged: save });
193
+ * const chapi = createChapiClient({ baseUrl, token: session.token });
194
+ */
195
+ export declare function createAuthSession(options: AuthSessionOptions): AuthSession;
package/dist/auth.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Auth helpers for the CHAPI client.
3
+ *
4
+ * The base client only gives you a `token` injection point. This module adds the pieces
5
+ * every consumer would otherwise hand-roll: typed login/refresh/logout calls, a typed error
6
+ * taxonomy, and an auto-refreshing session that transparently keeps a short-lived access
7
+ * token fresh and follows refresh-token ROTATION (every refresh returns a new refresh token
8
+ * that must replace the old one).
9
+ */
10
+ import { createChapiClient } from './client';
11
+ function hasCode(err, ...codes) {
12
+ const code = err?.error?.code;
13
+ return typeof code === 'string' && codes.includes(code);
14
+ }
15
+ /** True for 401-class failures (missing/invalid/expired/revoked credentials). */
16
+ export function isUnauthorized(err) {
17
+ return hasCode(err, 'UNAUTHORIZED', 'INVALID_TOKEN', 'TOKEN_REVOKED');
18
+ }
19
+ /** True when authenticated but not permitted (403). */
20
+ export function isForbidden(err) {
21
+ return hasCode(err, 'FORBIDDEN', 'INSUFFICIENT_PERMISSIONS');
22
+ }
23
+ /** True when rate limited (429). */
24
+ export function isRateLimited(err) {
25
+ return hasCode(err, 'RATE_LIMIT_EXCEEDED');
26
+ }
27
+ /** True when the account is locked (423). */
28
+ export function isAccountLocked(err) {
29
+ return hasCode(err, 'ACCOUNT_LOCKED');
30
+ }
31
+ /** `POST /v1/auth/login`. Returns the openapi-fetch `{ data, error }` result. */
32
+ export function login(client, body) {
33
+ // Cast: the generated body type is exact; consumers pass username OR email + password.
34
+ return client.POST('/v1/auth/login', { body: body });
35
+ }
36
+ /** `POST /v1/auth/refresh`. Returns a NEW access token AND a NEW (rotated) refresh token. */
37
+ export function refresh(client, refreshToken) {
38
+ return client.POST('/v1/auth/refresh', { body: { refreshToken } });
39
+ }
40
+ /** `POST /v1/auth/logout`. Requires the client to be authenticated. */
41
+ export function logout(client) {
42
+ return client.POST('/v1/auth/logout', {});
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Auto-refreshing session
46
+ // ---------------------------------------------------------------------------
47
+ /** Decode a JWT's `exp` (seconds since epoch), or null if it can't be read. */
48
+ function jwtExp(token) {
49
+ try {
50
+ const payload = token.split('.')[1];
51
+ if (!payload)
52
+ return null;
53
+ const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
54
+ const exp = JSON.parse(json).exp;
55
+ return typeof exp === 'number' ? exp : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Create a session that keeps the access token fresh and follows refresh-token rotation.
63
+ *
64
+ * Pass `session.token` as the `token` option of `createChapiClient`. Before each request the
65
+ * provider checks the access token's `exp`; if it is expired (or within the skew window) it
66
+ * calls `/v1/auth/refresh`, stores the rotated tokens, and fires `onTokensChanged`.
67
+ *
68
+ * @example
69
+ * const session = createAuthSession({ baseUrl, tokens, onTokensChanged: save });
70
+ * const chapi = createChapiClient({ baseUrl, token: session.token });
71
+ */
72
+ export function createAuthSession(options) {
73
+ const skew = options.refreshSkewSeconds ?? 30;
74
+ let tokens = { ...options.tokens };
75
+ // A bare client used only to call /v1/auth/refresh (no token middleware → no recursion).
76
+ const refreshClient = createChapiClient({ baseUrl: options.baseUrl });
77
+ let inFlight = null;
78
+ async function doRefresh() {
79
+ if (!tokens.refreshToken) {
80
+ throw new Error('No refresh token available; the user must log in again.');
81
+ }
82
+ const { data, error } = await refresh(refreshClient, tokens.refreshToken);
83
+ if (error || !data) {
84
+ throw new Error('Refresh failed; the user must log in again.');
85
+ }
86
+ const next = data;
87
+ tokens = {
88
+ accessToken: next.accessToken,
89
+ // Rotation: prefer the new refresh token; fall back only if the server omitted it.
90
+ refreshToken: next.refreshToken ?? tokens.refreshToken,
91
+ sessionToken: tokens.sessionToken,
92
+ };
93
+ options.onTokensChanged?.({ ...tokens });
94
+ return tokens.accessToken;
95
+ }
96
+ function refreshNow() {
97
+ // Collapse concurrent refreshes so a burst of requests triggers exactly one refresh.
98
+ if (!inFlight) {
99
+ inFlight = doRefresh().finally(() => {
100
+ inFlight = null;
101
+ });
102
+ }
103
+ return inFlight;
104
+ }
105
+ async function token() {
106
+ const exp = jwtExp(tokens.accessToken);
107
+ const now = Math.floor(Date.now() / 1000);
108
+ if (exp !== null && exp - skew <= now) {
109
+ return refreshNow();
110
+ }
111
+ return tokens.accessToken;
112
+ }
113
+ return { token, getTokens: () => ({ ...tokens }), refreshNow };
114
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @cos/chapi-client — typed client for the CHAPI API.
3
+ *
4
+ * Thin wrapper over openapi-fetch: every path, method, query param, request
5
+ * body and response is typed from the committed `openapi.json` (regenerated
6
+ * into `types.ts`). No hand-maintained call signatures.
7
+ */
8
+ import { type ClientOptions, type Client } from 'openapi-fetch';
9
+ import type { paths } from './types';
10
+ export interface ChapiClientOptions extends ClientOptions {
11
+ /** API origin, e.g. https://api.example.com (no trailing slash needed). */
12
+ baseUrl: string;
13
+ /**
14
+ * Bearer token, or a function returning one (sync or async) so callers can
15
+ * refresh short-lived tokens per request. Attached as `Authorization: Bearer`.
16
+ */
17
+ token?: string | (() => string | Promise<string>);
18
+ }
19
+ export type ChapiClient = Client<paths>;
20
+ /**
21
+ * Create a typed CHAPI client.
22
+ *
23
+ * @example
24
+ * const chapi = createChapiClient({ baseUrl: 'https://api.example.com', token });
25
+ * const { data, error } = await chapi.GET('/v2/lots', {
26
+ * params: { query: { page: 1, limit: 25, sort: '-lotNumber' } },
27
+ * });
28
+ * if (error) throw new Error('request failed');
29
+ * data.data.forEach((lot) => { ... }); // fully typed
30
+ */
31
+ export declare function createChapiClient(options: ChapiClientOptions): ChapiClient;
package/dist/client.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @cos/chapi-client — typed client for the CHAPI API.
3
+ *
4
+ * Thin wrapper over openapi-fetch: every path, method, query param, request
5
+ * body and response is typed from the committed `openapi.json` (regenerated
6
+ * into `types.ts`). No hand-maintained call signatures.
7
+ */
8
+ import createClient from 'openapi-fetch';
9
+ /**
10
+ * Create a typed CHAPI client.
11
+ *
12
+ * @example
13
+ * const chapi = createChapiClient({ baseUrl: 'https://api.example.com', token });
14
+ * const { data, error } = await chapi.GET('/v2/lots', {
15
+ * params: { query: { page: 1, limit: 25, sort: '-lotNumber' } },
16
+ * });
17
+ * if (error) throw new Error('request failed');
18
+ * data.data.forEach((lot) => { ... }); // fully typed
19
+ */
20
+ export function createChapiClient(options) {
21
+ const { token, ...rest } = options;
22
+ const client = createClient(rest);
23
+ if (token) {
24
+ client.use({
25
+ async onRequest({ request }) {
26
+ const value = typeof token === 'function' ? await token() : token;
27
+ if (value)
28
+ request.headers.set('Authorization', `Bearer ${value}`);
29
+ return request;
30
+ },
31
+ });
32
+ }
33
+ return client;
34
+ }
@@ -0,0 +1,5 @@
1
+ export { createChapiClient } from './client';
2
+ export type { ChapiClient, ChapiClientOptions } from './client';
3
+ export type { paths, components, operations } from './types';
4
+ export { login, refresh, logout, createAuthSession, isUnauthorized, isForbidden, isRateLimited, isAccountLocked, } from './auth';
5
+ export type { AuthTokens, AuthSession, AuthSessionOptions, ChapiApiError } from './auth';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createChapiClient } from './client';
2
+ // Auth helpers: typed login/refresh/logout, error predicates, auto-refreshing session.
3
+ export { login, refresh, logout, createAuthSession, isUnauthorized, isForbidden, isRateLimited, isAccountLocked, } from './auth';