
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
Typed client for the hlix API — auth, organization scoping, typed errors, SSE streaming, and bounded retry on idempotent verbs only.
@hlix/sdkThe authenticated hlix client. Typed end to end from the published API contract, with the things a caller should not have to reimplement: credentials, workspace selection, typed errors, Server-Sent Events, and bounded retry.
npm install @hlix/sdk
Requires Node.js 20 or newer. The only runtime dependency is openapi-fetch.
import { createHlix, apiKeyCredential, HlixNotFoundError } from "@hlix/sdk"
const hlix = createHlix({
baseUrl: "https://server.hlix.ai",
credential: apiKeyCredential(process.env.HLIX_API_KEY!),
organizationId: process.env.HLIX_WORKSPACE_ID,
})
const comments = await hlix.tasks.listComments(taskId)
try {
await hlix.tasks.get("does-not-exist")
} catch (error) {
if (error instanceof HlixNotFoundError) {
// 404 also means "shared with someone else" — see below.
}
}
Nothing in this package restates a request or response shape. Types are read out
of the generated paths in @hlix/api-client, which is generated from
apps/backend/openapi.json, which is emitted from the Zod validators the routes
actually run. One schema, from request validation to your editor.
A consequence worth knowing: responses the contract has not yet pinned come back
as unknown rather than as an invented interface. tasks.listComments() is
fully typed; projects.list() is not, because the contract does not yet
describe that body. That is deliberate — a fabricated type would be wrong the
first time a column changed.
Two credential kinds:
apiKeyCredential("hlix_…") // x-api-key
sessionCredential("session=…") // the better-auth session cookie
API keys are verified by the backend's API middleware and do not become browser
sessions. They also do not carry a default workspace, so pass organizationId
for every programmatic client.
organizationId selects the workspace (X-Organization-Id). Omit it and the
server falls back to the session's active workspace — fine for a browser, a trap
for a script that assumes it knows where it just wrote. The SDK never invents
one.
Every failure throws a typed error carrying status, the parsed body, and the
requestId when the server sent one.
| Class | Status |
|---|---|
HlixValidationError | 400 with a schema failure — exposes decoded issues |
HlixBadRequestError | 400 otherwise (e.g. no active workspace) |
HlixAuthenticationError | 401 |
HlixPermissionError | 403 |
HlixNotFoundError | 404 |
HlixConflictError | 409 |
HlixRateLimitError | 429 — exposes retryAfter |
HlixNotImplementedError | 501 |
HlixServerError | 5xx |
HlixTransportError | no response at all |
HlixValidationError.issues matters: the server sends its issue list as a JSON
string inside error.message. Printing that raw is how you end up with an
error message containing an escaped array. The SDK decodes it for you.
HlixNotFoundError does not mean "does not exist". The API answers 404 for
a resource the caller was never granted, so existence is not revealed.
Bounded exponential backoff with full jitter, honouring Retry-After.
A non-idempotent request is never retried. Not on 503, not on 429, not when
the transport failed and the SDK cannot know whether the server acted. POST
and PATCH are absent from the allowlist, and the allowlist is an allowlist —
a verb nobody considered is not retried. A retried task dispatch is a duplicated
coder run, which is exactly the hazard durable attempt identity exists to
eliminate.
createHlix({ …, retry: { attempts: 1 } }) // off
createHlix({ …, retry: { attempts: 5, baseDelayMs: 100 } })
Retried statuses are 429, 500, 502, 503 and 504. 501 is excluded: the contract
documents it as a permanent refusal, so a retry only buys latency.
const controller = new AbortController()
for await (const event of hlix.tasks.stream(taskId, { signal: controller.signal })) {
console.log(event.event, JSON.parse(event.data))
}
stream(path) consumes any text/event-stream route. It is not routed through
the typed client — that parses a body, and a stream must be read as it arrives —
and retry does not apply, because a stream that drops has already delivered
events and restarting it would replay them.
The parser follows the WHATWG spec, which means an event whose data buffer is
empty is not dispatched. The backend's event: keepalive frames carry no
data, so they never surface. They still keep the connection warm; if you need a
liveness signal, use a timeout rather than waiting for a keepalive.
hlix.raw is the untouched @hlix/api-client transport for anything this
facade does not wrap — same types, no retry, no throwing, { data, error }.
bun run --filter '@hlix/sdk' build # dist/index.js + a self-contained dist/index.d.ts
bun run --filter '@hlix/sdk' test
@hlix/api-client is a private workspace package, so it is bundled into both
outputs. The only external dependency in the shipped bundle is openapi-fetch.
FAQs
Typed client for the hlix API — auth, organization scoping, typed errors, SSE streaming, and bounded retry on idempotent verbs only.
We found that @hlix/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.