Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
# `adapter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stage-8 concrete `SecretsLike` implementation backed by a
`VaultSecretsBackend` (real `@suaveplan/vault.Vault` via `fromVault`,
or any backend honouring the structural contract). Suitable for
multi-host / cloud deployments that need an encrypted, replicated
container with a controlled unlock flow alongside per-plugin scoping.
The vault is passed in unlocked by the host startup sequence; the
adapter never sees the master key, mirrors the env adapter's allow-
list semantics, and emits the canonical
`api.secrets.reads_total{backend="vault"}` counter on every
successful read.
The adapter is structurally a `SecretsLike` from
`@suaveplan/api-types` and passes the `runSecretsConformance`
harness from `@suaveplan/api-secrets/testing` unchanged.
## Features
- `createVaultSecretsAdapter({ vault, allowlist }): VaultSecretsAdapter`
— factory returning a frozen-shaped `SecretsLike` plus an
idempotent `dispose()`.
- Per-plugin allow-list enforced BEFORE the vault is touched —
rejections throw `SECRETS_VAULT.NOT_ALLOWED`.
- Locked-vault detection: every `get` / `require` checks
`vault.isLocked` and raises `SECRETS_VAULT.VAULT_LOCKED` with no
plaintext fallback.
- Backend error wrapping: an unexpected throw from the backend
surfaces as `SECRETS_VAULT.VAULT_UNAVAILABLE` with the original
error attached on `cause`.
- `require` raises `SECRETS_VAULT.NOT_FOUND` for permitted-but-
absent names; `get` resolves to `undefined`.
- OTel `api.secrets.reads_total{backend="vault"}` counter increments
on every successful resolution (both `get` and `require`).
- Zod-validated options; `SECRETS_VAULT.INVALID_OPTIONS` on failure.
## Usage: Basic
```ts
import { createVaultSecretsAdapter, fromVault }
from "@suaveplan/api-secrets-vault";
import { Vault } from "@suaveplan/vault";
const vault = Vault.open(vaultConfig);
await vault.unlock(password, secretKey);
const secrets = createVaultSecretsAdapter({
vault: fromVault(vault),
allowlist: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"],
});
const stripeKey = await secrets.get("STRIPE_SECRET_KEY");
const jwt = await secrets.require("JWT_SIGNING_KEY");
```
## Usage: Advanced
Wire a deterministic backend for tests (no vault unlock required):
```ts
import { createVaultSecretsAdapter }
from "@suaveplan/api-secrets-vault";
import type { VaultSecretsBackend }
from "@suaveplan/api-secrets-vault/types";
function makeFixtureBackend(seed: Record<string, string>): VaultSecretsBackend {
const store = new Map(Object.entries(seed));
const listeners = new Set<(p: { name: string }) => void>();
return {
get isLocked() {
return false;
},
async get(name) {
return store.get(name);
},
async set(name, value) {
store.set(name, value);
for (const l of listeners) l({ name });
},
async delete(name) {
store.delete(name);
},
async listNames() {
return [...store.keys()];
},
onSecretUpdated(l) {
listeners.add(l);
return () => listeners.delete(l);
},
};
}
const adapter = createVaultSecretsAdapter({
vault: makeFixtureBackend({ STRIPE: "sk_test_123" }),
allowlist: ["STRIPE"],
});
```
## API Reference
- `interface CreateVaultSecretsAdapterOptions`
- `vault: VaultSecretsBackend` — pre-unlocked.
- `allowlist: readonly string[]`
- `interface VaultSecretsAdapter extends SecretsLike`
- `readonly allowlist: Allowlist`
- `dispose(): void` — idempotent
- `createVaultSecretsAdapter(options): VaultSecretsAdapter`
- Throws `SECRETS_VAULT.INVALID_OPTIONS` on bad options.
- Returned `SecretsLike`:
- `get(name): Promise<string | undefined>`
- `require(name): Promise<string>`
- `list(): readonly string[]`
## Implementation Notes
The allow-list check ordering matters — running it BEFORE the
locked-state check means a non-permitted name on a locked vault
raises `NOT_ALLOWED`, not `VAULT_LOCKED`. This is deliberate: the
allow-list rejection is the canonical "you cannot ask about this
name" answer regardless of vault state, and the alternative would
leak liveness information about the vault to plugins that aren't
allowed to read from it.
Backend exceptions are wrapped in `VAULT_UNAVAILABLE` only if they
are not already a `SecretsVaultError` — a backend that throws our
own typed error (e.g. via the `fromVault` write-path) surfaces
through unchanged so codes don't double-wrap.
`dispose()` is idempotent and currently a no-op at the adapter
level; the watcher owns the live subscription. Kept on the surface
so capability-adapter lifecycles look uniform across the family.