Internal Storage Protocol (Follower)
These endpoints are the internal object storage protocol between the primary node and follower nodes. They are not public browser or third-party client APIs.
This page describes the follower-side /api/v1/internal/storage/* endpoints that actually perform object reads and writes. The primary separately exposes /api/v1/internal/remote-node-control/* as the binding control plane and /api/v1/internal/remote-tunnel/* as the transport used by followers that cannot be reached directly.
All paths below are relative to:
/api/v1/internal/storageand are registered only on follower nodes.
Direct vs. Reverse Tunnel
Section titled “Direct vs. Reverse Tunnel”The remote-node protocol has three layers:
/api/v1/internal/storage/*exists only on the follower and performs object access, binding sync, and remote storage target management/api/v1/internal/remote-node-control/*exists only on the primary and is the independent control plane from which followers pull binding desired state/api/v1/internal/remote-tunnel/*exists only on the primary and transports object requests over the reverse tunnel; it does not reconcile binding state
In direct mode, the primary directly calls the follower’s /api/v1/internal/storage/*. In reverse_tunnel mode, the primary registers the same internal storage request in the tunnel registry; the follower polls or opens a WebSocket to the primary, runs the internal storage logic locally, and returns the response.
For auto, the primary resolves the current base_url into an explicit resolved_transport: a non-empty base_url selects direct, while an empty base_url selects reverse_tunnel. The follower periodically polls the independent control plane for every binding, including disabled and effectively direct bindings, so transport transitions do not depend on either the old or the new object-data path.
Binding state converges by revision:
- The primary owns
name,is_enabled,resolved_transport, and a monotonically increasingdesired_revision. The revision changes only when one of those follower-visible values changes. - The follower calls
GET /api/v1/internal/remote-node-control/binding-state?applied_revision=N, treats the primary response as authoritative, and persists that desired state locally. - The follower refreshes its runtime binding registry and then starts, stops, or replaces reverse-tunnel workers. It marks the local
applied_revisiononly after both the registry and worker topology have been reconciled. - The next pull sends the new
applied_revisionas an implicit ACK. The primary accepts ACKs only up to its current desired revision. A higher stale follower revision does not override primary state; the follower is brought back to the primary’s current revision.
Primary-side binding control entry:
| Method | Path | Description |
|---|---|---|
GET | /api/v1/internal/remote-node-control/binding-state | Pull desired state and report the applied revision through the applied_revision query |
Primary-side reverse-tunnel entries:
| Method | Path | Description |
|---|---|---|
POST | /api/v1/internal/remote-tunnel/poll | Follower long-polls pending requests |
POST | /api/v1/internal/remote-tunnel/complete | Follower returns a polled request result |
GET | /api/v1/internal/remote-tunnel/connect | Follower opens a streaming WebSocket tunnel |
Both the binding control plane and reverse-tunnel endpoints use remote-node signature auth. Direct, reverse-tunnel, and disabled nodes may access the binding control plane. The reverse-tunnel data plane additionally requires the node to be enabled and currently resolved to reverse_tunnel, so an effectively direct node cannot use poll, complete, or connect even with a valid signature.
This binding control extension does not change the tunnel frame wire format: the WebSocket frame version remains 1. It also does not raise the internal-storage protocol version, which remains v5 with a v4 compatibility floor; rolling upgrades are handled through an optional capability and JSON defaults.
Authentication
Section titled “Authentication”Two access forms are supported:
- primary-signed request headers:
x-aster-access-keyx-aster-timestampx-aster-noncex-aster-signature
- presigned query:
aster_access_keyaster_expiresaster_signature
Control-plane endpoints require signed headers. Object GET / PUT can support presigned URLs depending on the scenario.
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
GET | /capabilities | Read follower protocol capabilities |
GET | /capacity | Read capacity status for the current follower receiving target |
PUT | /binding | Push binding state to legacy followers without the binding control pull capability |
GET | /targets | List remote storage targets available to the current binding |
POST | /targets | Create a remote storage target |
PATCH | /targets/{target_key} | Update a remote storage target |
DELETE | /targets/{target_key} | Delete a remote storage target |
POST | /compose | Compose part objects into a target object |
GET | /objects | List object keys by prefix |
GET | /objects/{tail}/metadata | Read object metadata |
PUT | /objects/{tail} | Upload object content |
GET | /objects/{tail} | Read object content |
HEAD | /objects/{tail} | Probe object existence and headers |
DELETE | /objects/{tail} | Delete object |
Version 0.4.0 removed the legacy /ingress-profiles and /ingress-profiles/{target_key} compatibility paths. Primaries and followers must use /targets.
GET /capabilities
Section titled “GET /capabilities”Typical fields include:
protocol_versionmin_supported_protocol_versionserver_versionfeaturesbrowser_corslimitssupports_listsupports_range_readsupports_stream_uploadsupports_capacity
The current protocol version is v5, with v4 as the compatibility floor, so the current local supported range is v4-v5. v4 / v5 are not wire-compatible with v2 or v3: internal storage JSON envelopes now use the stable string ApiErrorCode in the top-level code field instead of the old numeric code. Upgrade both primary and follower before binding remote policies across this boundary.
Current followers explicitly advertise features.binding_state_pull. The capability defaults to false, which defines the rolling-upgrade boundary:
- With
binding_state_pull = true, the primary stops pushing binding state and relies entirely on follower pull reconciliation. - When the capability is absent or
false, the primary retains the legacyPUT /bindingpush. - When a new follower calls an old primary and receives
404for binding-state, it keeps its locally persisted legacy push state.
Protocol v5 adds remote-storage-target driver capabilities. The Rust model field is named remote_storage_target, but the v4 / v5 wire JSON still serializes it as managed_ingress and also accepts remote_storage_target as a deserialization alias. The legacy wire key is a compatibility detail, not a separate product model.
The compatibility boundary is explicit:
- When a
v4follower omits this capability, the primary applies the legacy assumption that Local and S3 drivers are available. - A
v5follower must declare remote-storage-target capability explicitly. Missing or disabled capability does not receive the implicitv4Local / S3 fallback. - The primary exposes only drivers both declared by the follower and backed by a descriptor registered in the current build. Unknown future driver IDs remain protocol data but do not automatically become configurable choices.
During policy loading and binding refresh, the primary validates:
protocol_version/min_supported_protocol_versionmust overlap with the local supported range, currentlyv4-v5- Base remote policies require
object_get,object_head,object_put,object_delete,metadata,range_get,accept_ranges_header,list, andcompose - Browser presigned download requires
browser_corsto allow therangerequest header and exposeAccept-Ranges,Content-Range, andContent-Length - Browser presigned upload requires
browser_corsto allow thecontent-typerequest header and exposeETag
GET /capacity
Section titled “GET /capacity”Returns StorageCapacityInfo for the follower’s current remote storage target driver:
{ "code": "success", "msg": "", "data": { "capacity": { "status": "supported", "total_bytes": 1099511627776, "available_bytes": 549755813888, "used_bytes": 549755813888, "source": "local_filesystem", "observed_at": "2026-05-28T12:00:00Z" } }}Local targets usually return real filesystem capacity. S3 targets explicitly return unsupported, which the primary converts into a user-visible unsupported capacity state. This endpoint is used for admin capacity observation and migration preflight, not the hot upload / download path.
PUT /binding
Section titled “PUT /binding”This endpoint is retained only for legacy followers that do not advertise features.binding_state_pull. A new primary pushes these desired-state fields to such followers:
nameis_enabledresolved_transport:directorreverse_tunnel; omission defaults toreverse_tunneldesired_revision: the primary desired-state revision; omission defaults to1
This updates binding metadata only; it does not move object data. The primary attempts a legacy push only when the current or pre-transition data path is actually usable. When no such path exists, it skips the push; followers that support the new control plane still converge through pull. The compatibility push can be removed once the minimum supported follower version explicitly advertises binding_state_pull.
Remote storage target management
Section titled “Remote storage target management”These endpoints let the primary manage follower-side remote storage targets, deciding whether future object writes land in follower-local storage or follower-managed S3.
Local target request:
{ "driver_type": "local", "name": "local-default", "base_path": "data/storage", "is_default": true}S3 target request:
{ "driver_type": "s3", "name": "edge-s3", "endpoint": "https://s3.example.com", "bucket": "aster-edge", "access_key": "AKIA...", "secret_key": "...", "base_path": "objects/", "is_default": false}Updates use a flat request shape and can change name, driver_type, connection fields, base_path, and is_default. The current target drivers are local and s3; the actual choices are also constrained by the follower’s remote_storage_target capability declaration. These control-plane endpoints accept only signed primary headers, not presigned query access.
Object operations
Section titled “Object operations”POST /compose
Section titled “POST /compose”Composes uploaded parts into a final object. Request fields include:
target_keypart_keysexpected_size
Successful composition returns bytes_written and cleans consumed part objects.
PUT /objects/{tail}
Section titled “PUT /objects/{tail}”Writes one object. The request must include Content-Length, and the follower checks size limits according to the ingress policy.
GET /objects/{tail}
Section titled “GET /objects/{tail}”Returns raw object bytes, not JSON.
Optional query parameters:
offsetlengthresponse-cache-controlresponse-content-dispositionresponse-content-type
Standard Range: bytes=... also works; partial responses use 206 Partial Content.
HEAD /objects/{tail}
Section titled “HEAD /objects/{tail}”Returns object existence and basic response headers.
GET /objects/{tail}/metadata
Section titled “GET /objects/{tail}/metadata”Returns wrapped JSON with fields such as:
sizecontent_type
DELETE /objects/{tail}
Section titled “DELETE /objects/{tail}”Deletes the object and returns an empty success response.
Listing
Section titled “Listing”GET /objects supports:
prefix: return only object keys with the requested prefixcursor: continue from a relative position, normally using the previous page’snext_cursorlimit: requested page size; it must be greater than0and is clamped to the server’s internal page-size ceiling
New clients should always send limit. A paged response looks like:
{ "code": "success", "msg": "", "data": { "items": ["files/part-001", "files/part-002"], "next_cursor": 2 }}next_cursor is present only when another page exists. Omitting limit preserves the legacy unpaged response used by older clients and returns every matching item at once. Returned items are relative keys inside the follower binding namespace, not raw provider prefixes.
When to read this page
Section titled “When to read this page”Look here instead of the ordinary files / upload / shares routes when:
- primary-to-remote-node writes fail
- managed follower part composition fails
- remote-node health is green but object listing / reading / deletion is wrong
- enrollment succeeded but later object synchronization behaves incorrectly