---
name: vaultsdk
description: Unified TypeScript SDK for secret managers. One interface to read a customer's credentials from their own Google Secret Manager, AWS Secrets Manager or Azure Key Vault, with reference recognition and caching. Use when an application must reference secrets that live in someone else's vault instead of storing them.
---

# VaultSDK

Read secrets from the vault their owner runs, storing only a reference. Three things and
no more: connection, discoverability, cache. Zero dependencies, WebCrypto throughout:
Node 18+, Cloudflare Workers, Deno, Bun, browser.

Repository: https://github.com/devicai/vault-sdk · License: Apache-2.0

## Install

```bash
npm install @devicai/vaultsdk   # self-contained; no peer dependencies, no vendor SDKs
```

## Quickstart

```ts
import { awsVault } from '@devicai/vaultsdk/aws';

// HOLD ON TO THIS. Credentials are parsed once and the access token is cached
// inside; a vault rebuilt per request mints a token per request. Since
// credentials are per customer, an instance is already per customer.
const vault = awsVault({ credentials: customerBlobJson, ttlMs: 5 * 60_000 });

const { value, version } = await vault.read(
  'arn:aws:secretsmanager:eu-west-1:123456789012:secret:stripe:api_key',
);
```

## API

```ts
// connection — one factory per vault, credentials bound in
googleVault({ credentials, ttlMs?, scope?, apiBase?, fetchImpl? })  // @devicai/vaultsdk/google
awsVault({ credentials, ttlMs?, scope?, apiBase?, fetchImpl? })     // @devicai/vaultsdk/aws
azureVault({ credentials, ttlMs?, scope?, apiBase?, loginBase?, fetchImpl? }) // …/azure

vault.read(reference)      // → { value, version? }   cached for ttlMs
vault.probe(reference)     // → { version?, elapsedMs, length, preview } — no cache, masked
vault.verify()             // → { identity?, scope? } — touches no secret
vault.list()               // → VaultSecretOption[] | null
vault.invalidate(ref?)     // forget one, or all
vault.provider             // 'aws' · vault.identity · vault.scope — no network call

// discoverability of a string — root import, no vault needed, no setup
import { parseReference, looksLikeReference, VaultError } from '@devicai/vaultsdk';
```

`scope` (project / region / vault URL) is taken from the credentials when they say, which
they usually do; pass it only to override.

## Credentials

`credentials` takes the blob the owner supplied, or the same fields as an object. Each vault
exports the type of that object — which is what to ask each customer for, and what to store
against them.

| Vault | Type | Fields |
| --- | --- | --- |
| Google Secret Manager | `GoogleCredentials` | `client_email`, `private_key`, `project_id?`, `token_uri?` |
| AWS Secrets Manager | `AwsCredentials` | `accessKeyId`, `secretAccessKey`, `region`, `sessionToken?` |
| Azure Key Vault | `AzureCredentials` | `tenantId`, `clientId`, `clientSecret`, `vaultUrl` |

```ts
import type { AwsCredentials } from '@devicai/vaultsdk/aws';

interface Customer { id: string; vault: AwsCredentials }   // your own column, typed
const vault = awsVault({ credentials: customer.vault });
```

Google's are snake_case because they are the downloaded key file's own field names:
`JSON.parse` it and it already satisfies the type.

**The two paths are not equally forgiving, on purpose.** A pasted *string* is whatever the
operator had on screen, so it is read tolerantly — `accessKeyId` or `AWS_ACCESS_KEY_ID`,
`clientId` or `appId`, and Azure takes the `az ad sp create-for-rbac` output verbatim. A typed
*object* is what a developer writes, so the type states one spelling. Validation is identical
either way: an object out of a database was never seen by the compiler, so a missing field
still raises `unauthenticated` at construction rather than at the first read.

## Reference forms (native, accepted verbatim)

| Vault | Form | Follows rotation |
| --- | --- | --- |
| Google Secret Manager | `projects/<project>/secrets/<name>[/versions/<version>]` | omit the version, or `latest` |
| AWS Secrets Manager | `arn:aws:secretsmanager:<region>:<account>:secret:<name>[:<json-key>[:<stage>[:<id>]]]` | omit stage and id (`AWSCURRENT`) |
| Azure Key Vault | `https://<vault>.vault.azure.net/secrets/<name>[/<version>]` | omit the version |

AWS's `<json-key>` is not optional in practice: the console stores secrets as key/value
by default, so without it you get the whole `{"api_key":"…"}` envelope.

The portal's Azure "Secret Identifier" **includes** the version — drop the last segment
or rotation stops being transparent.

## The rules that hold everywhere

- **Reads are routed by the reference.** Hand a vault one belonging to another and it
  raises `invalid_reference` rather than reading from the wrong place.
- **Recognition needs no setup.** `parseReference` / `looksLikeReference` know every shape
  the SDK supports, whether or not you imported that adapter — forgetting an import must
  not change what your application decides to encrypt at rest.
- **Never compose a reference yourself.** `list()` returns `{ name, reference }`; only the
  adapter knows its own native form.
- **`list()` returning `null` is normal**, not an error: listing needs a broader grant than
  reading. Degrade to a free-text field.
- **Nothing is kept beyond the TTL.** Holding a credential to survive an outage is a policy
  with consequences on the day it is revoked, so it belongs to your application, not here.

## Errors

`VaultError` with `.kind`: `unauthenticated` (reconnect) · `forbidden` (they grant the role) ·
`not_found` (typo or deleted) · `unavailable` (transient, `.retryable` is true) ·
`invalid_reference`. Check `.kind`, never the message.

## Extending it

```ts
interface VaultAdapter<Ref, Credentials> {
  id; name;
  credentialsFrom(raw: Credentials | string): VaultCredentials;  // and name the identity
  verify(source, scope?): Promise<VaultIdentity>;
  read(ref, source): Promise<VaultReadResult>;
  list(source, scope?): Promise<VaultSecretOption[] | null>;
}
export const myVault = (config) => createVault(new MyAdapter(config), config);
```

Credentials answer with **headers for a specific request**, not with a token
(`authorize(request) => headers`) — Google and Azure carry a bearer token, AWS signs each
call. Helpers in core: `authorizedFetch`, `bearerCredentials`, `headerTokenCredentials`,
`signingCredentials`, `cachedToken`, `sha256`, `hmacSha256`, `signRsaSha256`, base64.

**The shape rule, which is the dangerous part:** shapes live in `core/src/reference.ts` and
must only accept forms that carry their vendor inside them, anchored exactly — never a bare
name or path. Before widening one, sweep your database for values the new shape would
match: report shape, collection and field, never values.

## Testing against it

```ts
import { memoryVault } from '@devicai/vaultsdk';

const vault = memoryVault({ secrets: { 'stripe-key': 'sk-test-123' } });
vault.rotate('stripe-key', 'sk-test-456');  // a rotation
vault.setFailure('forbidden');              // a revocation → throws
vault.reads;                                // proves the cache really cached
```

References are `mem://<name>[@<version>]`.

## Cloud-side gotchas that cost real time

- **AWS**: the IAM policy must cover the six random characters AWS appends to the ARN
  (`…:secret:my-secret-*`); a secret named `test-2` is not covered by `devic/*`.
- **Azure**: creating a vault does not grant access to its contents — Owner is control
  plane, reading secrets is data plane. Three distinct roles: *Key Vault Secrets Officer*
  (the human), *Key Vault Secrets User* (the app, read-only), *Key Vault Reader* (optional,
  what makes listing work). RBAC propagation takes up to ~10 minutes, and the portal caches
  the roles from when you signed in.
- **Azure**: *Vault access policy* and *RBAC* are two incompatible permission models —
  mixing them grants nothing, silently.
