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,126 @@
# @suaveplan/testing-integration-step-ca - smallstep/step-ca testcontainers fixture for @suaveplan/testing-integration — real PKI for HTTPS/WebRTC integration tests
## Summary
A `ServiceFixture` for `smallstep/step-ca` that gives any integration test suite a real, ephemeral certificate authority — root cert, JWK provisioner, and an `issueCert()` helper that mints leaf certificates on demand.
<!-- begin:auto-docs -->
## Documentation tree
- [parse-pem-blocks](./src/fixture/parse-pem-blocks.md) — `parsePemBlocks` is a small, dependency-free PEM splitter used by `StepCaFixture.issueCert()` to separate a leaf certif…
- [step-ca-fixture](./src/fixture/step-ca-fixture.md) — `StepCaFixture` manages the full lifecycle of a `smallstep/step-ca` container for integration tests that need a real PK…
<!-- end:auto-docs -->
## Purpose
WebRTC, secure SMTPS/IMAPS, HTTPS edges (Caddy/nginx), and other integration scenarios refuse non-secure contexts. Without a real PKI chain you end up either rolling per-suite self-signed certs or sprinkling `rejectUnauthorized: false` everywhere. This fixture replaces both with a single `await fixture.start()` that returns a config with a trusted root cert, a fingerprint, and an `issueCert()` method backed by a JWK provisioner inside the container.
## Features
- `ServiceFixture<StepCaConfig>` implementation matching the `@suaveplan/testing-integration` contract — drops into `TestEnvironment` alongside `mailpit`, `garage-s3`, `postgres`, `redis`.
- 5-var env-bridge passthrough mode (`STEPCA_URL`, `STEPCA_ROOT_CERT_PEM`, `STEPCA_ROOT_CERT_PATH`, `STEPCA_FINGERPRINT`, `STEPCA_PROVISIONER_PASSWORD`); partial sets (4 of 5) fall through to container mode.
- Bake-init shell entrypoint runs `step ca init`, creates an EC P-256 JWK provisioner, merges it into `ca.json`, captures the root fingerprint, and `exec`s `step-ca` — all in one container start.
- Host-side root-cert tmpfile (`rootCertPath`) automatically materialised, available for `NODE_EXTRA_CA_CERTS`, Playwright `--ignore-certificate-errors-spki-list`, or volume-mounting into sibling containers.
- `issueCert({ commonName, sans?, ttl? })` shells `step ca certificate` against the JWK provisioner; returns `{ certPem, chainPem, keyPem, serial }`.
- Injectable `containerFactory`, `execFn`, and `fetchFn` for unit-testability of the fixture itself (100% Istanbul coverage on touched modules).
- HTTPS health probe with TLS verification disabled (root not yet trusted on the host).
## Basic example
```ts
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture();
const config = await ca.start();
// Issue a leaf cert for our test server
const leaf = await ca.issueCert({
commonName: "host.test",
sans: ["host.test", "localhost"],
ttl: "24h",
});
// Use the root in a Node TLS context
import { createSecureContext } from "node:tls";
const trust = createSecureContext({ ca: config.rootCertPem });
await ca.stop();
```
## Advanced example — Playwright + secure context
```ts
import { TestEnvironment } from "@suaveplan/testing-integration";
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
import { chromium } from "playwright";
const ca = new StepCaFixture();
const env = new TestEnvironment([ca]);
await env.startAll();
const cfg = ca.start ? await ca.start() : null;
// Mount the root into a sibling container or load it into the test process:
process.env.NODE_EXTRA_CA_CERTS = cfg!.rootCertPath;
// Issue an HTTPS cert for our app server
const { certPem, keyPem } = await ca.issueCert({
commonName: "app.test",
sans: ["app.test"],
ttl: "1h",
});
// Launch Playwright with the root trusted (Chromium will follow the system
// store; for tests, --ignore-certificate-errors-spki-list works too)
const browser = await chromium.launch({
args: [`--ignore-certificate-errors-spki-list=${cfg!.fingerprint}`],
});
await env.stopAll();
```
## API reference
### `class StepCaFixture implements ServiceFixture<StepCaConfig>`
#### `new StepCaFixture(options?: StepCaFixtureOptions)`
Options:
- `image?: string` — override the Docker image. Defaults to `STEPCA_IMAGE` (a pinned `smallstep/step-ca` tag).
- `caName?: string` — CA display name. Defaults to `"Suaveplan Test CA"`.
- `dnsNames?: string` — comma-separated DNS SANs for the CA. Defaults to `"localhost,step-ca"`.
- `provisionerName?: string` — JWK provisioner name. Defaults to `"api"`.
- `healthTimeoutMs?: number` / `healthPollIntervalMs?: number` — override health-poll timing.
- `containerFactory?`, `execFn?`, `fetchFn?` — injection points for unit-testing the fixture itself.
#### `start(): Promise<StepCaConfig>`
Returns `{ caUrl, rootCertPem, rootCertPath, fingerprint, provisionerName, provisionerPassword }`. In container mode, also materialises `rootCertPath` to a host-side tmpfile.
#### `stop(): Promise<void>`
Stops the container (idempotent) and unlinks the host-side root-cert tmpfile (best-effort).
#### `healthCheck(): Promise<boolean>`
`GET https://localhost:<mappedPort>/health` with TLS verification disabled. Returns `false` when no container has been started or on any network error.
#### `issueCert(opts: IssueCertOptions): Promise<IssuedCert>`
Issues a leaf via the JWK provisioner. Throws `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` on empty `commonName`, non-zero `step` exit, or read failures; throws `INTEGRATION.STEP_CA.PARSE_PEM_FAILED` on malformed PEM or invalid `step certificate inspect` JSON.
### `parsePemBlocks(text: string): { leaf: string; chain: string }`
Splits a PEM payload into the first `-----BEGIN CERTIFICATE-----`…`-----END CERTIFICATE-----` block (`leaf`) and the concatenation of remaining blocks (`chain`). Throws on empty input, non-PEM input, or unterminated blocks.
### `buildStepCaInitScript(args)`
Builds the bake-init shell script used as the container entrypoint. Exported for inspection in tests; not normally consumed directly.
## Implementation notes
- The container runs all init steps as part of its entrypoint (`sh -c "<initScript>"`) and `exec`s `step-ca` so health passes only after init has completed. The fixture then reads `root_ca.crt` and `secrets/fingerprint` via `container.exec`.
- `caName`, `dnsNames`, and `provisionerName` are validated against `^[A-Za-z0-9 _.-]{1,128}$` before being interpolated into the entrypoint script. Special characters are JSON-escaped via `JSON.stringify` so a malformed override cannot inject shell metacharacters.
- The default health-fetch uses `node:https.request` with `rejectUnauthorized: false`. The host trust store does not yet contain the test root, so TLS verification on the health probe must be off; this is the fixture's only HTTPS read of an untrusted endpoint.
- `parsePemBlocks` is a pure helper with no dependency on `step-ca` and is reused by `issueCert()` for splitting leaf-from-chain.
- Coverage gate: 100% Istanbul on every non-`*.types.ts`, non-barrel module. Container/`fetch` defaults are excluded via `/* istanbul ignore next */` because they're only exercisable in the integration smoke test.

View File

@@ -0,0 +1,63 @@
# parsePemBlocks
## Purpose
`parsePemBlocks` is a small, dependency-free PEM splitter used by `StepCaFixture.issueCert()` to separate a leaf certificate from its chain in a single concatenated PEM payload. The function is exported standalone because the same split is useful in any code that consumes `step ca certificate` output, where the leaf is followed by zero or more intermediate certs.
## Features
- Pure function — no I/O, no globals, no dependencies.
- Returns `{ leaf, chain }` where `leaf` is the first `-----BEGIN CERTIFICATE-----`…`-----END CERTIFICATE-----` block (inclusive of markers) and `chain` is the concatenation of every subsequent block separated by a single newline.
- Tolerant of arbitrary text between blocks (comments, blank lines, headers).
- Strict failures: empty / non-PEM input throws `"no PEM CERTIFICATE blocks found"`; a `BEGIN CERTIFICATE` line lacking a matching `END CERTIFICATE` throws `"unterminated PEM block"`.
## Usage: basic example
```ts
import { parsePemBlocks } from "@suaveplan/testing-integration-step-ca/fixture/step-ca-fixture";
const payload = `-----BEGIN CERTIFICATE-----
LEAF
-----END CERTIFICATE-----`;
const { leaf, chain } = parsePemBlocks(payload);
// leaf = full BEGIN/END block; chain = ""
```
## Usage: advanced example
```ts
const payload = [
"# Issued at 2026-04-28T10:33Z",
"-----BEGIN CERTIFICATE-----",
"LEAFLEAF",
"-----END CERTIFICATE-----",
"",
"-----BEGIN CERTIFICATE-----",
"INTERMEDIATE",
"-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----",
"ROOT",
"-----END CERTIFICATE-----",
].join("\n");
const { leaf, chain } = parsePemBlocks(payload);
// leaf = "-----BEGIN CERTIFICATE-----\nLEAFLEAF\n-----END CERTIFICATE-----"
// chain = "<INTERMEDIATE block>\n<ROOT block>"
```
## API reference
### `parsePemBlocks(text: string): { leaf: string; chain: string }`
- **Parameters:** `text` — the raw PEM payload to split.
- **Returns:** `{ leaf, chain }`. `chain` is the empty string when the payload contains exactly one block.
- **Throws:** `Error("StepCaFixture.parsePemBlocks: no PEM CERTIFICATE blocks found")` — payload contains no `BEGIN CERTIFICATE` line.
- **Throws:** `Error("StepCaFixture.parsePemBlocks: unterminated PEM block")` — payload has a `BEGIN` but no matching `END`.
## Implementation notes
- The implementation walks the payload by `indexOf("-----BEGIN CERTIFICATE-----")` then `indexOf("-----END CERTIFICATE-----")` — O(n) over the input, no regex backtracking risk.
- Blocks are sliced inclusive of both markers, so the returned strings are well-formed PEM and can be fed straight to `crypto.X509Certificate` or `tls.createSecureContext`.
- The chain separator is a single `\n` (not `\r\n`); callers that need OS-specific line endings should normalise after splitting.
- This function deliberately does NOT validate the base64 body or enforce 64-column wrapping — `step` and Node's TLS stack will reject malformed bodies on use, and tests of the splitter should not double-up on cryptographic validation.

View File

@@ -0,0 +1,86 @@
# StepCaFixture
## Purpose
`StepCaFixture` manages the full lifecycle of a `smallstep/step-ca` container for integration tests that need a real PKI chain — WebRTC, secure SMTPS/IMAPS, HTTPS edges (Caddy/nginx), and any scenario that refuses non-secure contexts. Implementing the `ServiceFixture<StepCaConfig>` contract from `@suaveplan/testing-integration`, it can be composed into a `TestEnvironment` alongside Mailpit, Garage S3, Postgres, Redis, and other fixtures, with parallel startup and reverse-order teardown.
## Features
- 5-var env-bridge passthrough mode (all of `STEPCA_URL`, `STEPCA_ROOT_CERT_PEM`, `STEPCA_ROOT_CERT_PATH`, `STEPCA_FINGERPRINT`, `STEPCA_PROVISIONER_PASSWORD`); a partial set (4 of 5) falls through to container mode.
- Bake-init shell entrypoint runs `step ca init`, generates an EC P-256 JWK provisioner, merges it into `ca.json`, captures the root fingerprint, and `exec`s `step-ca` — all in one container start.
- Host-side root-cert tmpfile (`rootCertPath`) materialised after health passes; suitable for `NODE_EXTRA_CA_CERTS`, Playwright trust args, or volume-mounting into sibling containers.
- HTTPS health probe with TLS verification disabled (root is not yet trusted on the host).
- `issueCert({ commonName, sans?, ttl? })` shells `step ca certificate` against the JWK provisioner; returns leaf + chain + key + serial.
- Injectable `containerFactory`, `execFn`, and `fetchFn` so the fixture itself is unit-testable at 100% Istanbul coverage.
## Usage: basic example
```ts
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture();
const cfg = await ca.start();
console.log(cfg.fingerprint); // 64-hex SHA-256 of the root
console.log(cfg.rootCertPath); // host-side tmpfile path
const { certPem } = await ca.issueCert({ commonName: "host.test" });
await ca.stop();
```
## Usage: advanced example
```ts
import { TestEnvironment } from "@suaveplan/testing-integration";
import { StepCaFixture } from "@suaveplan/testing-integration-step-ca";
const ca = new StepCaFixture({
caName: "Edge Test CA",
dnsNames: "localhost,step-ca,edge.test",
provisionerName: "api",
healthTimeoutMs: 30_000,
});
const env = new TestEnvironment([ca]);
await env.startAll();
const cfg = await ca.start();
process.env.NODE_EXTRA_CA_CERTS = cfg.rootCertPath;
const leaf = await ca.issueCert({
commonName: "edge.test",
sans: ["edge.test", "edge.local"],
ttl: "24h",
});
await env.stopAll();
```
## API reference
### `new StepCaFixture(options?: StepCaFixtureOptions)`
`StepCaFixtureOptions` accepts `image`, `caName`, `dnsNames`, `provisionerName`, `healthTimeoutMs`, `healthPollIntervalMs`, plus the injectable test seams `containerFactory`, `execFn`, `fetchFn`.
### `start(): Promise<StepCaConfig>`
In env-bridge mode: returns the env-supplied config without starting a container. In container mode: starts the container with the bake-init entrypoint, polls `/health` until ok, reads `root_ca.crt` + `secrets/fingerprint` via exec, materialises the host-side tmpfile, and returns `{ caUrl, rootCertPem, rootCertPath, fingerprint, provisionerName, provisionerPassword }`. Throws on container failure; if `waitForHealth()` rejects the container is stopped and the reference cleared before the error propagates.
### `stop(): Promise<void>`
Idempotent. Removes the host-side root-cert tmpfile (best-effort) and stops the container exactly once.
### `healthCheck(): Promise<boolean>`
`GET https://localhost:<mappedPort>/health` via the injected `fetchFn` (default uses `node:https.request` with `rejectUnauthorized: false`). Returns `false` when no container is running, when the response is not `ok`, or on any network error.
### `issueCert(opts: IssueCertOptions): Promise<IssuedCert>`
Empty `commonName` → throws `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` without invoking exec. Non-zero `step` exit → `INTEGRATION.STEP_CA.ISSUE_CERT_FAILED` with stderr (or stdout fallback) attached as `cause`. Malformed PEM or `step certificate inspect` JSON failure → `INTEGRATION.STEP_CA.PARSE_PEM_FAILED`.
## Implementation notes
- The container runs init+serve in a single entrypoint (`sh -c "<initScript>"`); the script `exec`s `step-ca` so the running PID has the daemon, not the shell. Health passing therefore implies init has fully completed.
- `caName`, `dnsNames`, `provisionerName` are validated against `^[A-Za-z0-9 _.-]{1,128}$` and JSON-escaped via `JSON.stringify` before interpolation into the entrypoint script. This blocks shell-metacharacter injection at the caller boundary.
- The provisioner password is generated with `crypto.randomBytes(24).toString("hex")` (192 bits of entropy).
- `parsePemBlocks` (sibling helper) is the only PEM-aware code; `issueCert` delegates leaf/chain splitting to it.
- The `defaultHealthFetch` and `container.exec` fallback paths are marked `/* istanbul ignore next */` — they're only exercisable in the integration smoke test (gated by `INTEGRATION_DOCKER=1`), not unit tests.