PHPackages                             laravel-chronicle/anchor-s3 - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. laravel-chronicle/anchor-s3

ActiveLibrary[File &amp; Storage](/categories/file-storage)

laravel-chronicle/anchor-s3
===========================

S3 Object Lock (WORM) anchoring adapter for the Chronicle audit ledger.

1.1.0(1w ago)101[3 issues](https://github.com/laravel-chronicle/anchor-s3/issues)[1 PRs](https://github.com/laravel-chronicle/anchor-s3/pulls)MITPHPPHP ^8.2CI passing

Since Jun 9Pushed 3d ago1 watchersCompare

[ Source](https://github.com/laravel-chronicle/anchor-s3)[ Packagist](https://packagist.org/packages/laravel-chronicle/anchor-s3)[ Docs](https://github.com/laravel-chronicle/anchor-s3)[ GitHub Sponsors](https://github.com/sponsors/ntoufoudis)[ RSS](/packages/laravel-chronicle-anchor-s3/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (14)Versions (4)Used By (0)

Chronicle S3 Object Lock Anchor
===============================

[](#chronicle-s3-object-lock-anchor)

[![Tests](https://github.com/laravel-chronicle/anchor-s3/actions/workflows/run-tests.yml/badge.svg)](https://github.com/laravel-chronicle/anchor-s3/actions/workflows/run-tests.yml)[![PHPStan](https://github.com/laravel-chronicle/anchor-s3/actions/workflows/phpstan.yml/badge.svg)](https://github.com/laravel-chronicle/anchor-s3/actions/workflows/phpstan.yml)[![Latest Version](https://camo.githubusercontent.com/1b32e08d5c80cbdb42da58c02814383d2310b28d1a3d2c8d3d6ebaadf2a11411/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c61726176656c2d6368726f6e69636c652f616e63686f722d73332e737667)](https://packagist.org/packages/laravel-chronicle/anchor-s3)[![Total Downloads](https://camo.githubusercontent.com/e458a84386478f1dd5da6dbb1204acf34756df4da28809ae99910475d525ba7d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c61726176656c2d6368726f6e69636c652f616e63686f722d73332e737667)](https://packagist.org/packages/laravel-chronicle/anchor-s3)[![License](https://camo.githubusercontent.com/7ab3e12a6c21045c7a708f06808e66722004350b78fb723ec32b4e3425447fa3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6c61726176656c2d6368726f6e69636c652f616e63686f722d73332e737667)](LICENSE)

An [S3 Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html) (WORM) anchoring adapter for [`laravel-chronicle/core`](https://github.com/laravel-chronicle/core).

It writes each checkpoint's digest - `sha256(id . chain_hash . created_at)` - to a **locked, versioned** S3 object in an independent trust domain. Even an attacker who rewrites the ledger and re-signs every checkpoint with a valid key still cannot alter the locked object, so `chronicle:verify --anchors` fails on the tampered checkpoint. The proof lives somewhere your application cannot reach, and therefore cannot quietly rewrite.

Installation
------------

[](#installation)

```
composer require laravel-chronicle/anchor-s3
```

The package auto-registers `AnchorS3ServiceProvider`, which binds a default `Aws\S3\S3Client` from `AWS_DEFAULT_REGION` and the standard AWS credential chain.

Choosing a retention mode
-------------------------

[](#choosing-a-retention-mode)

Object Lock has two retention modes, and the choice has consequences that are easy to underestimate. Read this before you create a bucket.

- **GOVERNANCE** - a principal holding `s3:BypassGovernanceRetention` can shorten or remove retention. This is the mode to start with, and the only mode you should use while you are evaluating the package or running anything you might want to clean up.
- **COMPLIANCE** - retention cannot be shortened or removed by *anyone*, including the AWS account root user, until it expires. This is what a regulated, production deployment wants: it is the mode that makes the WORM guarantee absolute. It is also, for exactly that reason, a mode you must not point at a test or throwaway bucket. An object written with a ten-year COMPLIANCE retention is billable and undeletable for ten years, with no support path that reverses it.

**Start with GOVERNANCE and short retention. Move to COMPLIANCE deliberately, once you know the bucket is the one you mean.** Note that the adapter defaults to `GOVERNANCE` when `mode` is unset or unrecognized.

Bucket setup (one-time)
-----------------------

[](#bucket-setup-one-time)

Object Lock requires a **versioned** bucket created with Object Lock enabled. The example below uses GOVERNANCE, which is the right choice for getting started:

```
aws s3api create-bucket \
  --bucket my-chronicle-anchors \
  --object-lock-enabled-for-bucket \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1
```

You can set a default retention rule on the bucket, but the adapter applies per-object retention on every write, so this is optional. If you do set one for a production bucket in COMPLIANCE mode, be certain the bucket is the one you intend:

```
# Production only. COMPLIANCE retention on these objects cannot be removed by anyone
# until it expires. Do not run this against a bucket you might want to delete.
aws s3api put-object-lock-configuration \
  --bucket my-chronicle-anchors \
  --object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":3650}}}'
```

Registration
------------

[](#registration)

Enable anchoring and register the provider in your published `config/chronicle.php`:

```
'anchoring' => [
    'enabled' => true,
    'providers' => [
        's3-object-lock' => [
            'provider' => \Chronicle\AnchorS3\S3ObjectLockAnchor::class,
            'bucket' => env('CHRONICLE_S3_ANCHOR_BUCKET'),
            'prefix' => 'chronicle/anchors',   // optional (default 'chronicle/anchors')
            'mode' => 'GOVERNANCE',            // 'GOVERNANCE' or 'COMPLIANCE'; unset defaults to GOVERNANCE
            'retain_days' => 3650,             // optional; defaults to 3650
        ],
    ],
],
```

New checkpoints are then anchored automatically. You can also anchor on demand and attest stored anchors with the Chronicle Artisan commands - see the [documentation](https://laravel-chronicle.dev/docs/artisan-commands) for the current command set, since those commands live in the core package.

Required IAM actions
--------------------

[](#required-iam-actions)

On the anchor bucket (both the bucket ARN and `arn:aws:s3:::my-chronicle-anchors/*`):

ActionUsed byWhy`s3:PutObject``anchor()`Write the digest object`s3:PutObjectRetention``anchor()`Apply per-object Object Lock retention`s3:GetObject``verify()`Re-read the exact object version`s3:GetObjectVersion``verify()`Read by `VersionId``s3:GetObjectRetention``verify()`Confirm lock metadata is presentGrant **no** `s3:DeleteObject*`. Anchors are write-once by design, and withholding delete permission is a second layer of defense behind the lock itself.

How it works
------------

[](#how-it-works)

- `anchor()` issues a single `PutObject` carrying the digest plus `ObjectLockMode` and `ObjectLockRetainUntilDate`. The receipt records `reference = "bucket/key@versionId"` and `proof = ETag`.
- `verify()` reads that exact version and passes only if the stored bytes equal the recomputed digest, lock metadata is present, and the ETag matches the receipt.

Because the retention headers travel on the same `PutObject` that writes the object, S3 rejects the write outright against a bucket without Object Lock - so a misconfigured bucket fails loudly and leaves nothing behind, rather than silently storing a deletable object that could be mistaken for a real anchor.

`verify()` performs one S3 read; it is deliberately **not** offline, unlike the core RFC 3161 anchor. That single network read is the price of an independent, account-isolated trust domain, and it is the point of this adapter.

Testing
-------

[](#testing)

The suite has three tiers. Only the first runs by default, and it needs nothing installed beyond the package's own dev dependencies.

```
composer test:unit       # unit tier: mocked S3, no Docker, no AWS, no network
composer test:localstack # LocalStack tier: needs Docker
composer test:aws        # real-AWS tier: opt-in, needs an AWS account
composer test:types      # PHPStan level 10
```

- **Unit** - verifies the adapter forms the right requests and handles responses and errors. This is what a first-time contributor runs; `composer install && composer test:unit` should be green with no further setup.
- **LocalStack** - runs the adapter against a real S3 API surface to catch request-shape and SDK regressions. Bring it up with `docker compose up -d localstack`. Note that LocalStack emulates the Object Lock *API* but does not *enforce* immutability, so this tier proves the conversation is correct, not that objects are actually locked.
- **Real AWS** - the only tier that proves the core guarantee: it anchors an object and confirms the object cannot be deleted. It is **GOVERNANCE-only by hard rule** - the suite refuses to run if the configured retention mode is anything else, precisely so a test can never create a COMPLIANCE object. See [CONTRIBUTING.md](CONTRIBUTING.md) for the bucket and IAM setup it expects.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE)

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance79

Regular maintenance activity

Popularity3

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 96% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~36 days

Total

2

Last Release

9d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/9a0e4562d814e61aaa5dc698cc667c6d6c90c98516142b60c0924b337c418355?d=identicon)[ntoufoudis](/maintainers/ntoufoudis)

---

Top Contributors

[![ntoufoudis](https://avatars.githubusercontent.com/u/93659348?v=4)](https://github.com/ntoufoudis "ntoufoudis (24 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravels3awsAuditchronicleWormanchoringobject-lock

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/laravel-chronicle-anchor-s3/health.svg)

```
[![Health](https://phpackages.com/badges/laravel-chronicle-anchor-s3/health.svg)](https://phpackages.com/packages/laravel-chronicle-anchor-s3)
```

###  Alternatives

[league/flysystem-aws-s3-v3

AWS S3 filesystem adapter for Flysystem.

1.7k285.7M1.1k](/packages/league-flysystem-aws-s3-v3)[aws/aws-sdk-php-laravel

A simple Laravel 9/10/11/12/13 service provider for including the AWS SDK for PHP.

1.7k38.2M85](/packages/aws-aws-sdk-php-laravel)[vinelab/cdn

Content Delivery Network (CDN) Package for Laravel

217244.0k1](/packages/vinelab-cdn)[kolay/xlsx-stream

Streaming XLSX reader and writer for PHP and Laravel. Constant memory regardless of file size, direct S3 multipart streaming, optional born-indexed random access.

437.9k](/packages/kolay-xlsx-stream)[unisharp/s3-presigned

An AWS S3 package for pre-signed upload purpose in Laravel and PHP.

151.8k](/packages/unisharp-s3-presigned)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
