# TypeScript SDK

> Install, configure and use the artefaktum npm package: push, pull, search and resolve artifacts

## Install

```sh
npm install artefaktum
```

Zero runtime dependencies, Node 20+, ESM and CommonJS builds. Types are generated from the API's own OpenAPI document, so a server-side rename is a compile error, not a runtime surprise.

```ts
import { Artefaktum } from "artefaktum";
// CommonJS: const { Artefaktum } = require("artefaktum");
```

## Configure

Create an API key in the [console](/console/).

| Option | Environment | Default |
|---|---|---|
| `apiKey` | `ARTEFAKTUM_API_KEY` | required; throws `MissingApiKeyError` if missing |
| `baseUrl` | `ARTEFAKTUM_BASE_URL` | `https://api.artefaktum.dev` |
| `project` | `ARTEFAKTUM_PROJECT` | `"default"` |
| `timeoutMs` | none | `30000` per API call; storage transfers have no timeout |
| `fetch` | none | `globalThis.fetch` |

`project` accepts a UUID or slug; a slug is resolved once and cached. Every project-scoped method also takes `project` to override it for one call.

Methods are camelCase; fields are snake_case, spelled as the REST API, MCP tools and Python SDK spell them (`external_key`, `latest_version.size_bytes`, `stale_upstream`). Timestamps accept an ISO 8601 string or a `Date`; durations name their unit (`timeoutMs`, `max_age_seconds`).

## Example

```ts
import { Artefaktum } from "artefaktum";

const client = new Artefaktum(); // reads ARTEFAKTUM_API_KEY; project "default"

const artifact = await client.artifacts.push("report.pdf", {
  title: "Q3 churn analysis",
  tags: ["churn", "q3"],
  external_key: "reports/q3-churn",
});

const page = await client.artifacts.search("q3 churn");
for (const hit of page.items) console.log(hit.score, hit.artifact.title);

await client.artifacts.pull(artifact.id, "out/"); // → out/report.pdf, sha256-verified
```

## Uploading

`push(source, options)` hashes the source, reserves an upload, PUTs the bytes directly to object storage (your API key never reaches that host), completes it, and by default waits until the artifact is `ready`.

```ts
await client.artifacts.push("data/export.parquet", { title: "Export" });          // path (Node): streamed
await client.artifacts.push(bytes, { title: "Export", filename: "export.json" }); // Uint8Array | ArrayBuffer | Blob
```

`source` is a file path (Node only) or bytes. Bytes need `filename` unless the `Blob` is a named `File`; `content_type` defaults to the Blob's type, else it's guessed from the name.

| Option | Meaning |
|---|---|
| `title` | Required. What the artifact is. |
| `description`, `tags`, `metadata` | Description, tag list, arbitrary JSON metadata. |
| `external_key` | Your own unique key for this artifact within the project. |
| `expires_at` | ISO string or `Date`; the artifact is deleted after this time. |
| `run`, `infer_lineage` | Link to a run id; `infer_lineage` (default `true`) links what the run read. |
| `wait`, `timeout_ms`, `signal` | Wait for `ready` (default `true`, 30 000 ms); `signal` cancels the transfer. |

Field sizes are limited; see [Limits](/docs/rest/#limits).

Storage transfers have no timeout of their own, since a total timeout would kill a large upload. Pass `signal` for a bound, e.g. `AbortSignal.timeout(10 * 60_000)`.

A failed PUT throws `StorageError` and leaves the artifact `pending_upload`, never `ready` and invisible to `search`, so it is safe to retry. With `wait: true`, a processing failure throws `ProcessingFailedError` or `ProcessingTimeoutError`, both carrying `.artifact`.

`createVersion(artifact_id, source, options)` adds a version through the same flow (`run`, `infer_lineage`, `content_type`, `filename`, plus the wait options).

### Compute once, reuse everywhere

`resolve` and `fulfil` cache an expensive computation under an `external_key`, so a later caller gets the stored result instead of recomputing it.

```ts
const r = await client.artifacts.resolve("openweather/vilnius/2026-09-21", {
  filename: "weather.json", content_type: "application/json", size_bytes: body.byteLength,
  title: "Vilnius weather", max_age_seconds: 3600,
});
if (r.status === "hit" && r.artifact) use(r.artifact);
else if (r.status === "create") await client.artifacts.fulfil(r, body);
else await sleep((r.retry_after_seconds ?? 2) * 1000); // "pending": someone else is producing it
```

`resolve(external_key, options)` requires `filename`, `content_type`, `size_bytes`, `title`, and takes `description`, `tags`, `metadata`, `max_age_seconds`, `run`. `status` is `"hit"` (fresh artifact in `.artifact`), `"create"` (call `fulfil` with this resolution and the bytes), or `"pending"` (someone else is producing it; back off using `.retry_after_seconds`). `fulfil` throws `TypeError` unless `status` is `"create"`.

## Downloading

```ts
const path = await client.artifacts.pull(id, "out/");            // Node: streams to disk, verifies sha256
const { data, filename } = await client.artifacts.pullBytes(id); // any runtime: verified bytes in memory
```

`pull` is Node-only; `dest` is a directory when it exists as one or ends with a separator (then created), otherwise the file to write. `pullBytes` works on any runtime, returning `{ data, filename, content_type, version_id, sha256 }`. Both take `{ verify?, signal? }`; `verify` defaults to `true` and a mismatch throws `IntegrityError`, leaving no file behind.

`downloadUrl(artifact_id, { version_id?, run? })` returns the signed URL directly.

## Finding

```ts
await client.artifacts.search("emission factors", { mode: "hybrid", tags_all: ["ghg"] });
await client.artifacts.getByExternalKey("reports/q3-churn");
for await (const a of client.artifacts.iterAll({ tag: "churn" })) console.log(a.title);
```

| Method | Purpose |
|---|---|
| `search(query, options)` | Ranked results. `mode`: `"hybrid"` (default), `"semantic"`, `"text"`, `"exact"`. Also `content_types`, `tags_all`, `status`, `external_key`, `created_after`/`before`, `exclude_superseded`, `limit` (20), `cursor`, `project`. Returns `{ items: [{ score, artifact }], next_cursor }`. |
| `get(artifact_id)` | Fetch one artifact by id. |
| `getByExternalKey(key, { project? })` | Fetch by external key. |
| `list(options)` | One page: `status`, `content_type`, `tag`, `external_key`, `created_before`/`after`, `expires_before`, `limit` (50), `cursor`. |
| `iterAll(options)` | Async generator over every match, following `next_cursor`. |

Search hides superseded artifacts by default (`exclude_superseded`); each result reports whether it is `superseded` or `stale_upstream`.

## Versions and relations

```ts
await client.artifacts.versions(artifact_id);
await client.artifacts.relations(artifact_id);
await client.artifacts.addRelation(artifact_id, to_artifact_id, "derived_from");
```

`versions` lists an artifact's versions; `relations` lists its relations to other artifacts. `addRelation(artifact_id, to_artifact_id, relation_type, { to_version_id?, metadata? })` requires `relation_type` to be one of `derived_from`, `supersedes`, `attachment_of`, `generated_by`, `related_to`; anything else throws `TypeError` first.

`update(artifact_id, options)` patches `title`, `description`, `tags`, `metadata`, `expires_at`; only fields passed are sent, and `clear_expires_at: true` removes the expiry. `delete(artifact_id)` is accepted with 202; bytes are removed in a background job.

Other namespaces: `client.projects.list()`; `client.runs.create`/`.seal`/`.artifacts` for grouping by pipeline run; `client.keys`, `client.usage` (admin scope); `client.whoami()`, `client.quota()`.

## Errors

Every failure from the API, storage, or the SDK's own helpers is an `ArtefaktumError` with `code`, `message`, `status`, `request_id` (quote it in support mail). `MissingApiKeyError` extends plain `Error`. Caller mistakes the compiler can't catch in JavaScript throw `TypeError`: `fulfil` on a non-`create` resolution, an unknown `relation_type`, nameless bytes without `filename`, an invalid `baseUrl`.

| Class | `code` |
|---|---|
| `NotFoundError` | `artifact_not_found`, `run_not_found`, `not_found`, `project_not_found` |
| `UnauthorizedError` / `ForbiddenError` | `unauthorized` / `insufficient_scope` |
| `QuotaExceededError` | `quota_exceeded` |
| `RequestTooLargeError` | `request_too_large` |
| `ConflictError` | `artifact_not_ready`, `external_key_conflict`, `idempotency_conflict`, `run_sealed` |
| `ValidationError` | `invalid_request` |
| `UploadError` | `upload_expired`, `object_verification_failed` |
| `ServiceUnavailableError` | `embedding_unavailable` |
| `ConnectionError` | `connection_error`: API unreachable after retries |
| `StorageError` | `storage_error`: object storage refused or was unreachable |
| `ProcessingFailedError` / `ProcessingTimeoutError` | `processing_failed` / `processing_timeout` (both carry `.artifact`) |
| `IntegrityError` | `integrity_error`: downloaded bytes don't match the recorded sha256 |
| `ArtefaktumError` | `version_mismatch`: the artifact gained a version mid-pull; retry |
| `ArtefaktumError` | `invalid_filename`, `unsupported_runtime` (a file path on a runtime without `fs`), `http_error` (non-JSON response) |

```ts
try {
  await client.artifacts.get(id);
} catch (err) {
  if (err instanceof ArtefaktumError && err.code === "artifact_not_found") { /* … */ }
  else throw err;
}
```

Prefer `err.code` to `instanceof` in libraries: a program loading both the ESM and CommonJS build sees distinct classes.

Reads (every `GET`, and `search`) retry up to twice on `429`/`502`/`503`/`504` and network failures, honouring `Retry-After` up to a 10 s ceiling. Writes, storage transfers and spent quota are never retried.

## Runtimes

Tested on Node 20 and 22. The core uses only `fetch` and Web Crypto, so it should also run on Bun, Deno and Cloudflare Workers. Pass bytes or a `Blob` to `push` and use `pullBytes` there, since file paths need Node's `fs`. Browsers are not supported: an API key does not belong in one.

See also [Quickstart](/docs/quickstart/) and [REST](/docs/rest/).
