The ONE scheme whose bytes may leave the Durable Object for a public bucket.
Drupal's private:// is not a naming convention, it is an access-control boundary: those files
serve through /system/files/, a route that runs a per-user access check on every request
(CfwFileStreamWrapper::getExternalUrl() builds that path). An R2 object has no user, so
mirroring a private file publishes it permanently to anyone holding the URL -- an
authentication bypass, and a worse one than serving a stale personalised page, because it does
not expire and cannot be invalidated after the fact.
So this is the file-side of the same rule src/site.ts enforces for renders: anything whose
correctness depends on knowing who is asking must not be answered by a layer that does not
know. It is a list rather than a !== 'private' test -- a scheme added later
(temporary://, a contrib scheme) is refused until someone decides it is publishable, which is
the direction a mistake should fail in.
The ONE scheme whose bytes may leave the Durable Object for a public bucket.
Drupal's
private://is not a naming convention, it is an access-control boundary: those files serve through/system/files/, a route that runs a per-user access check on every request (CfwFileStreamWrapper::getExternalUrl()builds that path). An R2 object has no user, so mirroring a private file publishes it permanently to anyone holding the URL -- an authentication bypass, and a worse one than serving a stale personalised page, because it does not expire and cannot be invalidated after the fact.So this is the file-side of the same rule
src/site.tsenforces for renders: anything whose correctness depends on knowing who is asking must not be answered by a layer that does not know. It is a list rather than a!== 'private'test -- a scheme added later (temporary://, a contrib scheme) is refused until someone decides it is publishable, which is the direction a mistake should fail in.