This document defines the public signed-URL contract for truss.
It is the normative reference for:
- the public URL shape for
GET /images/by-pathandGET /images/by-url - canonicalization rules used by signature generation and verification
- compatibility expectations for SDK, CDN, and reverse-proxy integrations
For the field-level transform schema, see OpenAPI. For deployment guidance, see the API Reference.
This specification covers the public image endpoints authenticated by HMAC-signed query strings:
GET /images/by-pathGET /images/by-url
The primary external contract is GET. truss also accepts HEAD on these routes, using the same canonicalization rules with HEAD as the HTTP method, but truss sign generates GET URLs.
Private Bearer-token endpoints such as POST /images and POST /images:transform are out of scope for this document.
The following parts of the signed-URL format are part of the compatibility contract:
- Endpoint paths:
/images/by-pathand/images/by-url - Authentication parameters:
keyId,expires,signature - Source selector parameters:
/images/by-path:path, optionalversion/images/by-url:url, optionalversion
- Public transform parameters:
width,height,fit,position,format,qualityoptimize,targetQualitybackground,rotateautoOrient,stripMetadata,preserveExifcrop,blur,sharpenwatermarkUrl,watermarkPosition,watermarkOpacity,watermarkMarginpreset
- Signature algorithm: HMAC-SHA256 over the canonical UTF-8 request string
- Signature encoding: lowercase hexadecimal
The following rules are also part of the contract:
- Query parameter names are case-sensitive.
- Query parameters must not be repeated.
- Unsupported query parameters are rejected with
400 Bad Request. - Query parameter order on the wire is not significant. truss canonicalizes parameters before verification.
Deployment data is not part of the cross-version compatibility promise. That includes concrete keyId values, shared secrets, preset names, source paths, source URLs, and the meaning of an application-specific version token.
| Parameter | Required | Meaning |
|---|---|---|
keyId |
Yes | Selects the shared secret used for verification |
expires |
Yes | Expiration time as a Unix timestamp in seconds |
signature |
Yes | Lowercase hex-encoded HMAC-SHA256 signature |
Expiration is evaluated as expires < now. In other words, a request is still accepted during the exact second identified by expires, and is rejected once the current Unix time is greater than that value.
truss verifies the HMAC over this canonical form:
METHOD
AUTHORITY
REQUEST_PATH
CANONICAL_QUERY
Use the uppercase HTTP method. For the primary public contract this is GET.
Use the externally visible authority in host[:port] form:
- If the server is configured with
TRUSS_PUBLIC_BASE_URL, truss uses that URL's authority for verification. - Otherwise truss uses the incoming
Hostheader.
Do not include the scheme, path, query string, or fragment in the canonical authority.
Examples:
images.example.comimages.example.com:8443
Use the literal public endpoint path:
/images/by-path/images/by-url
Build the canonical query string as follows:
- Start from decoded query parameter names and values.
- Exclude
signature. - Reject duplicates. A parameter may appear at most once.
- Sort the remaining parameters lexicographically by parameter name.
- Serialize the sorted parameters using
application/x-www-form-urlencodedrules.
Important consequences:
- Sign the decoded value set, not the raw query substring from an incoming URL.
- Spaces are encoded as
+. - Reserved bytes are percent-encoded.
- Because parameters are sorted during canonicalization, callers do not need to preserve insertion order.
For this logical parameter set:
path=image.png
width=800
format=webp
keyId=public-demo
expires=1900000000
the canonical query becomes:
expires=1900000000&format=webp&keyId=public-demo&path=image.png&width=800
The canonical string is therefore:
GET
images.example.com
/images/by-path
expires=1900000000&format=webp&keyId=public-demo&path=image.png&width=800
The signature is the lowercase hex digest of:
HMAC-SHA256(secret, canonical_string_utf8_bytes)
Start the server:
TRUSS_SIGNING_KEYS='{"public-demo":"secret-value"}' \
TRUSS_PUBLIC_BASE_URL=https://images.example.com \
truss serve --storage-root ./imagesGenerate a signed URL:
truss sign \
--base-url https://images.example.com \
--path image.png \
--key-id public-demo \
--secret secret-value \
--expires 1900000000 \
--width 800 \
--format webpFetch it:
curl -o image.webp 'https://images.example.com/images/by-path?...&signature=...'For Node.js / TypeScript applications, you can use the official package:
npm install @nao1215/truss-url-signerSee packages/truss-url-signer for the package README and API reference.
The official signer validates the same request-invariant option matrix as the Rust server for public URL inputs such as fit, position, quality, targetQuality, watermark opacity, and crop syntax.
If you are implementing the signer yourself in another language, the equivalent flow in TypeScript is:
import { createHmac } from "node:crypto";
const params = new URLSearchParams([
["path", "image.png"],
["width", "800"],
["format", "webp"],
["keyId", "public-demo"],
["expires", "1900000000"],
]);
const canonicalParams = new URLSearchParams(
[...params.entries()]
.filter(([name]) => name !== "signature")
.sort(([a], [b]) => a.localeCompare(b)),
);
const canonical = [
"GET",
"images.example.com",
"/images/by-path",
canonicalParams.toString(),
].join("\n");
const signature = createHmac("sha256", "secret-value")
.update(canonical, "utf8")
.digest("hex");
canonicalParams.set("signature", signature);
const signedUrl =
`https://images.example.com/images/by-path?${canonicalParams.toString()}`;This example intentionally signs the decoded parameter values and lets URLSearchParams produce the canonical wire encoding.
For signed public traffic behind CloudFront, nginx, Envoy, or another proxy:
- Set
TRUSS_PUBLIC_BASE_URLto the public origin such ashttps://images.example.com. - Forward the full query string unchanged.
- Include all signed-URL query parameters in the cache key, or forward all query strings.
- If you rely on
Acceptnegotiation by omittingformat, also forwardAcceptand include it in the cache key because responses may vary on that header. - If your CDN does not vary on
Accept, prefer settingformatexplicitly or enableTRUSS_DISABLE_ACCEPT_NEGOTIATION=true.
truss treats the signed public URL format as a stable external contract even while the project is pre-1.0.
The following compatibility rules apply:
- Existing endpoint paths, parameter names, canonicalization rules, and HMAC algorithm will not change silently in patch or minor releases.
- Existing parameter meanings will not be repurposed under the same endpoint path.
- New optional query parameters may be added in future releases. Existing signed URLs that do not use them remain valid.
- If a breaking change is ever required, truss will introduce it through a documented migration path, such as a parallel endpoint, dual-format support during a transition window, or a clearly announced release-note break.
- Deprecations will be documented before removal. When practical, truss will accept both old and new forms during the deprecation window rather than invalidating existing signed URLs immediately.
For callers that need the most stable behavior across deployments, prefer:
- explicit
formatinstead ofAcceptnegotiation - explicit transform parameters instead of deployment-defined
presetnames - a configured
TRUSS_PUBLIC_BASE_URLinstead of relying on inbound proxyHostbehavior