# Capability Spec: api-secrets-worker-proxy **Package**: `@suaveplan/api-secrets-worker-proxy` **Status**: SHIPPED (0.1.0) > Every requirement carries a `**Verified by:**` line. ## Requirements ### Requirement: Proxy preserves SecretsLike surface and forwards every call to main The package SHALL expose `createSecretsProxyClient(port)` returning a `SecretsLike`-compatible proxy and `createSecretsProxyServer(port, secrets, allowedKeys)` returning a `WorkerRpcServer`. Every `get` and `require` call SHALL be marshalled to the main process — there MUST be no worker-side cache of secret values, neither in module state nor in client-instance state. Two sequential `get("X")` calls MUST produce TWO main-side RPC invocations (so a rotated secret is picked up immediately AND a compromised worker cannot exfiltrate cached values). `list()` is sync on the canonical contract and SHALL be served by a worker-side snapshot populated either at construction (`initialList`) or by a hidden `__refreshList()` accessor — name lists are manifest-pinned and not sensitive. #### Scenario: two gets produce two main-side RPC calls - **WHEN** worker code calls `ctx.secrets.get("X")` twice in succession and the source value is mutated between calls - **THEN** the proxy observes the post-mutation value on the second call **Verified by:** `src/worker-side/worker-side.test.ts::"two get() calls produce two main-side invocations (no worker-side cache)"` --- ### Requirement: Non-permitted key rejected on main side with SECRETS_PROXY.KEY_NOT_PERMITTED The MAIN-side server SHALL enforce a per-plugin allowed-keys whitelist BEFORE consulting the underlying source. Requests for non-permitted names SHALL throw `SECRETS_PROXY.KEY_NOT_PERMITTED` and the source SHALL NOT be invoked. Permitted keys whose value is absent in the source SHALL surface as `SECRETS_PROXY.KEY_NOT_FOUND` from `require()` — the two cases are distinct so the operator can tell "your plugin didn't declare this" from "your plugin declared it but the source is missing it." #### Scenario: non-permitted key is rejected before source consulted - **WHEN** the whitelist is empty AND worker calls `ctx.secrets.get("LEAKED")` for a key the source DOES contain - **THEN** the call rejects with `SECRETS_PROXY.KEY_NOT_PERMITTED` and the source is not consulted #### Scenario: permitted-but-absent key surfaces SECRETS_PROXY.KEY_NOT_FOUND - **WHEN** the whitelist permits `DECLARED` but the source has no value AND worker calls `require("DECLARED")` - **THEN** the call rejects with `SECRETS_PROXY.KEY_NOT_FOUND` **Verified by:** `src/main-side/main-side.test.ts::"non-permitted key rejects with KEY_NOT_PERMITTED before the source is consulted"` AND `src/main-side/main-side.test.ts::"require() on permitted-but-absent key rejects with KEY_NOT_FOUND"` --- ### Requirement: list() returns frozen allowedKeys snapshot, not the source's enumeration The MAIN-side server's `list` handler SHALL return the FROZEN snapshot of `allowedKeys` supplied at server construction, NOT the underlying source's `list()` output. The worker SHALL be unable to enumerate names outside its manifest, regardless of source state. #### Scenario: source contains extra names, list returns only allowed - **WHEN** the source contains `ALLOWED` and `HIDDEN` AND only `ALLOWED` is in `allowedKeys` AND the worker calls `__refreshList()` - **THEN** the snapshot is `["ALLOWED"]` **Verified by:** `src/main-side/main-side.test.ts::"list() returns the frozen allowedKeys snapshot, not the source's list"` --- ### Requirement: close() transitions proxy to PROXY_DISPOSED for all methods The worker-side proxy SHALL expose `close(reason?)`. After `close()`, every subsequent `get`, `require`, sync `list`, and async `__refreshList` call SHALL throw `SECRETS_PROXY.PROXY_DISPOSED`. The `close()` operation SHALL be idempotent. #### Scenario: post-close methods all reject - **WHEN** the worker calls `close()` and then invokes any proxy method - **THEN** every call throws `SECRETS_PROXY.PROXY_DISPOSED` **Verified by:** `src/worker-side/worker-side.test.ts::"close() makes subsequent calls reject with PROXY_DISPOSED"` --- ### Requirement: Conformance suite passes against the proxy surface The package SHALL be runnable as the surface argument to `runSecretsConformance` from `@suaveplan/api-secrets/testing`. Every scenario MUST pass identically to a direct in-process adapter. This is the Stage-4 precondition gate. #### Scenario: runSecretsConformance passes - **WHEN** `runSecretsConformance({ label: "worker-proxy", factory })` runs against the proxy - **THEN** every scenario passes **Verified by:** `tests/conformance/run-secrets-conformance.test.ts`