Preview storage relay contract
Implementation reference for the managed original storage used by Visual Previews. Users and operators do not need this page to configure previews.
Relay v1 Client Contractā
The shared types and runtime parsers live in
packages/shared/src/previewStorage/v1.ts; the isolated transport lives in
packages/core/src/services/previewStorage/v1.ts. The relay implementation is
provided separately by integry/propr-routing and must implement this contract:
GET /v1/preview-storage/statusreturnsPreviewStorageStatusV1:version: 1,installationId,enabled,quotaBytes,usedBytes,reservedBytes,maxObjectBytes,retentionDays,allowedContentTypes, anddeleteSupported. Byte counts are nonnegative safe integers; object size and retention are positive.POST /v1/preview-artifacts/uploadsacceptsPreviewUploadRequestV1: version,taskId,repository(owner/repository full name), optional positive integerpullRequestNumber, sanitizeddisplayFilename, originalsizeBytes,contentType, and lowercase hexsha256. Task IDs are nonempty opaque strings of at most 256 characters, without control characters or surrounding whitespace. Display filenames are portable basenames of at most 128 characters, sanitized bysanitizePreviewDisplayFilename. Installation authority comes exclusively from the relay token; callers never supply an installation ID in an upload request. Connect must authorize the repository/task/PR association for that installation, persist the metadata, atomically reserve quota, and bind the grant to all those constraints. It returnsPreviewUploadV1with matching metadata and size/type/hash,artifactId,objectKey, andput: { url, headers, expiresAt }.- The client accepts a replayable staged
filePath, hashes it using streaming SHA-256, then sends it with a direct streaming HTTPSPUT. Keep the staged file unchanged and available until upload settles. Both passes use bounded buffers; neither requires a full-sizeUint8Array/Buffer. The PUT is re-hashed before finalization to detect changed input. Node fetch usesduplex: 'half'and the exact returned object-store headers. The signed header map must includeContent-Typeexactly matchingcontentType,Content-Lengthexactly matching the decimalsizeBytes, and the mandatory create-only conditionalIf-None-Match: *. The closed signed-header allowlist rejects other conditional headers and values. Redirects are rejected, and the relay bearer credential is never forwarded to the object store. POST /v1/preview-artifacts/:id/finalizeaccepts version, object key, size/type/hash. The relay verifies the stored object before returning aPreviewArtifactV1:version: 1,artifactId,state: 'ready', verified size/type/hash, all persisted task/repository/PR/filename metadata,viewerUrl, andretentionExpiresAt. The client checks these against the grant and request. Retention must be a parseable future timestamp. The artifact projection excludes object keys, upload URLs, and tokens.viewerUrlmust be a stable authenticated HTTPS link on the exact configured trusted Connect origin, with no credentials, query string, or fragment. Connect must require viewer authentication and authorize repository access on every request; the URL itself grants no access.DELETE /v1/preview-artifacts/:idis used only whendeleteSupportedis true. It returns a successful HTTP status. No automatic mutation retries are made; the relay must expire abandoned upload reservations.
Relay calls use PROPR_ROUTING_URL and the existing PROPR_GH_RELAY_TOKEN
bearer credential. Viewer trust is configured separately by PROPR_CONNECT_URL
(default https://connect.propr.dev), passed to the client as
trustedConnectOrigin; it must be an HTTPS origin, not a URL with a path.
The trusted origin is never taken from a remote upload/finalize response. Known error codes (quota_exceeded,
object_too_large, content_type_not_allowed, object_mismatch) are parsed from a bounded JSON
error body before HTTP-status fallback, so the two HTTP 413 cases remain distinct. Raw response
messages, oversized/unknown bodies, and transport errors are discarded. Unknown versions or
malformed statuses fail closed. Future v2 support can be added alongside v1.
Publication outcomesā
storeManagedVisualPreviewOriginals(evidence, { taskId, repository, pullRequestNumber? })
returns one versioned result per input asset, in input order. Each result includes
version: 1, assetIndex, and relativePath (the index disambiguates duplicate
paths), plus either { stored: true, artifact: PreviewArtifactV1 } or
{ stored: false, code }. Successful artifacts carry the trusted viewerUrl and
retention metadata, so the publisher can render links without uploading
again. Staged evidence retains its task ID for existing publication callers.
Failures are isolated per asset, including local file errors and unavailable or
disabled storage. Codes are a bounded union (PreviewStorageErrorCodeV1,
plus_required, or disabled); raw remote errors are discarded. A failed asset
does not discard successful results or stop later uploads or GitHub publication.
Only these codes may be used for fallback text. Never log or publish upload grants,
object keys, relay tokens, or raw remote response/error bodies.