WORM retention policy
A WORM retention policy is the rule set fixing how long a stored object stays immutable and under which S3 Object-Lock mode: GOVERNANCE (operator-overridable) or COMPLIANCE (locked to the root, no exceptions). Caisson's @caisson/audit-worm computes a 6-year-floor retain-until date and only ever lets that date move later, never earlier.
In code
async extendRetention(
key: string,
newRetainUntil: Date,
): Promise<ArtifactMeta> {
assertSafeKey(key);
assertValidRetainUntil(newRetainUntil);
const current = await this.currentRetainUntil(key);
if (
current !== undefined &&
newRetainUntil.getTime() <= current.getTime()
) {
throw new ValidationError(
"audit-worm: retention can only be EXTENDED — the new date must be strictly later than the current lock (ADR-0202)",
{
key,
currentRetainUntil: current.toISOString(),
requested: newRetainUntil.toISOString(),
},
);
}
await this.putRetention(key, this.mode, newRetainUntil);
const meta = await this.headOrThrow(key);
return { ...meta, retainUntil: newRetainUntil };
}
async escalateToCompliance(
key: string,
retainUntil: Date,
optIn: IrreversibleComplianceOptIn,
): Promise<ArtifactMeta> {
assertSafeKey(key);
assertValidRetainUntil(retainUntil);
this.assertComplianceAllowed(optIn);
const current = await this.currentRetainUntil(key);
if (current !== undefined && retainUntil.getTime() < current.getTime()) {
throw new ValidationError(
"audit-worm: COMPLIANCE escalation cannot shorten retention — the date must be at or later than the current lock (ADR-0202)",
{
key,
currentRetainUntil: current.toISOString(),
requested: retainUntil.toISOString(),
},
);
}
await this.putRetention(key, "COMPLIANCE", retainUntil);
const meta = await this.headOrThrow(key);
return { ...meta, retainUntil };
}How it holds
6-year floor, never silently shortened
retainUntilFrom(now, years) throws a ValidationError if years is below MIN_RETENTION_YEARS (6, matching HIPAA §164.316(b)(2) and SEC 17a-4): a too-short term is rejected outright, never auto-extended or silently accepted.
Extend-only, never earlier
extendRetention reads the live lock via GetObjectRetention (never HeadObject, which can silently omit lock fields under a permission gap) and refuses any date that isn't strictly later than the current one.
COMPLIANCE escalation is a three-belt opt-in
Selecting COMPLIANCE mode requires a typed IrreversibleComplianceOptIn naming the exact bucket, and is refused under NODE_ENV=test and outside NODE_ENV=production; no code path reaches it by accident.
Every escalation is chain-evidenced
escalateRetention applies the store change first, then appends a retention.escalated record to the tenant's audit chain; if the chain append fails, the retention change is already applied, and the whole call throws loudly to flag the evidence gap for reconciliation. It is never silently swallowed.