Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

+243
View File
@@ -0,0 +1,243 @@
# TypeScript SDK — `@toolctrl/api-client-ts`
The official TypeScript SDK for the toolctrl boilerplate REST API. Pairs a
`@hey-api/openapi-ts`-generated typed client with a hand-written Higher-Level
wrapper that handles auth, retry, idempotency, pagination, and webhook
signature verification.
- Source: `packages/api-client-ts/`
- Types-only sibling: `packages/api-types/` (`@toolctrl/api-types`)
- Spec: `docs/improvement-plan/08-mobile-cli-storybook-sdk/ts-sdk.md`
## Installation
The SDK is currently a workspace package. Add it as a peer / dependency in any
consumer inside this monorepo:
```jsonc
{
"dependencies": {
"@toolctrl/api-client-ts": "workspace:*"
}
}
```
After the SDK is published to the internal registry it becomes:
```bash
npm install @toolctrl/api-client-ts @toolctrl/api-types
```
The package targets Node 18+ and modern browsers (uses native `fetch` and
`crypto.randomUUID`).
## Quickstart
```ts
import { createToolctrlClient } from "@toolctrl/api-client-ts";
const client = createToolctrlClient({
baseUrl: "https://api.toolctrl.de",
auth: { apiKey: process.env.TOOLCTRL_API_KEY! },
tenantId: process.env.TOOLCTRL_TENANT_ID,
});
// Typed REST call — falls back to `unknown` if T is omitted.
const me = await client.request<{ id: string; email: string }>({
method: "GET",
path: "/v1/session/current-user",
});
```
## Authentication
Three modes are supported via the `auth` field.
```ts
// Bearer token — `Authorization: Bearer <token>` on every call.
createToolctrlClient({ baseUrl, auth: { bearerToken: jwt } });
// API key — sent as `X-API-Key: <key>`.
createToolctrlClient({ baseUrl, auth: { apiKey: process.env.TOOLCTRL_API_KEY! } });
// Async getter — used for refresh-aware JWT flows. Resolved before every
// outgoing request; the result is cached until `client.refreshAuth()` is
// called or the getter returns a new value.
createToolctrlClient({
baseUrl,
auth: { getToken: () => loadTokenFromKeychain() },
});
```
Refresh the cached bearer manually if the consumer detects token expiry
between requests:
```ts
await client.refreshAuth();
```
Token logging: the SDK never logs the secret material — only error responses
including the `x-request-id` for correlation.
## Retry semantics
By default the client retries on `408`, `425`, `429`, `500`, `502`, `503`,
`504`, and network errors with exponential backoff (base = 1s, max = 30s,
±250ms jitter). `Retry-After` headers — both seconds and HTTP-date forms —
are honored verbatim and capped at the configured `maxDelayMs`.
```ts
createToolctrlClient({
baseUrl,
retry: {
maxAttempts: 5, // default 5 (initial request counts as attempt 1)
baseDelayMs: 1000, // default 1000
maxDelayMs: 30000, // default 30000
},
});
```
Non-retryable 4xx responses surface as `ToolctrlApiError`:
```ts
import { ToolctrlApiError } from "@toolctrl/api-client-ts";
try {
await client.request({ method: "GET", path: "/v1/missing" });
} catch (err) {
if (err instanceof ToolctrlApiError) {
console.error(err.status, err.requestId, err.body);
}
}
```
Exhausted retries throw `ToolctrlNetworkError` with the underlying `cause`.
## Idempotency
POST / PUT / PATCH requests automatically receive an `Idempotency-Key` header
generated via `crypto.randomUUID()`. Callers can override with a deterministic
key or opt out entirely.
```ts
// Caller-supplied key — useful for retrying the same logical write.
await client.request({
method: "POST",
path: "/v1/orders",
body: { sku: "x" },
idempotencyKey: "order_2026_05_17_abc",
});
// Disable auto-generation globally.
createToolctrlClient({ baseUrl, idempotency: { autoGenerate: false } });
```
GET / HEAD / DELETE requests never receive an auto-generated key.
## Pagination
The async iterator `client.paginate<T>(path, opts)` follows whichever pagination
style the upstream endpoint emits:
- `nextCursor` (opaque cursor)
- `nextPageToken` (cursor under a different name)
- `nextPage` (offset)
```ts
for await (const contact of client.paginate<{ id: string; email: string }>("/v1/contacts", {
query: { search: "@acme.com" },
})) {
console.log(contact.id, contact.email);
}
```
A safety stop (`maxPages`, default 1000) prevents runaway iteration when an
endpoint never emits a terminating null cursor.
## Webhook signature verification
Server-side webhook handlers should `verifyWebhookSignature` before reading any
payload data. The helper uses HMAC-SHA256 plus constant-time compare and
applies a 5-minute replay window.
```ts
import { verifyWebhookSignature } from "@toolctrl/api-client-ts";
export async function action({ request }: Route.ActionArgs) {
const signature = request.headers.get("X-Toolctrl-Signature");
const timestamp = request.headers.get("X-Toolctrl-Timestamp");
const payload = await request.text();
if (
!signature ||
!timestamp ||
!verifyWebhookSignature({
secret: process.env.TOOLCTRL_WEBHOOK_SECRET!,
payload,
signature,
timestamp,
})
) {
return new Response("forbidden", { status: 403 });
}
// ...handle the event
}
```
The companion `signWebhookPayload` is exported for tests and webhook senders
that produce signatures in JS land:
```ts
import { signWebhookPayload } from "@toolctrl/api-client-ts";
const signature = signWebhookPayload({
secret: process.env.TOOLCTRL_WEBHOOK_SECRET!,
payload,
timestamp: Math.floor(Date.now() / 1000),
});
```
## Sentry trace propagation
If the calling context provides `sentry-trace` and/or `baggage` headers, pass
them through via `sentry`:
```ts
createToolctrlClient({
baseUrl,
sentry: {
trace: getActiveSentryTrace(),
baggage: getSentryBaggage(),
},
});
```
The SDK forwards both headers verbatim, enabling end-to-end Sentry trace
linking between consumer and toolctrl API.
## Regenerating the client
When the upstream API changes, regenerate locally:
```bash
pnpm --filter @toolctrl/api-client-ts generate-spec
pnpm --filter @toolctrl/api-client-ts generate
# Or both in one shot:
pnpm --filter @toolctrl/api-client-ts generate:all
```
The CI workflow `.github/workflows/sdk-codegen.yml` regenerates on every PR
that touches `apps/api/src/openapi.ts` or the SDK package and fails the merge
if the committed `src/generated/` tree drifts from the captured OpenAPI
document.
## Error model
| Class | When it fires | Notable fields |
| ---------------------- | ------------------------------------------- | --------------------------------------------- |
| `ToolctrlApiError` | API returned a non-retryable HTTP error | `status`, `body`, `requestId` (`x-request-id`) |
| `ToolctrlNetworkError` | Network failure after all retries exhausted | `cause` (the underlying transport error) |
Both inherit from the built-in `Error` so existing `instanceof Error` checks
continue to work.