seekrit
Docs/TypeSafe

TypeSafe

TypeSafe's Jev 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 put the keys in an environment, mint a token bound to it, and export SEEKRIT_TOKEN. A token reads everything in its environment, so there is nothing per-key to set up first.

1. Wrap the process

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 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.

import seekrit
from typesafe_sdk import TypeSafeClient

secrets = seekrit.Client().resolve()
client = TypeSafeClient(api_key=secrets["TYPESAFE_API_KEY"])
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:

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, not httpx, so it takes seekrit.transport_httpx2 rather than seekrit.transport:

pip install 'seekrit[httpx2]'
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 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. 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.

  • 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:

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