# TypeSafe

TypeSafe's [Jev](https://docs.typesafe.ai/introduction) model answers typed
questions instead of returning text. It has one credential, `TYPESAFE_API_KEY`,
sent as `Authorization: Bearer` to one endpoint,
`POST https://api.typesafe.ai/v1/systemone`. Both SDKs read the key from the
environment by default and take it as a constructor argument when you want to
override that.

> **Note:** **Start here if seekrit is new:** [three commands](/docs/guides/frameworks#get-running-in-five-minutes) put the keys in an environment, mint a token bound to it, and export `SEEKRIT_TOKEN`. A token reads [everything in its environment](/docs/guides/frameworks#which-secrets-does-the-agent-get), so there is nothing per-key to set up first.

## 1. Wrap the process

```bash
seekrit secrets set TYPESAFE_API_KEY ts-… --app storefront --env development

seekrit run -- python classify.py
seekrit run -- node classify.mjs
```

Nothing changes in the app. `TypeSafeClient()` with no arguments picks up the
injected `TYPESAFE_API_KEY`.

The same applies to the SDKs' other environment variables — `TYPESAFE_BASE_URL`,
`TYPESAFE_DEFAULT_MODEL`, `TYPESAFE_LOG_LEVEL`. None of those is a secret, so
keep them in your compose file or Procfile rather than in seekrit.

For containers and CI, [`seekrit-run`](/docs/guides/run) does the same thing as a
static binary with no Node dependency.

## 2. Resolve in code

Use this where there is no process to wrap: a serverless handler, a Worker, a
notebook.

```python
import seekrit
from typesafe_sdk import TypeSafeClient

secrets = seekrit.Client().resolve()
client = TypeSafeClient(api_key=secrets["TYPESAFE_API_KEY"])
```

```ts
import { TypeSafeClient } from "@typesafe-ai/sdk";
import { Seekrit } from "@seekrit/sdk";

const secrets = await new Seekrit().resolve();
const client = new TypeSafeClient({ apiKey: secrets.TYPESAFE_API_KEY });
```

Resolve once per handler, not once per question. `resolve()` is a network round
trip and a decrypt.

## 3. Hold a placeholder, not a key

Your code holds `{{seekrit:TYPESAFE_API_KEY}}`; the value is substituted into the
outbound request and exists nowhere else. Both SDKs have a seam for this, so
neither needs the proxy running.

### TypeScript

`TypeSafeClientConfig` takes a `fetch`, which is all the substitution needs:

```ts
import { TypeSafeClient, noul } from "@typesafe-ai/sdk";
import { seekritFetch } from "@seekrit/sdk/fetch";

const client = new TypeSafeClient({
  apiKey: "{{seekrit:TYPESAFE_API_KEY}}",
  fetch: seekritFetch({
    allow: { "api.typesafe.ai": ["TYPESAFE_API_KEY"] },
  }),
});

const response = await client.systemOne({
  state: { document: ticket.body },
  questions: { urgency: noul("Does this express urgency?") },
});
```

### Python

The TypeSafe Python SDK is built on
[httpx2](https://github.com/pydantic/httpx2), not `httpx`, so it takes
`seekrit.transport_httpx2` rather than `seekrit.transport`:

```bash
pip install 'seekrit[httpx2]'
```

```python
from seekrit.transport_httpx2 import SeekritTransport
from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient(
    api_key="{{seekrit:TYPESAFE_API_KEY}}",
    transport=SeekritTransport(allow={"api.typesafe.ai": ["TYPESAFE_API_KEY"]}),
)

response = client.system_one(
    state=ticket.body,
    questions={"urgency": Noul(instructions="Does this express urgency?")},
)
```

Use `AsyncSeekritTransport` with `AsyncTypeSafeClient`. The arguments are
identical to the `httpx` transport's, and a test in the SDK fails if the two
drift apart, so everything in
[in-process injection](/docs/guides/agent-proxy/in-process) applies unchanged:
the allowlist is default-deny, a denied or unresolved name is refused with a 403
and never sent, and `scope` narrows per request.

Both shims run in your process, which makes them a weaker boundary than the
[egress proxy](/docs/guides/agent-proxy). That page sets out the trade-off.

## Gotchas

- **The Python SDK will not accept `seekrit.transport`.** `httpx2` is a separate
  distribution, not a newer release of `httpx`, so `httpx2.BaseTransport` and
  `httpx.BaseTransport` are unrelated classes. Passing the wrong one raises a
  type error from `TypeSafeClient`, naming neither package usefully. Use
  `seekrit.transport_httpx2` here and `seekrit.transport` for clients built on
  `httpx`; both can be imported in the same process.

- **Do not call `httpx2.alias_httpx()` to paper over that.** It rebinds
  `import httpx` for the whole process, and httpx2's own docs say libraries must
  never do it. Pick the module that matches the client you are configuring.

- **`state` is the request body, and the shim does not read it for you.**
  Substitution gates placeholders; it has no opinion about content. A credential
  that some earlier step put into the document you are classifying is sent to
  TypeSafe like any other text. That is true of every inference API, and it is
  the case for keeping credentials out of the process at all — see
  [the ladder](/docs/guides/agent-proxy/in-process#what-this-does-and-does-not-guarantee).

- **`dangerouslyAllowBrowser` does not make a service token safe to ship.** The
  JS SDK exposes the option; a `skt_` token in a client bundle is a published
  credential. Resolve server-side.

- **One host, one path.** If you constrain the allowlist beyond the host,
  `POST /v1/systemone` is the whole surface the SDKs use. That makes this a
  narrower rule than most:

  ```python
  from seekrit.transport_httpx2 import AllowRule, SeekritTransport

  SeekritTransport(rules=[AllowRule(
      host="api.typesafe.ai",
      methods=("POST",),
      paths=("/v1/systemone",),
      allow=("TYPESAFE_API_KEY",),
  )])
  ```
