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,76 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frozen registry of error codes thrown across the worker → main `SecretsLike`
proxy boundary. The codes here are distinct from the `SECRETS.*` namespace
owned by `@suaveplan/api-secrets` so an operator reading a log line can tell
at a glance whether the failure originated in the proxy transport (whitelist
rejection, disposed proxy, transport-side not-found) versus the concrete
underlying source. This separation matters during incident response: a
`SECRETS_PROXY.KEY_NOT_PERMITTED` is a sandbox-escape attempt, whereas a
`SECRETS.SECRET_NOT_FOUND` is a misconfigured deployment.
## Features
- Three stable codes under the `SECRETS_PROXY` namespace, declared with
`defineCodes` so the union type is statically inferrable.
- `SecretsProxyError` class with the canonical `error.name = "SecretsProxyError"`
pinned by the constructor so structured loggers can aggregate every
proxy-domain failure under one bucket without inspecting the code.
- Full `SuaveplanError` ancestry — `cause`, `context`, `timestamp`, `traceId`
preserved across the wire by `api-worker-rpc`'s error-reconstruction.
## Usage: Basic
```ts
import {
SECRETS_PROXY_CODES,
SecretsProxyError,
} from "@suaveplan/api-secrets-worker-proxy/error-codes";
throw new SecretsProxyError(
SECRETS_PROXY_CODES.KEY_NOT_PERMITTED,
"error.secrets_proxy.key_not_permitted",
{ context: { name: "STRIPE_KEY" } }
);
```
## Usage: Advanced
```ts
import {
SECRETS_PROXY_CODES,
type SecretsProxyCode,
SecretsProxyError,
} from "@suaveplan/api-secrets-worker-proxy/error-codes";
function isProxyFailure(err: unknown): err is SecretsProxyError {
return err instanceof SecretsProxyError;
}
function classify(err: unknown): SecretsProxyCode | "OTHER" {
if (!isProxyFailure(err)) return "OTHER";
return err.code;
}
```
## API Reference
- `SECRETS_PROXY_CODES` — frozen record `{ KEY_NOT_PERMITTED, KEY_NOT_FOUND, PROXY_DISPOSED }`.
- `SecretsProxyCode` — union of fully-qualified `SECRETS_PROXY.*` codes.
- `SECRETS_PROXY_ERROR_NAME` — literal `"SecretsProxyError"`.
- `class SecretsProxyError extends SuaveplanError<SecretsProxyCode>` — public constructor `(code, messageKey, options?)`.
## Implementation Notes
- `defineCodes` produces an immutable record; the prefix is supplied
once and combined with each leaf to form the wire code.
- The constructor widens the abstract base's protected constructor to
public so callers throw without per-call subclasses.
- The `name` field is pinned AFTER `super()` runs because the base
resets it; this is the documented pattern across the project.

View File

@@ -0,0 +1,76 @@
# `main-side`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Mounts a concrete `SecretsLike` adapter onto a main-process `MessagePort`
so a worker-isolated API plugin can request secrets across the
`api-worker-rpc` transport without ever holding a value in its own heap
longer than the immediate caller scope. The allowed-keys whitelist is
enforced on the MAIN side BEFORE the secret value crosses the wire —
this is the sandbox-gating invariant from the API plugin system
design doc §5.2b. A worker reading a value it did not declare in its
manifest is a security incident, and pre-marshal enforcement is the
mechanism that turns "incident" into "rejected request."
## Features
- `createSecretsProxyServer(port, secrets, allowedKeys)` returns a
`WorkerRpcServer` whose `close()` tears down the transport.
- Three served methods: `get`, `require`, `list`. Every `get`/`require`
validates the name argument with Zod (non-empty string) at the wire
boundary even though the typed proxy delivers a string.
- `list()` returns the FROZEN snapshot of `allowedKeys` — NOT the
underlying source's `list()` output. The worker is therefore unable
to enumerate names outside its manifest, regardless of source state.
- Source errors from `require()` are wrapped as
`SECRETS_PROXY.KEY_NOT_FOUND` so the proxy boundary owns a single
not-found code; the original error is preserved on `cause`.
## Usage: Basic
```ts
import { createSecretsProxyServer } from "@suaveplan/api-secrets-worker-proxy/main-side";
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
const ports = createInProcessPortPair();
const server = createSecretsProxyServer(ports.serverPort, mySecrets, [
"STRIPE_KEY",
"DATABASE_URL",
]);
// hand `ports.clientPort` to the worker; tear down with `server.close()`.
```
## Usage: Advanced
```ts
import { createSecretsProxyServer } from "@suaveplan/api-secrets-worker-proxy/main-side";
// Per-plugin sandbox: each plugin gets its own port + its own
// allowed-keys list derived from manifest.secrets[].
for (const plugin of plugins) {
const allowed = plugin.manifest.secrets.map((d) => d.name);
const server = createSecretsProxyServer(plugin.port, vaultSecrets, allowed);
plugin.onDispose(() => {
server.close("plugin-unloaded");
});
}
```
## API Reference
- `createSecretsProxyServer(port: PortLike, secrets: SecretsLike, allowedKeys: ReadonlyArray<string>): WorkerRpcServer`
## Implementation Notes
- `allowSet` is built once at construction; lookups are O(1).
- `frozenList` is `Object.freeze`d at construction so even the served
handler cannot mutate it.
- Both `get` and `require` re-validate the params tuple against
`nameArgsSchema` — defence-in-depth against forged inbound frames.
- The `require` path wraps the source's `SECRETS.SECRET_NOT_FOUND` into
`SECRETS_PROXY.KEY_NOT_FOUND` so the worker sees a single, stable
transport-domain code.

View File

@@ -0,0 +1,75 @@
# `redaction`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Re-exports the canonical `<redacted>` marker and `redactSecret` helper
from `@suaveplan/api-secrets`, plus an additional `redactSecretLine`
worker-side paranoid helper that scrubs values out of arbitrary log
lines. The redaction surface is duplicated here so worker code under
sandbox isolation can scrub its log pipe without taking a direct
dependency on `@suaveplan/api-secrets` (which transitively pulls in the
canonical Zod schemas it does not need). Logging is the most common
secret-leak path and an explicit, documented helper at the proxy
package boundary is a defence-in-depth measure.
## Features
- `REDACTED_MARKER` — the literal `"<redacted>"` token every sink
in the stack grep-matches.
- `redactSecret(value)` — replaces any secret string with the marker,
intentionally ignoring contents so empty-vs-set distinctions cannot
leak through observation.
- `redactSecretLine(line, name)` — finds `name = value` or
`name: value` patterns in the supplied line and replaces the value
with the marker, preserving the name so an operator can still tell
which secret was logged.
## Usage: Basic
```ts
import {
REDACTED_MARKER,
redactSecret,
redactSecretLine,
} from "@suaveplan/api-secrets-worker-proxy/redaction";
const masked = redactSecret(value); // "<redacted>"
const scrubbed = redactSecretLine(
"STRIPE_KEY=sk_live_abc loaded",
"STRIPE_KEY"
); // "STRIPE_KEY=<redacted> loaded"
```
## Usage: Advanced
```ts
import { redactSecretLine } from "@suaveplan/api-secrets-worker-proxy/redaction";
function scrubAll(line: string, names: readonly string[]): string {
return names.reduce(
(acc, name) => redactSecretLine(acc, name),
line
);
}
logger.info(scrubAll(rawLine, manifest.secrets.map((d) => d.name)));
```
## API Reference
- `REDACTED_MARKER: "<redacted>"` — canonical token.
- `redactSecret(value: string): "<redacted>"` — returns the marker.
- `redactSecretLine(line: string, name: string): string` — name-aware line scrub.
## Implementation Notes
- Regex meta-characters in the supplied name are escaped before
compilation so callers can safely pass names containing `.` or `$`.
- An empty `name` is a no-op so callers can unconditionally invoke
the helper without a guard.
- The value-shape pattern after the delimiter is `\S+` (conservative);
callers needing multi-token values should pre-tokenise.

View File

@@ -0,0 +1,82 @@
# `testing`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Real, no-mock testing harness for the worker-side secrets proxy. Wraps
a real `MessageChannel` (via `api-worker-rpc`'s `createInProcessPortPair`),
a real in-memory `SecretsLike` adapter (`Map`-backed, throws real
`SECRETS.SECRET_NOT_FOUND` from `require`), the main-side proxy server,
and the worker-side proxy client into a single `SecretsProxyFixture`
that downstream tests construct in a single call. The harness is the
exact surface every downstream Stage-4 source uses to demonstrate
conformance — the in-memory adapter is the reference implementation.
## Features
- `InMemorySecrets` — real `SecretsLike` implementation with a
`Map` store, `set/delete` mutation accessors for write-then-read
cache-bust tests, and full `get/require/list` parity.
- `createSecretsProxyFixture(opts)` — wires up channel + source +
server + client; returns the four artifacts plus a `close()` that
tears them all down in order.
- Returned client is seeded with `initialList = allowedKeys` so the
sync `list()` accessor works without a round-trip.
## Usage: Basic
```ts
import { createSecretsProxyFixture } from "@suaveplan/api-secrets-worker-proxy/testing";
const fx = createSecretsProxyFixture({
seed: { STRIPE_KEY: "sk_test_abc" },
allowedKeys: ["STRIPE_KEY"],
});
try {
const v = await fx.client.require("STRIPE_KEY");
} finally {
fx.close();
}
```
## Usage: Advanced
```ts
import { createSecretsProxyFixture } from "@suaveplan/api-secrets-worker-proxy/testing";
// Demonstrate no-cache invariant: mutate the source mid-test and
// observe the worker proxy seeing the rotated value.
const fx = createSecretsProxyFixture({
seed: { K: "v1" },
allowedKeys: ["K"],
});
try {
await fx.client.get("K"); // "v1"
fx.source.set("K", "v2");
await fx.client.get("K"); // "v2" — no caching
} finally {
fx.close();
}
```
## API Reference
- `class InMemorySecrets implements SecretsLike` — `get`, `require`,
`list`, `set`, `delete`.
- `createSecretsProxyFixture(opts: SecretsProxyFixtureOptions): SecretsProxyFixture`
- `interface SecretsProxyFixtureOptions { seed?: Record<string,string>; allowedKeys: readonly string[] }`
- `interface SecretsProxyFixture { ports, source, server, client, close() }`
## Implementation Notes
- `InMemorySecrets.require` throws a real `SecretsError` carrying
`SECRETS.SECRET_NOT_FOUND`; the main-side proxy then re-wraps it as
`SECRETS_PROXY.KEY_NOT_FOUND` so the worker sees the transport-domain
code.
- `close()` tears down client first, then server, then ports —
matches the documented teardown order in `api-worker-rpc`.
- The frozen array returned by `list()` is a snapshot at the time of
the call; mutations after that point are not reflected.

View File

@@ -0,0 +1,75 @@
# `worker-side`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Worker-side `SecretsLike` proxy. Every method call is marshalled across
the `api-worker-rpc` transport to the main-side server, and the proxy
intentionally maintains NO cache of secret values — neither in module
state nor in instance state. Two sequential `get("X")` calls produce
two main-side invocations, so a rotated secret is picked up immediately
on the next access, and a compromised worker cannot exfiltrate values
from cached state because no such state exists.
## Features
- `createSecretsProxyClient(port, opts?)` returns a `SecretsProxyClient`
that satisfies the canonical `SecretsLike` interface plus `close()`.
- No value caching — every `get`/`require` round-trips to main-side.
- `list()` is sync on the canonical contract but async on the wire, so
the proxy resolves it via a frozen snapshot supplied either at
construction (`initialList`, manifest-derived) or via the hidden
`__refreshList()` accessor for advanced refresh-by-demand callers.
- `close()` is idempotent; subsequent calls throw
`SECRETS_PROXY.PROXY_DISPOSED`.
## Usage: Basic
```ts
import { createSecretsProxyClient } from "@suaveplan/api-secrets-worker-proxy/worker-side";
const secrets = createSecretsProxyClient(workerSidePort, {
initialList: ["STRIPE_KEY"],
});
const key = await secrets.require("STRIPE_KEY");
```
## Usage: Advanced
```ts
import { createSecretsProxyClient } from "@suaveplan/api-secrets-worker-proxy/worker-side";
const secrets = createSecretsProxyClient(port, {
initialList: manifest.secrets.map((d) => d.name),
defaultTimeoutMs: 2_000,
});
// Re-read after a rotation event: NO caching, so a fresh call sees
// the rotated value with no manual cache-bust.
async function refreshOnRotate(name: string): Promise<string> {
return secrets.require(name);
}
secrets.close("worker-shutdown");
```
## API Reference
- `createSecretsProxyClient(port: PortLike, opts?: CreateSecretsProxyClientOptions): SecretsProxyClient`
- `interface CreateSecretsProxyClientOptions extends WorkerRpcClientOptions { initialList?: ReadonlyArray<string> }`
- `interface SecretsProxyClient extends SecretsLike { close(reason?): void }`
## Implementation Notes
- `disposed` is a closure flag; `guardDisposed` throws the
`PROXY_DISPOSED` code rather than silently returning.
- `get()` collapses `null` to `undefined` to align with the
`SecretsLike` contract on the canonical surface — the wire allows
null because that is what JSON-cloneable returns for an absent value.
- `__refreshList()` mutates `listSnapshot` and returns the new frozen
array; the sync `list()` accessor reads the latest snapshot.
- Cache-free design is the explicit security invariant — see the
package README for the threat model.