# WebAssembly components

WebAssembly breaks the usual ways of getting a secret into a program. A guest
cannot spawn a process, so there is nothing for [`seekrit run`](/docs/guides/run)
to wrap. It never opens a socket — outbound requests go through the host — so
there is nothing for the [proxy](/docs/guides/agent-proxy) to intercept. And the
one mechanism that does survive, an environment variable set by the host, puts
the plaintext in the host's configuration, which is the thing you were trying to
avoid.

The **seekrit secrets component** is the WASM-native answer. It is a WASI 0.2
component that holds the service token, calls seekrit once, and decrypts inside
its own linear memory. Your guest imports an interface and asks for a name:

```rust
use seekrit::secrets::store;

let api_key = store::get("STRIPE_API_KEY")?.expect("STRIPE_API_KEY is in scope");
```

No token in your code, no token in your bundle, and no plaintext anywhere in the
host.

> **Note:** This is the tightest boundary seekrit offers. `seekrit run` puts values in a child process's environment, where anything that can read `/proc` can read them. Here they exist only inside a sandbox the host cannot reach into, and only the guest that imported the interface can ask for one.

## The interface

```wit
package seekrit:secrets@0.1.0;

interface store {
  variant error { not-configured(string), denied(string), unavailable(string), decrypt(string) }

  get:        func(name: string) -> result<option<string>, error>;
  get-all:    func()             -> result<list<tuple<string, string>>, error>;
  list-names: func()             -> result<list<string>, error>;
  forget:     func();
}
```

The `@0.1.0` there is the *interface* version, and it moves only when the
interface itself changes — it is not the version of the artifact you pull, which
has its own release train. Compose against `seekrit:secrets/store@0.1.0`
whatever release you are on.

`get` returns `ok(none)` for a name that is simply not in scope, which is
different from an error — your guest can tell "no such secret" from "I could not
ask", and fail closed on the second without treating the first as an outage.

`list-names` returns names without materializing any value. Names are the half
of a secret seekrit treats as loggable, so this is the call for a health check
or a startup assertion that a required variable exists.

`forget` drops the decrypted set; the next call resolves again. Use it after a
rotation, or to shorten the window a value sits in memory in a long-lived
instance.

## Getting it

The component ships through two channels, because its two audiences reach for
different things. Same bytes either way.

**As an OCI artifact**, which is what Spin, wasmCloud, and `wkg` take. The same
component is published to both registries — use whichever your tooling already
authenticates to:

{/* x-release-please-start-version */}

```
ghcr.io/seekritdev/secrets:0.3.0
docker.io/seekritdev/secrets:0.3.0
```

{/* x-release-please-end */}

If you pull from CI, prefer `ghcr.io` — Docker Hub rate-limits anonymous pulls
per IP, and a shared CI runner will hit that ceiling long before you do.

**As a plain file**, which is what `wac` and a Dockerfile want:

```bash
curl -fsSLO https://wasm.seekrit.dev/latest/seekrit-secrets.wasm
```

{/* x-release-please-start-version */}

Everything is versioned (`:0.3.0` / `/v0.2.0/`) and carries a `latest`. The
`rc.1` and environment-only builds are the same reference with `-rc1` and
`-env`: `ghcr.io/seekritdev/secrets:latest-rc1`, or
`https://wasm.seekrit.dev/latest/seekrit-secrets-env.wasm`. The Spin build is a
package of its own — `ghcr.io/seekritdev/secrets-spin:0.3.0`, or
`https://wasm.seekrit.dev/latest/seekrit-secrets-spin.wasm` — so that a Spin
dependency can name it with an ordinary version constraint.

{/* x-release-please-end */}

The OCI artifact carries the component's own imports and exports in its
metadata, so the registry page shows that `seekrit:secrets/store` is the only
thing it exports — and which WASI version it needs.

## Composing it into your app

Link it into your own component with
[`wac`](https://github.com/bytecodealliance/wac) before you deploy:

```bash
wac plug my-app.wasm --plug seekrit-secrets.wasm -o composed.wasm
```

`composed.wasm` is what you deploy. It exports whatever your app exported; the
seekrit import is satisfied internally and is no longer visible from outside.

## Four builds, and picking the wrong one fails early

WASI hosts differ in what they provide, and an import a host cannot satisfy makes
a component **fail to load** rather than fail at a call site — with an error that
points at WASI rather than at seekrit. So there are four artifacts:

| File | Configuration arrives via | Use on |
| --- | --- | --- |
| `seekrit-secrets.wasm` | `wasi:config/store@0.2.0-draft`, falling back to the environment | wasmCloud 1.x |
| `seekrit-secrets-spin.wasm` | `wasi:config/store@0.2.0-draft-2024-09-27`, falling back to the environment | [Spin](/docs/guides/spin) 3.0+ |
| `seekrit-secrets-rc1.wasm` | `wasi:config/store@0.2.0-rc.1`, falling back to the environment | wasmCloud `wash-runtime` |
| `seekrit-secrets-env.wasm` | the environment only | wasmtime, Wasmer, NGINX Unit, anything else |

The first three are the same interface — same functions, same types — under
three package versions, because hosts pinned different snapshots of the same
WASI That is not a detail you can paper over: wasmtime gives pre-release versions no
semver-compatible matching, so those three are unrelated names to the linker
and none stands in for another.

If your app will not start with a message naming a `wasi:config` interface, read
the version in that message and pick the matching build. If the message names
`wasi:config` with no version at all, your host does not provide it: use `-env`.

Each artifact also declares a **WASI version**, recorded in the release notes.
On a wasmtime-based host (Spin, wasmCloud, wasmtime itself) any 0.2.x release
links it, since wasmtime matches 0.2 imports across the whole 0.2 track. A host
that matches import names exactly needs that version or newer.

## Configuration

Every key is accepted in three spellings — `SEEKRIT_TOKEN`, `seekrit_token`, and
`seekrit-token` — because hosts disagree about what a configuration key may
contain. Set whichever one your runtime allows.

| Key | Required | Meaning |
| --- | --- | --- |
| `SEEKRIT_TOKEN` | yes | A [service token](/docs/guides/service-tokens). |
| `SEEKRIT_API_URL` | no | Defaults to `https://api.seekrit.dev`. |
| `SEEKRIT_WITH` | no | `group:environment` composition overrides, comma-separated. |
| `SEEKRIT_BRANCH` | no | A [branch](/docs/guides/branches) of the token's environment. |
| `SEEKRIT_INTERPOLATE` | no | `0` leaves [secret references](/docs/guides/references) as literal text. |

> **Warning:** Never compile the token into the `.wasm`. Components are content-addressed and pushed to registries, so a token baked into one is a published credential — the same mistake as shipping a token in a browser bundle. It comes from the host, at load time, or not at all.

## Fermyon Spin

Spin has its own page: **[Fermyon Spin](/docs/guides/spin)**. It composes for
you, so you declare the component as a dependency rather than running `wac`:

{/* x-release-please-start-version */}

```toml
[component.app.dependencies]
"seekrit:secrets/store" = { version = "0.3.0", package = "seekritdev:secrets-spin", registry = "ghcr.io" }
```

{/* x-release-please-end */}

Use the `secrets-spin` package there, not `secrets` — Spin pins a different
snapshot of the `wasi:config` draft, and the two do not substitute for each
other. Three other manifest settings are load-bearing; the Spin page covers
them.

## wasmCloud

wasmCloud can use this component, but it is the one host where you probably want
the other integration instead. It publishes a pluggable **secrets API**, and
seekrit ships a backend for it — so `wash` and wadm know about your secrets, and
decryption still happens on your own lattice rather than in the seekrit API. See
the [wasmCloud guide](/docs/guides/wasmcloud).

To use the component anyway — when you want the value to exist only inside the
guest's sandbox, and are content for the platform not to know about it:

{/* x-release-please-start-version */}

Reference `ghcr.io/seekritdev/secrets:0.3.0` in your application manifest, and
link the component to a `wasi:config` provider and an outgoing-HTTP provider.
`seekrit_token` goes in the link configuration, alongside whatever else that
component needs.

On `wash-runtime`, which registers `wasi:config/store@0.3.0` rather than
the `0.3.0` that wasmCloud 1.x registers, use
`ghcr.io/seekritdev/secrets:0.3.0` instead.

{/* x-release-please-end */}

## wasmtime, Wasmer, NGINX Unit

Use `seekrit-secrets-env.wasm` and pass the token as an environment variable:

```bash
wasmtime serve -S cli -S http --env SEEKRIT_TOKEN="$SEEKRIT_TOKEN" composed.wasm
```

## One resolve per instance

The decrypted set is cached for the lifetime of a **component instance**. On a
host that instantiates per request — Spin does — that means one resolve per
request, and nothing survives the request. On a long-lived instance it means one
resolve total, however many times you call `get`.

Failures are not cached. An API that was briefly unreachable is retried on the
next call rather than remembered.

## What it does not do

It hands your guest a plaintext; where the guest sends it is the guest's
business. If you need the value never to be in the program at all — a
placeholder substituted at the network edge, against an allowlist — that is the
[credential broker](/docs/guides/agent-proxy), and it is a stronger boundary than
any in-process shim.

There is no write path. Like every [language SDK](/docs/guides/sdks), this is
read-only.
