S3 Object Lock
S3 Object Lock is AWS S3's built-in WORM control: GOVERNANCE mode blocks delete/overwrite except for a privileged bypass caller; COMPLIANCE mode blocks it for everyone, including AWS account root, until a retain-until date. Caisson's audit-worm store writes every artifact with a conditional write-once PUT and a matching ObjectLockMode + retain-until date, so the S3 lock provably matches the database row.
In code
assertSafeKey(key);
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: body,
ContentLength: body.byteLength,
// Write-once: S3 fails a conditional PUT to an existing key with 412 (TM-H).
IfNoneMatch: "*",
// Retention lock: object date == DB `retain_until` (ADR-0051/0054).
ObjectLockMode: this.mode,
ObjectLockRetainUntilDate: opts.retainUntil,
});
try {
await this.client.send(command);
} catch (err) {
// 412 Precondition Failed == the key already holds an immutable object (WORM violation).
if (httpStatusOf(err) === 412) throw new ArtifactExistsError(key);
throw err;
}How it holds
Write-once PUT enforces WORM before any lock check
Every put() is a conditional PutObjectCommand with IfNoneMatch: "*", S3 answers 412 on an existing key, which the store maps to ArtifactExistsError. No overwrite code path exists independent of the lock itself.
COMPLIANCE mode sits behind a three-belt fail-closed gate
assertComplianceAllowed refuses COMPLIANCE under a test runner, refuses it outside NODE_ENV === "production", and refuses it without a typed IrreversibleComplianceOptIn naming the exact bucket, all three checked at construction, before any S3 call.
Retention only ever extends or escalates, never shortens
extendRetention rejects any date not strictly later than the current lock; escalateToCompliance rejects any date earlier than the current lock. Both read the authoritative lock via GetObjectRetention first, HeadObject silently omits lock fields without s3:GetObjectRetention, which would fail open.
The S3 lock date is the database row's date
ObjectLockRetainUntilDate is set to the caller's opts.retainUntil on every write, and metaFrom projects it straight back out on get/head, so the retain-until an auditor reads off the S3 object is the same value stored in the DB row, not a derived approximation.