first commit
This commit is contained in:
113
docs/api-stack/api/api-secrets-env/src/adapter/adapter.md
Normal file
113
docs/api-stack/api/api-secrets-env/src/adapter/adapter.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# `adapter`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stage-4 concrete `SecretsLike` implementation that resolves secret names
|
||||
against the host process's environment variables. Suitable for local
|
||||
development, container orchestrators that inject secrets as env vars,
|
||||
single-host deployments, and anywhere the operator already has tooling
|
||||
around env injection. Per-plugin allowed-keys enforcement keeps a worker
|
||||
or untrusted plugin from enumerating or reading any env var beyond what
|
||||
its manifest declared.
|
||||
|
||||
The adapter is structurally a `SecretsLike` from `@suaveplan/api-types`
|
||||
and passes the `runSecretsConformance` harness from
|
||||
`@suaveplan/api-secrets/testing` unchanged. The env source is an
|
||||
injectable function so tests never mutate the real process environment;
|
||||
in production the default reads from `process.env` overlaid with
|
||||
`Bun.env` when running under Bun.
|
||||
|
||||
## Features
|
||||
|
||||
- `createEnvSecrets({ allowedKeys, source?, prefix? }): SecretsLike` —
|
||||
factory returning a frozen-shaped instance.
|
||||
- Per-plugin allow-list enforced BEFORE the env source is consulted, so
|
||||
a non-permitted name never resolves a value regardless of what is in
|
||||
the environment. Rejections throw `SECRETS_ENV.KEY_NOT_PERMITTED`.
|
||||
- Optional `prefix` for multi-tenant single-host deployments. `list()`
|
||||
returns the un-prefixed allowed-keys; the prefix is a lookup detail.
|
||||
- Injectable `source` function: tests pass a deterministic record;
|
||||
production defaults to a fresh `process.env`/`Bun.env` snapshot per
|
||||
call so a rotated value is picked up immediately with no caching.
|
||||
- Zod-validated options; `SECRETS_ENV.INVALID_OPTIONS` on failure.
|
||||
- `require()` throws `SECRETS_ENV.KEY_NOT_FOUND` for permitted-but-absent
|
||||
names; `get()` resolves to `undefined` for parity with the contract.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createEnvSecrets } from "@suaveplan/api-secrets-env/adapter";
|
||||
|
||||
const secrets = createEnvSecrets({
|
||||
allowedKeys: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"],
|
||||
});
|
||||
|
||||
const stripeKey = await secrets.get("STRIPE_SECRET_KEY");
|
||||
const jwt = await secrets.require("JWT_SIGNING_KEY");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a deterministic source and apply a per-plugin prefix:
|
||||
|
||||
```ts
|
||||
import { createEnvSecrets } from "@suaveplan/api-secrets-env/adapter";
|
||||
|
||||
const snapshot = {
|
||||
BILLING_STRIPE_SECRET_KEY: "sk_test_123",
|
||||
BILLING_WEBHOOK_SECRET: "whsec_456",
|
||||
} as const;
|
||||
|
||||
const secrets = createEnvSecrets({
|
||||
allowedKeys: ["STRIPE_SECRET_KEY", "WEBHOOK_SECRET"],
|
||||
prefix: "BILLING_",
|
||||
source: () => snapshot,
|
||||
});
|
||||
|
||||
await secrets.require("STRIPE_SECRET_KEY"); // → "sk_test_123"
|
||||
|
||||
try {
|
||||
await secrets.get("OTHER_PLUGIN_KEY");
|
||||
} catch (err) {
|
||||
// SECRETS_ENV.KEY_NOT_PERMITTED — not in allow-list.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CreateEnvSecretsOptions`
|
||||
- `allowedKeys: readonly string[]` — required, non-empty.
|
||||
- `source?: () => Readonly<Record<string, string | undefined>>` —
|
||||
default: `process.env` overlaid with `Bun.env`.
|
||||
- `prefix?: string` — default `""`.
|
||||
- `createEnvSecrets(options): SecretsLike` — throws
|
||||
`SecretsEnvError(SECRETS_ENV.INVALID_OPTIONS)` on bad options.
|
||||
- Returned `SecretsLike`:
|
||||
- `get(name): Promise<string | undefined>` — `KEY_NOT_PERMITTED` if
|
||||
`name ∉ allowedKeys`; `undefined` if permitted-but-absent.
|
||||
- `require(name): Promise<string>` — `KEY_NOT_PERMITTED` or
|
||||
`KEY_NOT_FOUND`.
|
||||
- `list(): readonly string[]` — frozen copy of `allowedKeys`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default source returns a fresh shallow snapshot per call
|
||||
(`{ ...process.env, ...Bun.env }`) so a rotation between two `get` calls
|
||||
is visible without any constructor caching. The Bun overlay only runs
|
||||
when the `Bun` global exists; the env-snapshot type
|
||||
`Record<string, string | undefined>` matches Node's `process.env` shape
|
||||
so `value === undefined` after lookup is the only sentinel needed.
|
||||
|
||||
`allowedKeys` is frozen and copied at construction; the returned `list()`
|
||||
hands out the same frozen array on every call so consumers cannot mutate
|
||||
the surface. The allow-list check runs BEFORE the source lookup so a
|
||||
non-permitted name never even reads the snapshot — that ordering matters
|
||||
when the source function is expensive or has side effects.
|
||||
|
||||
Zod option-validation runs once at construction; the resulting
|
||||
`SECRETS_ENV.INVALID_OPTIONS` carries the raw issue list on
|
||||
`error.context.issues` for upstream diagnostic surfaces.
|
||||
@@ -0,0 +1,103 @@
|
||||
# `descriptor-builder`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Convenience builder that turns a compact allow-list declaration into a
|
||||
fully-typed `SecretDescriptor[]` suitable for embedding in a plugin
|
||||
manifest's `secrets[]` array. Pairs with `createEnvSecrets` so a plugin's
|
||||
allowed-keys and manifest descriptors come from the same source of truth
|
||||
instead of drifting through copy-paste.
|
||||
|
||||
The builder accepts either bare name strings (shorthand for
|
||||
`{ name, required: true }`) or full input rows with `required`,
|
||||
`description`, and `format` overrides. A second helper extracts just the
|
||||
names for direct hand-off to `createEnvSecrets`'s `allowedKeys` option.
|
||||
|
||||
## Features
|
||||
|
||||
- `buildEnvSecretDescriptors(rows)` — accept `string | EnvSecretDescriptorInput`;
|
||||
defaults `required` to `true` (env deployments fail-fast on missing
|
||||
declared secrets).
|
||||
- `descriptorAllowedKeys(descriptors)` — extracts the `name` field list,
|
||||
returning a frozen array ready for `createEnvSecrets({ allowedKeys })`.
|
||||
- Output arrays and descriptor objects are frozen so the manifest is
|
||||
immutable past construction.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildEnvSecretDescriptors,
|
||||
descriptorAllowedKeys,
|
||||
} from "@suaveplan/api-secrets-env/descriptor-builder";
|
||||
|
||||
const descriptors = buildEnvSecretDescriptors([
|
||||
"STRIPE_SECRET_KEY",
|
||||
"JWT_SIGNING_KEY",
|
||||
]);
|
||||
const allowedKeys = descriptorAllowedKeys(descriptors);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Mix shorthand names with full rows carrying format hints:
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildEnvSecretDescriptors,
|
||||
descriptorAllowedKeys,
|
||||
} from "@suaveplan/api-secrets-env/descriptor-builder";
|
||||
import { createEnvSecrets } from "@suaveplan/api-secrets-env/adapter";
|
||||
|
||||
const descriptors = buildEnvSecretDescriptors([
|
||||
{
|
||||
name: "STRIPE_SECRET_KEY",
|
||||
required: true,
|
||||
description: "Stripe API server key",
|
||||
format: "string",
|
||||
},
|
||||
{
|
||||
name: "OPTIONAL_WEBHOOK_KEY",
|
||||
required: false,
|
||||
description: "Optional override; falls back to JWT key",
|
||||
},
|
||||
"JWT_SIGNING_KEY",
|
||||
]);
|
||||
|
||||
const secrets = createEnvSecrets({
|
||||
allowedKeys: descriptorAllowedKeys(descriptors),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface EnvSecretDescriptorInput`
|
||||
- `name: string`
|
||||
- `required?: boolean` (default `true`)
|
||||
- `description?: string`
|
||||
- `format?: "string" | "base64" | "json"`
|
||||
- `buildEnvSecretDescriptors(rows: ReadonlyArray<string | EnvSecretDescriptorInput>): readonly SecretDescriptor[]`
|
||||
— frozen.
|
||||
- `descriptorAllowedKeys(descriptors: readonly SecretDescriptor[]): readonly string[]`
|
||||
— frozen.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`required` defaults to `true` because env-var-style deployments treat a
|
||||
declared-but-missing secret as a hard install error — the alternative
|
||||
(silent `false`) led to plugins shipping with empty Stripe keys in
|
||||
historical incidents. Override per row when an env var is genuinely
|
||||
optional.
|
||||
|
||||
The `description` and `format` fields are only included on the output
|
||||
descriptor when present on the input row. The conditional spread keeps
|
||||
the output object shape predictable for downstream Zod validation
|
||||
(`secretDescriptorSchema.strict()` rejects unknown keys but accepts
|
||||
omitted optional keys). Output arrays and descriptor objects are
|
||||
`Object.freeze`'d so a downstream caller cannot mutate a manifest after
|
||||
the fact — the manifest is the authoritative declaration and should be
|
||||
immutable past build time.
|
||||
@@ -0,0 +1,91 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable error-code registry and dedicated `SuaveplanError` subclass for the
|
||||
env-backed secrets adapter. Every failure mode the adapter can produce maps
|
||||
to one of these codes; structured loggers and middleware can dispatch on
|
||||
either `error.code` or the stable `error.name` (`SecretsEnvError`) without
|
||||
parsing message strings or relying on `instanceof` chains across module
|
||||
boundaries.
|
||||
|
||||
The codes intentionally namespace under `SECRETS_ENV.*` rather than reusing
|
||||
`SECRETS.*` (owned by `@suaveplan/api-secrets`). The split lets operators
|
||||
instantly distinguish env-adapter rejections (allow-list violation,
|
||||
malformed options, missing key for a permitted name) from upstream
|
||||
`SecretsLike` contract failures or proxy-boundary failures.
|
||||
|
||||
## Features
|
||||
|
||||
- `SECRETS_ENV_CODES` — frozen registry built via `defineCodes` from
|
||||
`@suaveplan/error`. Codes: `KEY_NOT_PERMITTED`, `KEY_NOT_FOUND`,
|
||||
`INVALID_OPTIONS`.
|
||||
- `SecretsEnvError` — concrete `SuaveplanError<SecretsEnvCode>` subclass.
|
||||
Public constructor lets callers throw without subclassing per call site.
|
||||
- `SECRETS_ENV_ERROR_NAME` — stable `"SecretsEnvError"` literal pinned on
|
||||
`Error.name` for log aggregation.
|
||||
- `SecretsEnvCode` — exhaustive union of fully-qualified `SECRETS_ENV.*`
|
||||
codes for use in switch statements and as the `code` field type.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
SECRETS_ENV_CODES,
|
||||
SecretsEnvError,
|
||||
} from "@suaveplan/api-secrets-env/error-codes";
|
||||
|
||||
throw new SecretsEnvError(
|
||||
SECRETS_ENV_CODES.KEY_NOT_PERMITTED,
|
||||
"error.secrets_env.key_not_permitted",
|
||||
{ context: { name: "STRIPE_SECRET_KEY" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Pattern-match on the typed code union for exhaustive handling:
|
||||
|
||||
```ts
|
||||
import {
|
||||
SECRETS_ENV_CODES,
|
||||
type SecretsEnvCode,
|
||||
SecretsEnvError,
|
||||
} from "@suaveplan/api-secrets-env/error-codes";
|
||||
|
||||
function describe(err: SecretsEnvError): string {
|
||||
const code: SecretsEnvCode = err.code;
|
||||
switch (code) {
|
||||
case SECRETS_ENV_CODES.KEY_NOT_PERMITTED:
|
||||
return "not in plugin manifest";
|
||||
case SECRETS_ENV_CODES.KEY_NOT_FOUND:
|
||||
return "permitted but absent from environment";
|
||||
case SECRETS_ENV_CODES.INVALID_OPTIONS:
|
||||
return "adapter mis-configured";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SECRETS_ENV_CODES: { readonly KEY_NOT_PERMITTED: "SECRETS_ENV.KEY_NOT_PERMITTED"; readonly KEY_NOT_FOUND: "SECRETS_ENV.KEY_NOT_FOUND"; readonly INVALID_OPTIONS: "SECRETS_ENV.INVALID_OPTIONS"; }` — frozen.
|
||||
- `type SecretsEnvCode = (typeof SECRETS_ENV_CODES)[keyof typeof SECRETS_ENV_CODES]`.
|
||||
- `SECRETS_ENV_ERROR_NAME: "SecretsEnvError"`.
|
||||
- `class SecretsEnvError extends SuaveplanError<SecretsEnvCode>` —
|
||||
public `constructor(code, messageKey, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is built with `defineCodes("SECRETS_ENV", [...] as const)` so
|
||||
the runtime values are strings of the form `"SECRETS_ENV.X"` and the type
|
||||
is the literal union — letting consumers narrow without runtime checks.
|
||||
`SecretsEnvError` widens the abstract `SuaveplanError` constructor from
|
||||
`protected` to `public`; the rationale is identical to `SecretsError` in
|
||||
`@suaveplan/api-secrets` — throwing at call sites without one-off
|
||||
subclasses keeps the failure-site code short. The `error.name` pin
|
||||
happens in the constructor so even hand-crafted throwables surface the
|
||||
canonical name to structured loggers.
|
||||
Reference in New Issue
Block a user