Distribution Servers API

Architecture

Distribution Servers split cleanly into a control plane (the ET Ducky cloud API) and a data plane (the hub agent's HTTPS listener on the LAN). The cloud evaluates access rules and mints short-lived ES256 (ECDSA P-256) JSON Web Tokens; the hub verifies them offline against the cloud's published public keys (JWKS) and serves bytes directly. The hub never holds a signing key, so a compromised hub cannot forge access for itself or any other hub.

Endpoints

EndpointPlanePurpose
POST /api/files/initiate-directCloudEvaluate ACLs and mint a transfer grant (read or write) for one path.
GET /blob/{blob-id}HubDownload a blob by id (read grant, Bearer).
PUT /blob/{blob-id}HubPublish a blob by id (write grant, Bearer).
DELETE /blob/{blob-id}HubRemove a blob (delete grant, Bearer).
/dav/{label}{path} (full WebDAV)HubRead-write WebDAV (mount key as Basic username:secret): PROPFIND/GET/HEAD plus PUT/DELETE/MKCOL/MOVE/COPY/LOCK/UNLOCK/PROPPATCH, each ACL-gated (write/delete). {label} is the connection's slug (drive name); the hub strips it.
POST /api/file-hub/mount-keys/validateCloudHub validates a presented mount key (cached) and gets back its per-path scope. Caller must be the bound hub.
GET /api/file-hub/jwks.jsonCloudPublic keys (JWKS) hubs use to verify grant tokens.
POST /api/file-hub/register ยท /heartbeatCloudHub registers its LAN endpoint + cert fingerprint and syncs config/manifest.

Minting a grant

POST /api/files/initiate-direct with a dashboard user token (Clerk Bearer). The body:

{
  "connectionId": "<connection-id>",
  "path": "/installers/agent-setup.exe",
  "action": "write",          // "read" | "write"
  "sizeBytes": 5242880          // required when action = "write"
}

The cloud resolves the connection, evaluates the caller's principals against the ACLs, and on success returns the grant:

{
  "hub": { "endpoint": "<hub-ip>:8443", "certFingerprint": "sha256:...", "mechanism": "jwtbearer" },
  "token": "<grant-jwt>",
  "expiresAt": "2026-06-02T21:05:00Z",
  "transferId": "..."
}

The caller then talks to https://{endpoint} directly, presenting the token as Authorization: Bearer. A FileHubTransfer audit row is recorded for every grant.

Token claims

Grant token (audience file-hub) — scopes one connection × path × action:

ClaimMeaning
issIssuer; must equal the hub's configured API base (https://etducky.com).
audfile-hub
conn_id / hub_idConnection id and the target hub agent id.
actionread, write, or delete.
pathNormalized relative path the grant covers.
blob_idOptional. When present, the URL blob id must match.
max_bytesOptional upload ceiling for writes.

(WebDAV mounts do not use a token. See mount keys below.)

Mount keys (WebDAV)

WebDAV mounts authenticate with an opaque mount key, not a JWT. A JWT is far longer than the ~127-character limit Windows imposes on stored credentials and expires within hours, so a mapped drive couldn't persist across a reboot. Instead, issuing a mount key returns a 32-hex key id (the Basic username) and a short random secret (the Basic password, shown once). The cloud stores only a salted hash of the secret plus the resolved per-path scope.

When a client connects, the hub calls POST /api/file-hub/mount-keys/validate with { keyId, secret }, authenticated as the hub agent. The cloud verifies the key belongs to that hub, isn't revoked or expired, and the secret matches, then returns { connId, hubId, paths: [{ path, permissions }], expiresAt }. The hub caches a positive result for ~60 seconds, so a burst of WebDAV requests costs at most one round-trip and revocation takes effect within about a minute. Keys are long-lived by default (org-configurable, e.g. 90 days) and revocable from the dashboard.

Publishing a blob (seeding)

Files can be published two ways: by writing to a mounted WebDAV drive (PUT), or programmatically (e.g. agent-to-agent) through the native /blob route with a write grant, shown here.

1. Get a write grant

curl -s -X POST "https://etducky.com/api/files/initiate-direct" \
  -H "Authorization: Bearer <CLERK_SESSION_JWT>" \
  -H "Content-Type: application/json" \
  -d '{"connectionId":"<conn>","path":"/installers/hello.txt","action":"write","sizeBytes":12}'

2. PUT the bytes to the hub

For a brand-new path the grant carries no blob_id, so the caller chooses the blob id (a fresh GUID). The grant goes in the Bearer header:

curl -k -X PUT "https://<hub-ip>:8443/blob/<fresh-guid>" \
  -H "Authorization: Bearer <GRANT_JWT>" \
  -H "Content-Type: text/plain" \
  --data-binary "@hello.txt"
# 201  { "blobId": "...", "sha256": "...", "sizeBytes": 12 }

The agent stores the blob in its cache keyed by that id with the grant's path as the relative path.

3. Read it back

Mint a read grant and GET https://<hub-ip>:8443/blob/{blobId}, or browse it over a mounted WebDAV drive — provided your ACLs grant read/list on that path.

Certificate model

Each hub serves with a self-signed TLS certificate generated on first run. The cloud records the certificate's fingerprint at registration and returns it in the grant (hub.certFingerprint) so callers can pin it. In manual testing, curl -k skips verification; production clients should pin the advertised fingerprint rather than disable verification.

ACL evaluation

Rules are allow grants keyed by principal and path prefix. For a given request the evaluator selects the rule(s) whose normalized path prefix is the longest match for the requested path, then unions the permissions granted to all of the caller's principals (their user id, their roles, and "everyone in org") at that depth. The request is allowed only if the required permission (list for PROPFIND, read for GET, write for PUT, delete for DELETE) is present. See Distribution Servers for the dashboard workflow.