# Object storage, provider adapters, and signed URLs

Canonical proposed publication URL: `https://3sln.me/artefacts/contracts/object-storage/v1`. It is a public versioned artefact, distinct from private account resources. The version is immutable after formal release; a gated public deployment exposes this explicitly labeled draft for review without declaring stable conformance. The draft JSON Schema 2020-12 in `docs/artefacts/object-storage-v1.json` defines method request variants, argument bounds, metadata and result schemas. Runtime checks additionally enforce byte limits, UTF-8 key lengths, range ordering and authority. Publication and independent interoperability review remain pending. Reviewer: ___ Date: ___ Outcome: ___.

Unlike the first draft's mandatory write contract, this contract supports capability negotiation. Required `capabilities` truthfully reports read, write, byte range, read/write presigning, atomic conditional writes, byte limits, and operational limits. Apps request method scopes separately. A provider may expose reads without writes, and the UI must say so. Changing a capability never expands an existing method grant.

Objects use stable logical keys, byte payloads, opaque versions, content type, size and modification metadata. Reads support inclusive `{start,end}` ranges only when advertised. Writes require `{absent:true}` or exact version conditions. Deletes require exact versions. No unconditional overwrite is implied. Driver errors map to safe protocol codes; upstream tokens and errors never reach clients.

`presign` requests a read or write bearer URL, HTTP method and required headers, with lifetime at most five minutes. The corresponding explicit presign grant is mandatory. URLs may expose data until expiry even after app/token revocation; approval must disclose this and offer proxy-only access instead. Upload signing needs content length/type bounds, namespace scope and required conditional semantics. Until these constraints are enforced, the implementation rejects presigned writes and advertises them unavailable.

## Provider boundaries

- Hosted R2: implemented driver prefixes every key with the owner/resource namespace and uses native R2 `onlyIf` for atomic create/update. Range reads are implemented. Conditional deletion is explicitly unavailable because the binding does not supply atomic If-Match delete; head-then-delete would race. Browser users never receive R2 binding credentials. [Workers binding API](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/).
- S3-compatible: SigV4 signing via pinned aws4fetch; region, bucket and optional prefix; conditional write conformance is provider-specific. User configuration cannot assert it is verified. Arbitrary endpoints require an operator hostname allowlist; no user-defined private/IP hosts, ports or redirects.
- WebDAV: HTTPS, restricted root and endpoint allowlist; credentials encrypted in server storage; reads first. Weak or missing ETags and unreliable conditional behavior prevent write claims. XML must be parsed with bounded non-DTD parsing; external entity expansion is forbidden.
- Google Drive: the portal now uses a managed writable adapter with encrypted OAuth/PKCE, `drive.file` scope, dedicated app-created folders, refresh, disconnect and same-account reconnect. Stable logical keys map to immutable Drive objects through the Bindle host's durable metadata. Conditional publication is enforced by this host's atomic mapping update, not an undocumented upstream Drive version condition. Whole-object digest and parent/version checks protect reads; byte ranges currently fetch/verify the bounded complete object before slicing. No native documents, arbitrary folder import, listing or presigning are exposed. Old/deleted physical objects remain in Drive; see [managed Drive design](../architecture/google-drive-integration.md). Credentials and real-account conformance remain deployment gates. The older direct-ID read adapter remains an internal utility.
- Dropbox: implemented read driver uses explicit folder paths, normalizes lowercase path keys and verifies returned path/revision. It uses the [files API](https://www.dropbox.com/developers/documentation/http/documentation) and requires files.metadata.read plus files.content.read scopes; use an app-folder application where feasible. OAuth setup and refresh remain deployment integration gates.
- OneDrive: implemented Graph read driver anchors requests to a selected drive/folder. Listings reject remote items and parent mismatches; continuation links must retain the exact Graph origin/path. [Graph downloads](https://learn.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0) expose preauthenticated download URLs: the adapter sends no OAuth header to that URL, follows no redirects, and requires its Microsoft download host to be explicitly operator-approved. Files.Read is the intended delegated scope; broader permissions require independent justification. OAuth application setup, folder-move race review, approved download hosts, and live verification remain gates.

Gateway network security does not rely on string URL validation alone. Operator allowlists must include only vetted public services; custom-host approval requires DNS/private-network checks and a safe egress policy. Credentials are sent only after endpoint validation, with redirect following disabled. Disabled/unconfigured provider cards must explain the required credential or operator setup.

## Release gates

Machine schemas, normalized object metadata and mocked provider conformance tests are present. Provider-native listing helpers are retained only as internal utilities and are not part of the object-storage contract or public RPC gateway. Listing requests fail before storage or provider requests; old capability records are projected without their legacy `list` field.

Provider response errors map to safe codes; token-bearing response bodies are not exposed. Range reads require an actual bounded partial response. S3 SigV4 GET presigning is restricted to the namespace and five minutes; uploads remain unavailable. R2 supports native atomic conditional PUT only; S3 writes require operator-reviewed conditional capability, and must never be enabled directly from an untrusted configuration flag.

Remaining gates include live credentials and provider conformance, OAuth refresh/revocation, DNS and connection-time egress enforcement, cross-owner gateway authorization, quota/rate enforcement, audited credential encryption, Unicode/name compatibility, and durable integration tests. Mocked adapters demonstrate request construction and defensive handling, not working production accounts. Connector verification reads an explicitly supplied known object key; it never falls back to enumeration or treats a missing test object as proof of read access. Empty connectors remain unverified until a readable bootstrap/probe object is supplied. The Worker currently enables S3/WebDAV configuration only; Google setup is enabled only when its client credentials and encryption key are configured; configured status does not establish live conformance. Dropbox/OneDrive OAuth cards remain unavailable. No remote provider account was connected or altered during implementation.


## Application-managed directories

The contract deliberately has no `list` method or listing capability. Apps maintain directory/index objects containing the exact keys, versions and metadata of their own objects. This keeps enumeration cost and indexing policy with the application, across providers that differ in native listing support and cost. It does not guarantee zero-cost operations: index reads/writes and provider-side key resolution can still incur requests. Inbox/message collection enumeration belongs to its separate collection contract and is unaffected.

An app chooses a collision-resistant namespace and a deterministic root index key, for example `apps/<app-id>/directory.json`, and records that root pointer in its existing granted preferences/resource state so another device can find it without enumeration. Applications must never overwrite another app's root. Resource IDs and root keys must be persisted/exported together for recovery. A provider with opaque IDs needs an explicitly provisioned root ID or a provider-owned logical-key mapping; names are not unique identifiers.

For writable synchronization, require reliable atomic conditional writes. Create objects with `absent:true`, update a directory against its exact previous version, and retry conflicts by rereading/merging rather than overwriting. Store new content first, then atomically update the directory pointer; there is no multi-object transaction. Use immutable content keys and a durable app-owned pending-operation manifest so interrupted writes can be reconciled. A read-only provider can host an externally supplied index but cannot offer writable app sync; a provider without reliable conditional writes must not claim safe concurrent writable sync.

Removing an entry and deleting its object are distinct effects with separate delete permission. Use a tombstone/pending-deletion ledger and retention window; failed index updates can leave orphans, and no listing fallback exists to rediscover them. Apps must retain known created keys for reconciliation and disclose these limits. Directory references do not grant access: every referenced object still requires current resource, namespace and method authorization. A lost sole root/index cannot be reconstructed by this contract; backup/export the directory state. Large directories should use bounded sharded index objects and a small CAS-updated root rather than one unbounded file.

The managed Google Drive adapter supplies persistent logical-key mappings, so apps can use deterministic directory keys without Drive listing. This map is authoritative host state and must accompany account/provider backups; another independent host cannot reconstruct it from Drive filenames. Same-Google-account reconnect preserves resource ID, root folder and mappings. External edits, moves, missing files or changed folder ownership fail safely. The independent legacy read adapter still accepts raw file IDs and is not used for new portal connections.
