storage: an s3: driver as a package of its own #20

Open
opened 2026-10-05 21:07:11 +00:00 by orion · 5 comments
Collaborator

Context

The registry stores through std/storage; production wants an S3 bucket. This is the s3: driver that URI.md section 11 names as the first Storage driver outside std, and milestone 10's "S3 as a package of its own".

Goal

A package torb/s3 with S3Storage, a Storage for the scheme s3: that works against AWS S3 and S3-compatible servers (MinIO, R2, Hetzner object storage). The registry wiring is split into #242.

Where it lives

FIRST-PARTY.md sections 2 to 5 decide it: the package is its own repository torb/s3 in the organisation torb on git.torb.dev, made from torb/package-template, with its own semantic version starting at 0.1.0, published to the registry by trusted publishing. It is not a directory of this repository. The work therefore lands as pull requests of torb/s3, not of torbscript/language.

Design

  • type S3Storage with Storage (a shared type, like FileStorage), schemes() answers ["s3"]; S3Storage.of(settings, credentials); S3Credentials (access key id, secret key, optional session token) with S3Credentials.fromEnvironment(); secrets never appear in Show output or in a StorageFailure text.
  • Addressing: s3://bucket/key means bucket and object key. Virtual-hosted style for AWS (no endpoint set), path style when a custom endpoint is given (MinIO and friends); both selectable in the settings.
  • Members, one HTTP request each through std/http:
    • read: GetObject; a 404 with NoSuchKey is StorageFailure.NotFound.
    • exists: HeadObject.
    • write: PutObject of the whole Bytes (no multipart in this version; the limit is documented in the README).
    • list: ListObjectsV2 with delimiter=/ and continuation tokens, answering the Uris of the entries below the prefix.
    • delete: a HeadObject first so that a missing key is NotFound like in FileStorage and MemoryStorage (S3 itself answers 204 for a missing key), then DeleteObject.
  • Signature Version 4 in a file of its own, with HmacSha256 and Sha256 of std/digest and the signing date from Clock.timestamp() (a clock that tests can inject, so signatures are deterministic); signed payload hash, no chunked uploads.
  • Errors: the XML <Error><Code> and <Message> map to NotFound (NoSuchKey, NoSuchBucket) and Refused (everything else, with the code and the request id in the reason); a transport failure is Refused.

Acceptance criteria

  • The repository torb/s3 exists from the package template; its project.trb names torb/s3, license MIT, language set; torb check and torb test tests/unit pass in its forge CI
  • S3Storage implements Storage: read, exists, write, list, delete as designed above, readText and writeText through the defaults
  • Signature Version 4 reproduces the values of AWS's published test vectors: canonical request, string to sign, signing key and Authorization header, with the clock injected
  • The driver is tested against recorded S3 responses served by a loopback double in tests/unit (the torb/postgres pattern): success, 404 NoSuchKey, 403 AccessDenied, a listing over two pages, an empty listing, a key with characters that need percent-encoding. The tests need no network, no bucket and no credentials; the loop never touches a real bucket
  • Credentials appear in no log line, no Show text and no failure message (a test searches for them)
  • Storage.registry([FileStorage(), S3Storage.of(...)]) dispatches s3://... to the driver and still names s3 in its "the registry knows ..." message (test)
  • The README says what the package is, the settings, the S3 servers supported, what is not supported (multipart, presigned URLs, ACLs), developing and publishing
  • Listings and errors are read with std/xml (#78), not with a private parser

Seed impact

None. No syntax, native, driver flag or std name looked up by the compiler changes; the package is pure TorbScript over std.

Dependencies

  • #78 (std/xml): FIRST-PARTY.md section 8 gap 24 says "torb/s3 waits" for it, and the last criterion keeps one XML implementation instead of a private subset parser (the first version of this issue allowed one; decided by the loop, see the comment).
  • The repository torb/s3, its trusted-publisher entry on the registry and its first publication are outside this loop's contract (its scope is torbscript/language; publishing and organisation settings are forbidden to the loop).
  • Done already: HMAC (std/digest), the wall clock (Clock.timestamp), Base64 and hexadecimal, std/http over TLS.
  • #242 (registry wiring) depends on this issue.

Size

One pull request of torb/s3 as two Conventional Commits: feat: Signature Version 4 and feat: S3Storage for s3: URIs. If it grows past that, the README and the test double go in a third docs/test commit, not a second issue.

Source

Moved from the planning lists of the docs; the design record stays the specification.

## Context The registry stores through `std/storage`; production wants an S3 bucket. This is the `s3:` driver that URI.md section 11 names as the first `Storage` driver outside `std`, and milestone 10's "S3 as a package of its own". ## Goal A package `torb/s3` with `S3Storage`, a `Storage` for the scheme `s3:` that works against AWS S3 and S3-compatible servers (MinIO, R2, Hetzner object storage). The registry wiring is split into #242. ## Where it lives FIRST-PARTY.md sections 2 to 5 decide it: the package is **its own repository `torb/s3` in the organisation `torb` on git.torb.dev**, made from `torb/package-template`, with its own semantic version starting at `0.1.0`, published to the registry by trusted publishing. It is not a directory of this repository. The work therefore lands as pull requests of `torb/s3`, not of `torbscript/language`. ## Design - `type S3Storage with Storage` (a `shared type`, like `FileStorage`), `schemes()` answers `["s3"]`; `S3Storage.of(settings, credentials)`; `S3Credentials` (access key id, secret key, optional session token) with `S3Credentials.fromEnvironment()`; secrets never appear in `Show` output or in a `StorageFailure` text. - Addressing: `s3://bucket/key` means bucket and object key. Virtual-hosted style for AWS (no endpoint set), path style when a custom endpoint is given (MinIO and friends); both selectable in the settings. - Members, one HTTP request each through `std/http`: - `read`: `GetObject`; a 404 with `NoSuchKey` is `StorageFailure.NotFound`. - `exists`: `HeadObject`. - `write`: `PutObject` of the whole `Bytes` (no multipart in this version; the limit is documented in the README). - `list`: `ListObjectsV2` with `delimiter=/` and continuation tokens, answering the `Uri`s of the entries below the prefix. - `delete`: a `HeadObject` first so that a missing key is `NotFound` like in `FileStorage` and `MemoryStorage` (S3 itself answers 204 for a missing key), then `DeleteObject`. - Signature Version 4 in a file of its own, with `HmacSha256` and `Sha256` of `std/digest` and the signing date from `Clock.timestamp()` (a clock that tests can inject, so signatures are deterministic); signed payload hash, no chunked uploads. - Errors: the XML `<Error><Code>` and `<Message>` map to `NotFound` (`NoSuchKey`, `NoSuchBucket`) and `Refused` (everything else, with the code and the request id in the reason); a transport failure is `Refused`. ## Acceptance criteria - [ ] The repository `torb/s3` exists from the package template; its `project.trb` names `torb/s3`, license MIT, `language` set; `torb check` and `torb test tests/unit` pass in its forge CI - [ ] `S3Storage` implements `Storage`: `read`, `exists`, `write`, `list`, `delete` as designed above, `readText` and `writeText` through the defaults - [ ] Signature Version 4 reproduces the values of AWS's published test vectors: canonical request, string to sign, signing key and `Authorization` header, with the clock injected - [ ] The driver is tested against **recorded S3 responses** served by a loopback double in `tests/unit` (the `torb/postgres` pattern): success, 404 `NoSuchKey`, 403 `AccessDenied`, a listing over two pages, an empty listing, a key with characters that need percent-encoding. The tests need no network, no bucket and no credentials; the loop never touches a real bucket - [ ] Credentials appear in no log line, no `Show` text and no failure message (a test searches for them) - [ ] `Storage.registry([FileStorage(), S3Storage.of(...)])` dispatches `s3://...` to the driver and still names `s3` in its "the registry knows ..." message (test) - [ ] The README says what the package is, the settings, the S3 servers supported, what is not supported (multipart, presigned URLs, ACLs), developing and publishing - [ ] Listings and errors are read with `std/xml` (#78), not with a private parser ## Seed impact None. No syntax, native, driver flag or std name looked up by the compiler changes; the package is pure TorbScript over `std`. ## Dependencies - **#78 (`std/xml`)**: FIRST-PARTY.md section 8 gap 24 says "`torb/s3` waits" for it, and the last criterion keeps one XML implementation instead of a private subset parser (the first version of this issue allowed one; decided by the loop, see the comment). - **The repository `torb/s3`, its trusted-publisher entry on the registry and its first publication** are outside this loop's contract (its scope is `torbscript/language`; publishing and organisation settings are forbidden to the loop). - Done already: HMAC (`std/digest`), the wall clock (`Clock.timestamp`), Base64 and hexadecimal, `std/http` over TLS. - #242 (registry wiring) depends on this issue. ## Size One pull request of `torb/s3` as two Conventional Commits: `feat: Signature Version 4` and `feat: S3Storage for s3: URIs`. If it grows past that, the README and the test double go in a third `docs`/`test` commit, not a second issue. ## Source - [ROADMAP.md, the package manager](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/ROADMAP.md#L96) - [ROADMAP.md, milestone 10 file systems](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/ROADMAP.md#L159) - [URI.md slice 12](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/URI.md#L2003) - FIRST-PARTY.md sections 2 to 5 (vendor, line to `std`, repository conventions, order) and section 8 gap 24 - RELEASE.md section 7.11 (the registry stores through the storage layer) _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop). s3: Storage driver as a package (ROADMAP milestone 10, URI.md slice 12); built against recorded responses, no real bucket or credentials touched by the loop. Package layout and name may need an owner decision, to settle in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued)

[loop:torbscript] intake: in scope (author orion, label loop). `s3:` Storage driver as a package (ROADMAP milestone 10, URI.md slice 12); built against recorded responses, no real bucket or credentials touched by the loop. Package layout and name may need an owner decision, to settle in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261009 until 2026-10-09T03:18:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261009 until 2026-10-09T03:18:00Z
Author
Collaborator

[loop:torbscript] decided by the loop: (1) name and layout: package torb/s3, type S3Storage, in a repository of its own under the organisation torb (FIRST-PARTY.md sections 2 to 5 already decide vendor, repository per package, template and versioning; the existing torb/sql, torb/postgres and torb/redis follow it). Not a directory of this repository, so the registry wiring (tools/registry, which is in this repository) is split into #242. (2) Listings and errors wait for std/xml (#78) instead of a private XML subset: the design record lists it as the one thing torb/s3 waits for (section 8 gap 24), a throwaway parser would be written twice, and the cost of waiting is zero because nothing else blocks. This replaces the issue's earlier note that the driver may parse the few elements itself. (3) Path style for custom endpoints and virtual-hosted style for AWS; PutObject as one request without multipart; delete does a HeadObject first so a missing key is NotFound as in FileStorage and MemoryStorage; the clock is injectable so signatures are testable against AWS's published vectors. All internal technical choices, no public commitment.

[loop:torbscript] decided by the loop: (1) name and layout: package `torb/s3`, type `S3Storage`, in a repository of its own under the organisation `torb` (FIRST-PARTY.md sections 2 to 5 already decide vendor, repository per package, template and versioning; the existing `torb/sql`, `torb/postgres` and `torb/redis` follow it). Not a directory of this repository, so the registry wiring (`tools/registry`, which is in this repository) is split into #242. (2) Listings and errors wait for `std/xml` (#78) instead of a private XML subset: the design record lists it as the one thing `torb/s3` waits for (section 8 gap 24), a throwaway parser would be written twice, and the cost of waiting is zero because nothing else blocks. This replaces the issue's earlier note that the driver may parse the few elements itself. (3) Path style for custom endpoints and virtual-hosted style for AWS; `PutObject` as one request without multipart; `delete` does a `HeadObject` first so a missing key is `NotFound` as in `FileStorage` and `MemoryStorage`; the clock is injectable so signatures are testable against AWS's published vectors. All internal technical choices, no public commitment.
Author
Collaborator

[loop:torbscript] refine: requirements brought to the definition of ready (goal, testable criteria against recorded responses and AWS's signing vectors, design records, no seed impact, size of one pull request as two commits) and the issue body rewritten; the registry half split off as #242. Not set ready, next: waiting (dependency): #78 (std/xml, FIRST-PARTY.md gap 24) is open, and the package belongs in a repository torb/s3 that does not exist yet, which is outside this loop's contract (scope torbscript/language; repository creation, trusted publisher and publishing are not the loop's). Refine worker usage: roughly 110k tokens.

[loop:torbscript] refine: requirements brought to the definition of ready (goal, testable criteria against recorded responses and AWS's signing vectors, design records, no seed impact, size of one pull request as two commits) and the issue body rewritten; the registry half split off as #242. Not set `ready`, next: waiting (dependency): #78 (`std/xml`, FIRST-PARTY.md gap 24) is open, and the package belongs in a repository `torb/s3` that does not exist yet, which is outside this loop's contract (scope `torbscript/language`; repository creation, trusted publisher and publishing are not the loop's). Refine worker usage: roughly 110k tokens.
Author
Collaborator

[loop:torbscript] refine: still blocked, nothing to refine further. The issue is at the definition of ready except for two external points: #78 (std/xml, epic; step 3a #570 reads S3 listings) is not done, and the repository torb/s3 is outside this loop's contract (repository creation, trusted publisher and publishing are not the loop's). next: waiting (dependency #78 step 3a / repo torb/s3 by the owner) until 2026-11-05T08:00:00Z

[loop:torbscript] refine: still blocked, nothing to refine further. The issue is at the definition of ready except for two external points: #78 (`std/xml`, epic; step 3a #570 reads S3 listings) is not done, and the repository `torb/s3` is outside this loop's contract (repository creation, trusted publisher and publishing are not the loop's). next: waiting (dependency #78 step 3a / repo torb/s3 by the owner) until 2026-11-05T08:00:00Z
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#20
No description provided.