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)
Changes
@@@ -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.@@@ -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 {@@@ -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.```shaws configure set aws_access_key_id ovk_… # issued on openvibe.zoneaws 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```
@@ -60,7 +60,7 @@ key ([Tenancy & auth](#tenancy--auth)).
@@ -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.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.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.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 |
| `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).
@@ -594,7 +594,7 @@ counts its viewer for the day, identifying no one (see Storage tiering).
## S3 surface and zones
## 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.
See [docs/s3.md](docs/s3.md) for the operations, the rules and the tests.
@@ -2,10 +2,10 @@
@@ -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
# 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
# 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).
# 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;
limit_req_zone $binary_remote_addr zone=media_s3:10m rate=50r/s;
server {
server {
@@ -1,8 +1,10 @@
@@ -1,8 +1,10 @@
# The S3 surface and zones
# 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
a named object zone of one project environment. Zones are made and deleted only through OpenVibe.Zone
(https://openvibe.zone).
(https://openvibe.zone).
@@ -44,7 +46,7 @@ a named object zone of one project environment. Zones are made and deleted only
@@ -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`).
- **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.
- **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.
- **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.
- **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.
- **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.
@@ -85,7 +87,7 @@ names no zone of its key (`NoSuchBucket`, a bad signature) counts nowhere.
```sh
```sh
aws configure set aws_access_key_id ovk_… # issued on openvibe.zone
aws configure set aws_access_key_id ovk_… # issued on openvibe.zone
aws configure set aws_secret_access_key … # shown once
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
no_check_bucket true
```
```