first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# `rotation`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Resolves the rotation policy carried by every `createFileAuditSink`
instance and implements the rotation primitive itself — the
`rename(currentFile → currentFile.<utc-iso>.log)` step plus the
post-rotation state reset. The module owns both dimensions documented
in the spec: size-based (`maxSizeBytes`, default 100 MiB) and
time-based (`maxAgeMs`, default 24h). Either condition reaching its
limit triggers a rotation on the next write.
Rotation lives in a separate module so the sink stays focused on
queueing + writing. The retention module reads the policy through the
same `RotationState` so the operator-facing knobs are documented in
exactly one place.
## Features
- `resolveRotationPolicy(input?)` — Zod-equivalent validator returning
a frozen `RotationPolicy`. Defaults filled from the design doc.
- `shouldRotate(state, nowMs)` — pure predicate; size OR age satisfies.
- `rotate(fs, basePath, state, clock)` — performs the rename, updates
state, returns the rotated path for retention to inspect.
- Raises `AUDIT_FILE.ROTATION_FAILED` on rename failure with full
`cause` for forensic logging.
## Usage: Basic
```ts
import {
resolveRotationPolicy,
rotate,
shouldRotate,
} from "@suaveplan/api-audit-file/rotation";
const policy = resolveRotationPolicy({ maxSizeBytes: 1024 * 1024 });
const state = { openedAtMs: Date.now(), bytesWritten: 0, policy };
if (shouldRotate(state, Date.now())) {
await rotate(fs, "/var/log/audit.log", state, () => Date.now());
}
```
## Usage: Advanced
Disable a dimension by setting it to `Number.POSITIVE_INFINITY` —
useful when an operator wants pure time rotation regardless of size:
```ts
const policy = resolveRotationPolicy({
maxSizeBytes: Number.POSITIVE_INFINITY,
maxAgeMs: 60 * 60 * 1000, // hourly
});
```
In tests, drive rotation deterministically with a `FakeClock` from
`@suaveplan/testing`:
```ts
import { FakeClock } from "@suaveplan/testing";
const clock = new FakeClock(0);
const state = { openedAtMs: 0, bytesWritten: 0, policy };
clock.advance(61_000);
await rotate(fs, basePath, state, clock.read);
```
## API Reference
- `interface RotationPolicy`
- `maxSizeBytes: number` — bytes before size rotation; default 100 MiB.
- `maxAgeMs: number` — ms before time rotation; default 24h.
- `maxFiles: number` — kept rotated files; default 30.
- `interface RotationState`
- `openedAtMs: number` — wall-clock open time of current segment.
- `bytesWritten: number` — running byte count.
- `policy: RotationPolicy` — frozen at construction.
- `resolveRotationPolicy(input?): RotationPolicy`
- `shouldRotate(state, nowMs): boolean`
- `rotate(fs, basePath, state, clock): Promise<string>` — returns the
rotated file path.
## Implementation Notes
The rotated suffix replaces `:` with `-` so the resulting filename is
Windows-portable and grep-friendly across shells. State mutation
happens AFTER the rename succeeds — a failed rename leaves the original
state intact, so the next call retries against the same segment rather
than corrupting accounting. `bytesWritten` resets to 0 instead of
re-stat'ing the new empty file; the next successful append updates it
in lock-step with the on-disk length.