# Object lock

Object lock is write-once-read-many (WORM) protection: a locked object version cannot be deleted or overwritten until
its retention expires or its legal hold is cleared. It is how you meet compliance and legal-hold requirements where data
must be provably immutable for a window of time.

Object lock has two independent controls:

- **Retention** has a mode and a retain-until date. Until that date, the version is protected.
- **Legal hold** is a separate `ON`/`OFF` flag with no expiry. While it is on, the version is protected regardless of
  retention.

A version is deletable only when no retention window is active and no legal hold is on.

## Retention modes

| Mode         | Behavior                                                                                                                            |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `GOVERNANCE` | Protects the version; a sufficiently privileged caller could bypass it on AWS. Lockwell does not implement that bypass (see below). |
| `COMPLIANCE` | Immutable until the retain-until date. No one can shorten or remove it, including the account root.                                 |

For both modes, an active retention deadline can be **extended** to a later date but **never shortened**. The server
rejects a write that would move the deadline earlier or weaken the mode.

## Enable object lock at bucket creation

Object lock can only be turned on when the bucket is created, and a bucket with object lock has versioning enabled (lock
requires versioning). On the native client, pass `objectLock: true` (Node) or set `ObjectLockEnabled` (Go) on the
create-bucket input. A bucket created without object lock cannot have it added later.

:::: native

::: code-group

```ts [Node]
await native.createBucket("vault", { objectLock: true, versioning: true });
```

```go [Go]
_, err := native.CreateBucket(ctx, lockwellnative.CreateBucketInput{
    Name:              "vault",
    Versioning:        "enabled",
    ObjectLockEnabled: true,
})
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.CreateBucketOptions;

nativeClient.createBucket("vault", new CreateBucketOptions("enabled", true));
```

:::

::::

For Java ERP integrations, `StorageProfiles.fiscalArchiveBucket()` returns this same versioning + Object Lock create
shape, and `StorageProfiles.fiscalArchiveWrite(...)` pairs a no-overwrite, checksummed, idempotent write with an
explicit `COMPLIANCE` retention spec. The ERP still supplies the retain-until date.

:::: s3

The S3 client enables object lock at create and takes a default retention rule (a mode and a number of days). New
objects inherit that default unless you set per-object retention on the write:

::: code-group

```ts [Node]
// The Node S3 client enables object lock by passing the lock config at create:
await s3.createBucket("vault");
// then set a default retention rule and per-object retention via PutObject (below).
```

```go [Go]
err := s3.CreateBucket(ctx, "vault",
    lockwellsdk.WithObjectLockEnabled(lockwellsdk.ObjectLockCompliance, 365))
```

```java [Java]
// createBucket(name, defaultMode, defaultDays):
s3.createBucket("vault", "COMPLIANCE", 365);
```

:::

::::

## Set and read retention and legal hold

The native client has dedicated set/get methods for retention and legal hold, applied after the object exists. They
enforce the same WORM gate as the S3 path. Retention takes a mode (`GOVERNANCE` or `COMPLIANCE`) and an RFC3339
retain-until date; legal hold is a separate flag.

:::: native

::: code-group

```ts [Node]
await native.setObjectRetention("vault", "ledger.json", {
  mode: "COMPLIANCE",
  retainUntil: "2030-01-01T00:00:00Z",
});
await native.setObjectLegalHold("vault", "ledger.json", true);

const r = await native.getObjectRetention("vault", "ledger.json"); // { mode, retainUntil }
const held = await native.getObjectLegalHold("vault", "ledger.json"); // boolean
```

```go [Go]
_, err := native.SetObjectRetention(ctx, "vault", "ledger.json", "COMPLIANCE", "2030-01-01T00:00:00Z")
_, err = native.SetObjectLegalHold(ctx, "vault", "ledger.json", "ON")

r, err := native.GetObjectRetention(ctx, "vault", "ledger.json")
fmt.Println(r.Mode, r.RetainUntil)

hold, err := native.GetObjectLegalHold(ctx, "vault", "ledger.json")
fmt.Println(hold.Status) // "ON" or "OFF"
```

```java [Java]
nativeClient.setObjectRetention("vault", "ledger.json", "COMPLIANCE", "2030-01-01T00:00:00Z");
nativeClient.setObjectLegalHold("vault", "ledger.json", "ON");

var r = nativeClient.getObjectRetention("vault", "ledger.json"); // mode() + retainUntil()
var hold = nativeClient.getObjectLegalHold("vault", "ledger.json"); // status()
```

:::

::::

The Go native `SetObjectLegalHold` / `GetObjectLegalHold` use the string `"ON"` / `"OFF"`; the Node native client takes
and returns a boolean; the Java native client takes and returns the string status. Pass a `versionId` option (Node) to
target a specific version; without it the current version is used.

:::: s3

S3 clients can set a version's retention and legal hold at write time, as options on `PutObject`. Mode and retain-until
must be supplied together, and the bucket must have Object Lock enabled. The Go S3 client also supports post-write
updates with typed `SetObjectRetention` / `SetObjectLegalHold` methods (and `Put...` aliases named for the S3
operations); pass `WithVersionID` to mutate a non-current version.

::: code-group

```ts [Node]
await s3.putObject("vault", "ledger.json", body, {
  objectLockMode: "COMPLIANCE",
  objectLockRetainUntil: "2030-01-01T00:00:00Z", // RFC3339
  objectLockLegalHold: true,
});
```

```go [Go]
until := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
_, err := s3.PutObject(ctx, "vault", "ledger.json", body,
    lockwellsdk.WithObjectLockRetention(lockwellsdk.ObjectLockCompliance, until),
    lockwellsdk.WithObjectLockLegalHold(true))
```

```java [Java]
var opts = new PutOptions()
    .objectLock("COMPLIANCE", "2030-01-01T00:00:00Z") // mode + RFC3339 retain-until
    .legalHold(true);
s3.putObject("vault", "ledger.json", body, opts);
```

:::

The Go S3 client uses an `ObjectRetention` with a mode enum and `time.Time` for a post-write retention update; legal
hold takes a boolean. Both mutation methods emit the standard S3 XML request shape and reject an invalid mode, zero
date, or non-future retention date before transport. `GetObjectRetention` and `GetObjectLegalHold` read the current lock
state. All four Go methods target a version through `WithVersionID`; without it, they use the current version. A version
with no retention reports a not-found error from `GetObjectRetention`.

::: code-group

```ts [Node]
const r = await s3.getObjectRetention("vault", "ledger.json");
console.log(r.mode, r.retainUntilDate); // "COMPLIANCE" "2030-01-01T00:00:00Z"

const held = await s3.getObjectLegalHold("vault", "ledger.json"); // boolean
```

```go [Go]
until := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
err := s3.SetObjectRetention(ctx, "vault", "ledger.json", lockwellsdk.ObjectRetention{
    Mode:            lockwellsdk.ObjectLockCompliance,
    RetainUntilDate: until,
}, lockwellsdk.WithVersionID(versionID))
err = s3.SetObjectLegalHold(ctx, "vault", "ledger.json", true,
    lockwellsdk.WithVersionID(versionID))

r, err := s3.GetObjectRetention(ctx, "vault", "ledger.json")
if lockwellsdk.IsNotFound(err) {
    // no retention set on this version
}
fmt.Println(r.Mode, r.RetainUntilDate)

held, err := s3.GetObjectLegalHold(ctx, "vault", "ledger.json") // bool
```

```java [Java]
var r = s3.getObjectRetention("vault", "ledger.json"); // mode() + retainUntilDate()
boolean held = s3.getObjectLegalHold("vault", "ledger.json");
```

:::

::::

## What protection blocks

While a version is under active retention or a legal hold:

- A delete of that version is refused. S3 returns `403 AccessDenied`; the native API returns `412` with
  `retention_blocked` or `legal_hold_blocked`. In a versioned bucket, deleting the key without a version id still writes
  a delete marker (the protected version is untouched and recoverable).
- An overwrite that would replace the protected version's data is refused.
- Shortening the retain-until date or weakening the mode is refused; extending the date is allowed.

A legal hold has no expiry. It stays until you explicitly set it `OFF` (a boolean `false` on the Node native client),
even after retention expires.

## No governance bypass

On AWS, a caller with the `s3:BypassGovernanceRetention` permission can delete a `GOVERNANCE`-mode object early by
sending a bypass header. Lockwell does not implement that bypass. A `GOVERNANCE` retention is enforced the same way as
`COMPLIANCE` until its date passes, on both the S3 and native paths. There is no header, scope, or admin call that
removes an active retention early. This is a deliberate non-goal that keeps the WORM guarantee honest.

::: info The governance bypass is a deliberate non-goal. `GOVERNANCE` and `COMPLIANCE` are equally immutable here, so do
not rely on object lock for a deletable-by-an-operator escape hatch. :::

If you need a deletable-by-an-operator escape hatch, do not rely on object lock for it. Object lock means the data is
immutable for the window you set.

## Related

- [Versioning](/guide/versioning) for the version history object lock protects.
- [Deleting objects](/guide/deleting-objects) for how a delete behaves when a version is protected.
- [Errors and retries](/guide/errors-and-retries) for the surface-specific error a blocked delete returns.
- [S3 operations reference](/reference/s3-operations) for the full operation matrix.
