openvibers/openvibe.media Commits

S3 docs: the surface answers at s3.openvibe.media (DNS-only since 2026-10-21, so objects may pass 100 MB); s3.openvibe.network stays the same surface behind Cloudflare (#152)

OP OpenVibers committed 23 days ago · 6a3a1c7b86 · 3 files, +14 −12

Changes

ddiff --git a/README.md b/README.md
iindex 2134916..b57df1e 100644
---- a/README.md
++++ b/README.md
@@@ -60,7 +60,7 @@ key ([Tenancy & auth](#tenancy--auth)).
|---|---|---|---|
| `media.resource.read` | first-party | `GET /api/v1/resources`, `GET /api/v1/resources/:ovrn` | the authority resource index ([Resource index](#resource-index-apiv1resources-first-party)). Active in openvibe-contracts since 0.108.0; the route enforces it (a Network service token holding it, loopback, audience `openvibe.media`) |
| `media.resource.control` | first-party | `POST /api/v1/resources/control` | `delete` a person's (or their project's) object: the soft delete, restorable until its retention ends ([Resource index](#resource-index-apiv1resources-first-party)). Active in openvibe-contracts since 0.169.0 |
--| `media.object.s3` | public (an access key) | the S3 surface at `s3.openvibe.network` (`s3.openvibe.media` once its DNS record exists; [docs/s3.md](docs/s3.md)) | PutObject, GetObject, HeadObject, DeleteObject(s), ListObjects(V2), ListBuckets, HeadBucket, presigned GET/PUT; a bucket is a zone |
++| `media.object.s3` | public (an access key) | the S3 surface at `s3.openvibe.media` ([docs/s3.md](docs/s3.md)) | PutObject, GetObject, HeadObject, DeleteObject(s), ListObjects(V2), ListBuckets, HeadBucket, presigned GET/PUT; a bucket is a zone |
| `media.access_key.manage` | first-party | `POST/GET/DELETE /api/v1/projects/:project/access-keys` | S3 access keys for a project admin, the secret shown once (Zone's console, Services) |
| `media.zone.provision` | first-party (Zone) | `PUT/GET /internal/zones/:id` | OpenVibe.Zone's zone records, applied by revision; usage and deletion progress |
@@@ -594,7 +594,7 @@ counts its viewer for the day, identifying no one (see Storage tiering).
## S3 surface and zones
--Media answers a subset of S3 at `https://s3.openvibe.network` (ADR-031; `https://s3.openvibe.media` once that name's DNS record exists). Each bucket is a zone of a project environment, and zones are managed by OpenVibe.Zone. Media issues and verifies the `ovk_` access keys, and every object stays an ordinary Media object.
++Media answers a subset of S3 at `https://s3.openvibe.media` (ADR-031; DNS-only since 2026-10-21, so an object may be larger than Cloudflare's 100 MB; `https://s3.openvibe.network` is the same surface behind Cloudflare). Each bucket is a zone of a project environment, and zones are managed by OpenVibe.Zone. Media issues and verifies the `ovk_` access keys, and every object stays an ordinary Media object.
See [docs/s3.md](docs/s3.md) for the operations, the rules and the tests.
ddiff --git a/deploy/nginx/s3.openvibe.media.conf b/deploy/nginx/s3.openvibe.media.conf
iindex 143db42..ccb7345 100644
---- a/deploy/nginx/s3.openvibe.media.conf
++++ b/deploy/nginx/s3.openvibe.media.conf
@@@ -2,10 +2,10 @@
# request whose Host is an S3 host (MEDIA_S3_HOSTS, server/s3/routes.js). proxy_pass has no URI part on purpose: nginx
# then passes the request line exactly as the client sent it, and SigV4 signs that path (a URI part would make nginx
# decode and re-encode it, and every key with an escaped character would fail its signature).
--# Certificate: the openvibe.media wildcard. DNS: s3.openvibe.media needs its record (the owner's Cloudflare action);
--# until it has one, s3.openvibe.network (the openvibe.network wildcard name and certificate) is the same surface, and
--# OpenVibe.Zone names it (ZONE_S3_ENDPOINT). MEDIA_S3_HOSTS lists both. Cloudflare's proxy carries at most 100 MB a
--# request on this plan, so one object over S3 is at most 100 MB until the name is DNS-only.
++# Certificate: the openvibe.media wildcard. DNS: s3.openvibe.media is a DNS-only record (since 2026-10-21), so clients
++# reach this vhost directly and an object may exceed the 100 MB a request that Cloudflare's proxy carries on this plan;
++# OpenVibe.Zone names it (ZONE_S3_ENDPOINT). s3.openvibe.network (the openvibe.network wildcard name and certificate,
++# behind Cloudflare) is the interim name of the same surface. MEDIA_S3_HOSTS lists both.
limit_req_zone $binary_remote_addr zone=media_s3:10m rate=50r/s;
server {
ddiff --git a/docs/s3.md b/docs/s3.md
iindex a0f5ecd..4d9efdf 100644
---- a/docs/s3.md
++++ b/docs/s3.md
@@@ -1,8 +1,10 @@
# The S3 surface and zones
--OpenVibe.Media answers a subset of the Amazon S3 API at `https://s3.openvibe.network`, path style
--(`/<bucket>/<key>`). Its intended name, `https://s3.openvibe.media`, has no DNS record yet: nginx already answers both
--names, and OpenVibe.Zone's pages show the `.network` one through `ZONE_S3_ENDPOINT`. The design is ADR-031 in OpenVibe.Contracts, with its two amendments. A **bucket is a zone**:
++OpenVibe.Media answers a subset of the Amazon S3 API at `https://s3.openvibe.media`, path style
++(`/<bucket>/<key>`). Since 2026-10-21 that name is a DNS-only record, so requests reach the server directly and one
++object may be as large as Media allows (Cloudflare's proxy would stop a request at 100 MB). OpenVibe.Zone's pages
++name it through `ZONE_S3_ENDPOINT`. The interim name, `https://s3.openvibe.network`, still answers the same
++surface through Cloudflare. The design is ADR-031 in OpenVibe.Contracts, with its two amendments. A **bucket is a zone**:
a named object zone of one project environment. Zones are made and deleted only through OpenVibe.Zone
(https://openvibe.zone).
@@@ -44,7 +46,7 @@ a named object zone of one project environment. Zones are made and deleted only
- **Overwrite.** The new bytes are stored first. Then, in one transaction under the tenant's quota lock, the previous object goes through Media's deletion path and the new one becomes ready. A held object is neither overwritten nor deleted (`AccessDenied`).
- **Quotas.** A write is checked against the zone's `quota_bytes` and the environment's quota, before the bytes arrive and again after.
- **Zone deletion.** When Zone pushes state `deleting`, the zone answers `NoSuchBucket` at once. The minute sweep (`zones.sweepDeleting`) tombstones every object with no restore window and purges every copy. The status reports `objects_remaining` and `held_objects` until nothing is left. Then Zone marks the zone `deleted`: its name is retired, and keys scoped only to it are revoked.
--- **HEAD through a proxy.** Cloudflare forwards a HEAD as a GET for paths it may cache (by extension: `.jpg`, `.bin`, `.zip` …), but SigV4 signs the method. A header-signed request with no body that arrives as GET and verifies only as HEAD is served as the GET, and the proxy returns just the headers. A presigned URL never gets this, because one may be shared for a HEAD alone; nor does any other method. Every S3 response is `Cache-Control: private, no-store`, so the edge never caches one (`cf-cache-status: BYPASS`). The fix at the edge itself is a DNS-only `s3.openvibe.media`.
++- **HEAD through a proxy.** On the Cloudflare-proxied `s3.openvibe.network`, Cloudflare forwards a HEAD as a GET for paths it may cache (by extension: `.jpg`, `.bin`, `.zip` …), but SigV4 signs the method. A header-signed request with no body that arrives as GET and verifies only as HEAD is served as the GET, and the proxy returns just the headers. A presigned URL never gets this, because one may be shared for a HEAD alone; nor does any other method. Every S3 response is `Cache-Control: private, no-store`, so the edge never caches one (`cf-cache-status: BYPASS`). The fix at the edge itself is a DNS-only `s3.openvibe.media`.
- **Never cached at the edge.** Every S3 answer is `Cache-Control: private, no-store`, with `CDN-Cache-Control` and `Cloudflare-CDN-Cache-Control: no-store`. This holds whatever Cache-Control an object was stored with (it is kept, not sent) and whatever a presigned URL's `response-cache-control` asks for. The S3 vhost enforces the same at nginx. On 2026-10-15, a probe showed Cloudflare serving a signed GET of an object stored with `public, max-age=…` to an unsigned request for the same URL. Only probe objects were exposed.
- **No browser execution.** Every response carries `Content-Security-Policy: sandbox; default-src 'none'` and `nosniff`, whatever type the object declares.
@@@ -85,7 +87,7 @@ names no zone of its key (`NoSuchBucket`, a bad signature) counts nowhere.
```sh
aws configure set aws_access_key_id ovk_… # issued on openvibe.zone
aws configure set aws_secret_access_key … # shown once
--aws --endpoint-url https://s3.openvibe.network s3 ls s3://assets/
--rclone config create oz s3 provider Other endpoint https://s3.openvibe.network access_key_id ovk_… secret_access_key … \
++aws --endpoint-url https://s3.openvibe.media s3 ls s3://assets/
++rclone config create oz s3 provider Other endpoint https://s3.openvibe.media access_key_id ovk_… secret_access_key … \
no_check_bucket true
```