83 lines
4.9 KiB
Markdown
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`.
|
|
|