Open source · Apache-2.0 · Zero dependencies

Their secrets stay theirs.
You store an address.

Read a customer's credentials straight from the secret manager they already run. They rotate on their own schedule and it just works; they revoke you and you stop.

Get started
$ npm install @devicai/vaultsdk
Works with — click to see the code
quickstart-aws.ts
import { awsVault } from '@devicai/vaultsdk/aws';

// the customer's own credentials, in the vault's own format
const vault = awsVault({
  credentials: customer.vaultCredentials,
});

// they paste the reference their tooling already gives them
const { value } = await vault.read(
  'arn:aws:secretsmanager:eu-west-1:123456789012:secret:stripe:api_key',
);

// and to let them pick one, instead of typing it
const options = await vault.list();
Vaults

Three vaults, one interface, no vendor SDKs

Plain HTTP on a zero-dependency core. The reference decides which vault answers.

Google Secret Manager

Service account key, exchanged for an access token.

@devicai/vaultsdk/googlegoogleVault({ credentials })
projects/<project>/secrets/<name>/versions/latest
GoogleCredentials client_email private_key project_id? token_uri?

AWS Secrets Manager

Access key or temporary credentials, SigV4-signed. Reads a field out of a key/value secret.

@devicai/vaultsdk/awsawsVault({ credentials })
arn:aws:secretsmanager:<region>:<account>:secret:<name>[:<json-key>]
AwsCredentials accessKeyId secretAccessKey region sessionToken?

Azure Key Vault

App registration, exchanged with Entra ID. Takes the az CLI output verbatim.

@devicai/vaultsdk/azureazureVault({ credentials })
https://<vault>.vault.azure.net/secrets/<name>
AzureCredentials tenantId clientId clientSecret vaultUrl

One package, @devicai/vaultsdk, no peer dependencies.

Adding a vault is two files

The vault, the routing and the cache are already vault-agnostic: an adapter is one file plus its shape. HashiCorp Vault is next.

Write an adapter
The basics

Four calls, and one error type

Connection, discoverability, cache — the whole SDK.

Read

const vault = awsVault({
  credentials,
  ttlMs: 5 * 60_000,          // 0 turns caching off
});

const { value, version } = await vault.read(reference);

// a rotation is picked up on the next read past the TTL
vault.invalidate(reference);  // forget one
vault.invalidate();           // forget all

Let them pick a secret

const options = await vault.list();

if (options === null) {
  // not allowed to enumerate: show a free-text field
} else {
  // [{ name: 'stripe-key', reference: '…' }]
  // never compose a reference yourself
}

Test a connection

// off the credentials, no network call
vault.provider;  vault.identity;  vault.scope;

// touches no secret, so a failure here is a bad
// credential and never a missing permission
await vault.verify();

// bypasses the cache, and never returns the value
const probe = await vault.probe(reference);
// { version, elapsedMs, length: 24,
//   preview: 'sk-••••••cdef' }

Handle failure once

import { VaultError } from '@devicai/vaultsdk';

try {
  await vault.read(reference);
} catch (err) {
  if (err instanceof VaultError) switch (err.kind) {
    case 'unauthenticated':   // wrong or expired
    case 'forbidden':         // they grant the role
    case 'not_found':         // typo, or deleted
    case 'unavailable':       // transient — retryable
    case 'invalid_reference': // not an address at all
  }
}
References

Native forms, verbatim — and nothing else

The address you store is called a reference. An operator pastes the string their tooling already gives them; the vault is inferred from its shape. Leave the version off and you follow their rotation.

Recognised

  • projects/acme/secrets/stripe/versions/latest
  • arn:aws:secretsmanager:eu-west-1:123456789012:secret:stripe…:api_key picks a field out of a key/value secret
  • https://acme-kv.vault.azure.net/secrets/stripe

Refused, on purpose

  • stripe-api-key
  • secret/data/app/config
  • https://acme.vault.azure.net.evil.com/secrets/k
  • https://api.example.com/v1/secrets/k
  • arn:aws:iam::123456789012:role/deploy
recognise.ts
import { looksLikeReference, parseReference }
  from '@devicai/vaultsdk';

looksLikeReference(pasted);       // false → a raw secret: encrypt it
parseReference(pasted).provider;  // 'google' · 'aws' · 'azure'

// both know every shape the SDK supports whether or not you opened
// that vault, so forgetting an import cannot change what you encrypt
resilience.ts
// one instance is one customer, because credentials are
const vault = awsVault({
  credentials: customer.vaultCredentials,
  ttlMs: 5 * 60_000,   // 0 disables caching entirely
});

await vault.read(reference);  // goes to the vault
await vault.read(reference);  // does not

vault.invalidate(reference);
await vault.read(reference);  // goes to the vault again
Cache

Off your critical path, and nowhere else

A warm round-trip is several hundred milliseconds, which rules out reading on every use. The default of five minutes bounds how long a rotation takes to notice.

  • Per instance, in memory — one instance is one customer, because credentials are.
  • Failures are never cached, and nothing is kept beyond the TTL.
Runs anywhere

No vendor SDK. Not even AWS's.

Plain HTTP and WebCrypto throughout, so the same package runs on Node 18+, Cloudflare Workers, Deno, Bun and the browser, with nothing to install alongside it.

worker.ts
// a Cloudflare Worker — same package, same code
import { azureVault } from '@devicai/vaultsdk/azure';

export default {
  async fetch(request, env) {
    const vault = azureVault({
      credentials: env.CUSTOMER_VAULT_CREDENTIALS,
    });

    const { value } = await vault.read(
      'https://acme-kv.vault.azure.net/secrets/stripe',
    );

    return callTheirApi(value);
  },
};
Compatibility

What each vault gives you

The surface is the same everywhere — read, list, verify, probe. What differs below is the vaults' own.

native first-class vault API supported the vault has no equivalent
Capability google aws azure
readnativenativenative
listnativenativenative
verifynativenativenative
follows a rotation/versions/latestAWSCURRENTno version in the URL
reports resolved version
pin an exact version
field of a key/value secret
binary secrets
one connection, many locations
credential blobservice account JSONaccess key ± session tokenaz ad sp create-for-rbac
identity proofJWT RS256 → tokenSigV4, per requestEntra ID client creds
token reused across reads