# OAuth storage stdlib

> std/oauth/storage is the token-store abstraction shared by the OAuth client ( OAuth.client(...) , RFC 6749 + 7636, RFC 8628 device flow). One storage handle backs every grant...

Website: https://harnlang.com/stdlib/oauth-storage.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

`std/oauth/storage` is the token-store abstraction shared by the OAuth
client (`OAuth.client(...)`, RFC 6749 + 7636, RFC 8628 device flow). One
storage handle backs every grant type, every refresh, and every revoke
the client performs. A handle is just a dict with storage closures —
`get`, `set`, `delete`, and `with_refresh_lock` — so the client never
needs to know whether a token lives in process memory, on disk, in a cloud
platform, or in a vault.

The five backends are:

| Constructor | Backing | Persists past `harn run`? | Best for |
|---|---|---|---|
| `memory()` | per-VM `BTreeMap` | no | short-lived agents, ephemeral scripts |
| `file(path, encryption_key)` | one AES-256-GCM-sealed file | yes | local development, single-host operators |
| `harn_cloud_session()` | host capability, per-session | yes (cloud) | a single user's cloud-managed agent |
| `harn_cloud_org()` | host capability, org-scoped | yes (cloud) | shared org credentials ("the org's GitHub bot") |
| `custom(handlers)` | caller-supplied closures | depends | vaults, KMS-backed key/value stores |

The shape of a stored entry is a `TokenSet`:

```harn
type TokenSet = {
  access_token: string,
  refresh_token?: string,
  expires_at_unix?: int,
  token_type?: string,
  scopes?: list<string>,
  metadata?: dict,
}
```

## Calling the API

```harn
import { memory } from "std/oauth/storage"

pipeline default(harness: Harness) {
  const store = memory()
  store.set(
    "github",
    {
      access_token: "abc",
      refresh_token: "rfr",
      expires_at_unix: 17000000000,
    },
    3600,
  )
  const token = store.get("github")              // -> TokenSet | nil
  if token != nil {
    harness.stdio.log("auth: Bearer " + token.access_token)
  }
  store.delete("github")
}
```

The OAuth client takes a `storage` option that accepts any of these
handles unchanged:

```harn
import { Providers } from "std/oauth/providers"
import { harn_cloud_org } from "std/oauth/storage"

const client = OAuth.client(Providers.github, {
  client_id: harness.env.get("GITHUB_OAUTH_CLIENT_ID"),
  scopes: ["repo"],
  storage: harn_cloud_org(),
})
```

`storage()` returns the namespace dict matching the OAuth epic's
`OAuth.Storage.*` shape, with `memory` / `harn_cloud_*` as ready
handles and `file` / `custom` as factory closures.

## File backend

`file(path, encryption_key)` writes a single envelope to `path`:

```json
{ "version": 1, "nonce": "<base64>", "ciphertext": "<base64>" }
```

The 32-byte AES-256-GCM key is derived from `encryption_key` via
HKDF-SHA256 (`info = "harn-oauth-storage-v1"`). Pass high-entropy bytes
or a string sourced from a KMS or `crypto.random_bytes(32)` — not a
user passphrase. A fresh random nonce is sampled on every write, and
the file is renamed into place from a sibling `.tmp` so partial writes
never corrupt existing tokens.

Decryption with the wrong key raises an error rather than returning a
plausible-looking but wrong TokenSet. Deleting the final entry removes
the file entirely so an `ls` on the storage directory reflects the
absence of tokens.

## Cloud backends

`harn_cloud_session()` and `harn_cloud_org()` route every call through
the `oauth_storage` host capability:

| Operation | Params | Returns |
|---|---|---|
| `oauth_storage.cloud_get` | `{scope, key}` | TokenSet or nil |
| `oauth_storage.cloud_set` | `{scope, key, token, ttl_seconds?}` | nil |
| `oauth_storage.cloud_delete` | `{scope, key}` | nil |
| `oauth_storage.cloud_acquire_refresh_lock` | `{scope, key, timeout_seconds}` | lock lease |
| `oauth_storage.cloud_release_refresh_lock` | `{scope, key, lock}` | nil |

`scope` is `"session"` for `harn_cloud_session()` and `"org"` for
`harn_cloud_org()`. The cloud-platform embedder is responsible for
tenant-scoped storage (RLS), refresh metadata, and a backend-native lock
lease so rotating refresh tokens are single-flight across every worker
sharing the same cloud storage key.

Tests substitute the host on the exact harness with
`harness.testing.respond("oauth_storage", "cloud_get", ...)` and matching
refresh-lock responses. With no embedder or fixture, a missing
`oauth_storage.cloud_*` operation raises a deterministic "not configured"
signal.

## Custom backend hook protocol

`custom(handlers)` lets callers plug in a vault, KMS, or
platform-native keychain. `handlers` is a dict with these keys:

```harn
import { custom } from "std/oauth/storage"

fn vault_get(_path) {
  return nil
}

fn vault_put(_path, _token_set, _options) {
  return nil
}

fn vault_delete(_path) {
  return nil
}

fn vault_with_lock(_path, body) {
  return body()
}

const store = custom({
  get: { key -> vault_get("oauth/" + key) },
  set: { key, token_set, ttl_seconds = nil ->
    vault_put("oauth/" + key, token_set, {ttl: ttl_seconds})
  },
  delete: { key -> vault_delete("oauth/" + key) },
  with_refresh_lock: { key, body ->
    vault_with_lock("oauth/" + key, body)
  },
  id: "my-vault",          // optional, surfaces in diagnostics
})

if store.id != "my-vault" {
  throw "custom backend id mismatch"
}
```

Contract for each closure:

| Closure | Signature | Returns |
|---|---|---|
| `get` | `fn(key: string) -> TokenSet \| nil` | The stored TokenSet, or `nil` if absent. **Must not raise** for a missing key. |
| `set` | `fn(key: string, token_set: TokenSet, ttl_seconds: int \| nil) -> nil` | Persists the TokenSet. `ttl_seconds` is a hint; backends may ignore it. |
| `delete` | `fn(key: string) -> nil` | Idempotent removal. **Must not raise** for a missing key. |
| `with_refresh_lock` | `fn(key: string, body: fn() -> any) -> any` | Optional. Runs `body()` while holding a backend-native lock for `key`. |

`custom` validates that required closures are functions before returning
the handle. When `with_refresh_lock` is omitted, Harn supplies an
in-process lock for the custom handle id. Durable backends should provide
their own hook so refresh-token rotation is single-flight across every
worker that shares the backend. The optional `id` field defaults to
`"custom"` and is exposed as `store.id` so diagnostics can distinguish
multiple custom backends.

Closure capture is by value in Harn, so the closures cannot mutate
outer scope to track state. Delegate to a real backend (a cloud platform,
the file backend, an HTTP API, an MCP tool) inside the closures
instead.

## Acceptance criteria mapping

This module satisfies the OA-03 acceptance criteria from issue #1904:

* Five backends implemented (`memory`, `file`, `harn_cloud_session`,
  `harn_cloud_org`, `custom`).
* File backend encrypts at rest with AES-256-GCM.
* Cloud backends route through `oauth_storage.cloud_*` host
  capabilities so embedders enforce RLS / tenant isolation and
  refresh-token single-flight.
* Custom backend hook protocol documented above.
* Conformance per backend (store / retrieve / refresh / delete) lives
  in `conformance/tests/stdlib/oauth/oauth_storage.harn`.

---

## Read next

- [Diff stdlib](https://harnlang.com/stdlib/diff.md)
- [Prompt library stdlib](https://harnlang.com/stdlib/prompt-library.md)
