Files
2026-05-27 23:29:53 +01:00

83 lines
4.9 KiB
Markdown

# @suaveplan/api-secrets-worker-proxy — worker-side proxy to a main-process SecretsLike adapter. Marshals every ctx.secrets.get/require/list call across api-worker-rpc, enforces a per-plugin allowed-keys whitelist on the main side BEFORE the secret value crosses the wire, and provides no worker-side caching so a rotated secret is picked up immediately and a compromised worker cannot exfiltrate cached values.
Stage-3 worker → main `SecretsLike` proxy for the API plugin system.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of error codes thrown across the worker → main `SecretsLike` proxy boundary. The codes here are distinc…
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of error codes thrown across the worker → main `SecretsLike` proxy boundary. The codes here are distinc…
- [main-side](./src/main-side/main-side.md) — Mounts a concrete `SecretsLike` adapter onto a main-process `MessagePort` so a worker-isolated API plugin can request s…
- [main-side](./src/main-side/main-side.md) — Mounts a concrete `SecretsLike` adapter onto a main-process `MessagePort` so a worker-isolated API plugin can request s…
- [redaction](./src/redaction/redaction.md) — Re-exports the canonical `<redacted>` marker and `redactSecret` helper from `@suaveplan/api-secrets`, plus an additiona…
- [redaction](./src/redaction/redaction.md) — Re-exports the canonical `<redacted>` marker and `redactSecret` helper from `@suaveplan/api-secrets`, plus an additiona…
- [testing](./src/testing/testing.md) — Real, no-mock testing harness for the worker-side secrets proxy. Wraps a real `MessageChannel` (via `api-worker-rpc`'s…
- [testing](./src/testing/testing.md) — Real, no-mock testing harness for the worker-side secrets proxy. Wraps a real `MessageChannel` (via `api-worker-rpc`'s…
- [worker-side](./src/worker-side/worker-side.md) — Worker-side `SecretsLike` proxy. Every method call is marshalled across the `api-worker-rpc` transport to the main-side…
- [worker-side](./src/worker-side/worker-side.md) — Worker-side `SecretsLike` proxy. Every method call is marshalled across the `api-worker-rpc` transport to the main-side…
<!-- end:auto-docs -->
## What it does
- **Main side** vends a concrete `SecretsLike` over a `MessagePort` via
`api-worker-rpc`, enforcing a per-plugin allowed-keys whitelist
BEFORE the secret value crosses the wire.
- **Worker side** exposes a `SecretsLike` proxy. Every method call
round-trips to the main side; **there is no worker-side cache** of
values.
## Security invariants
1. A non-manifested key is rejected on the main side with
`SECRETS_PROXY.KEY_NOT_PERMITTED`. The underlying source is never
consulted, so a malicious worker cannot probe the source's state.
2. `list()` returns the FROZEN snapshot of `allowedKeys`, not the
source's `list()`. The worker cannot enumerate names outside its
manifest.
3. Two sequential `get("X")` calls produce TWO main-side invocations.
No caching means a rotated secret is picked up immediately AND a
compromised worker has no internal state to exfiltrate.
4. `close()` makes every subsequent method reject with
`SECRETS_PROXY.PROXY_DISPOSED`.
## Modules
- [`error-codes`](./src/error-codes/error-codes.md) — `SECRETS_PROXY.*` registry + `SecretsProxyError` class.
- [`main-side`](./src/main-side/main-side.md) — `createSecretsProxyServer(port, secrets, allowedKeys)`.
- [`worker-side`](./src/worker-side/worker-side.md) — `createSecretsProxyClient(port, opts?)`.
- [`redaction`](./src/redaction/redaction.md) — `<redacted>` marker + `redactSecretLine`.
- [`testing`](./src/testing/testing.md) — real `MessageChannel` + `InMemorySecrets` harness.
## Quick start
```ts
import { createSecretsProxyServer } from "@suaveplan/api-secrets-worker-proxy/main-side";
import { createSecretsProxyClient } from "@suaveplan/api-secrets-worker-proxy/worker-side";
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
const ports = createInProcessPortPair();
const server = createSecretsProxyServer(ports.serverPort, mySecrets, ["STRIPE_KEY"]);
const proxy = createSecretsProxyClient(ports.clientPort, { initialList: ["STRIPE_KEY"] });
const key = await proxy.require("STRIPE_KEY");
proxy.close();
server.close();
ports.close();
```
## Conformance
`tests/conformance/run-secrets-conformance.test.ts` runs
`runSecretsConformance` from `@suaveplan/api-secrets/testing` against the
worker-side proxy. This is The precondition gate — every scenario
that passes against a direct in-process adapter MUST also pass through the
proxy surface before Stage 4 can complete.
## Dependencies
- `@suaveplan/api-secrets` — canonical contract + redaction marker.
- `@suaveplan/api-types` — `SecretsLike` type.
- `@suaveplan/api-worker-rpc` — `MessagePort`-based RPC transport.
- `@suaveplan/error`, `tslib`, `zod`.