feat: Presigned object URLs (s3 driver)

POST /buckets/:id/object-presigns mints SigV4 query-signed GET/PUT
URLs so clients move bytes directly against S3 — large transfers skip
the backend entirely.

- storage.Presigner optional interface; S3Store implements, local
  driver returns a clear error (no external surface to sign).
- TTL 60s–1h (default 15m); download requires read role + existing
  object row (no key probing); upload requires write role.
- S3_PUBLIC_ENDPOINT rewrites the client-facing host for internal
  S3_ENDPOINTs — signatures bind the host, URLs are valid only where
  minted.
- CLI: primora objects:presign <bucket> <key> [--upload] [--ttl].

Verified live against cloudserver: GET fetched the object with no
auth headers; PUT pushed 1 MB direct to S3; unsigned list → 403;
untracked-key download presign → 404.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 20:35:21 +02:00
1 parent a84049b8fc
commit fd36389328
22 files changed
+446 -7

No files matched your search

+61
View File
@@ -677,6 +677,37 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BucketObject"
/buckets/{bucketID}/object-presigns:
post:
tags: [Storage]
operationId: presignBucketObject
description: >-
Mint a short-lived URL the client calls against object storage
directly — keeps large transfers off the backend. Requires
BACKEND_STORAGE_DRIVER=s3; the URL's host comes from
S3_PUBLIC_ENDPOINT (or S3_ENDPOINT when unset). Signatures bind the
host — a presigned URL is valid only at the endpoint it was minted
for.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/BucketID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PresignObjectRequest"
responses:
"200":
description: Presigned URL
content:
application/json:
schema:
$ref: "#/components/schemas/PresignedURL"
"404":
description: Bucket or object not found (download)
/buckets/{bucketID}:
patch:
tags: [Storage]
@@ -3130,6 +3161,36 @@ components:
format: uuid
nullable: true
required: [objectKey, newObjectKey]
PresignObjectRequest:
type: object
properties:
key:
type: string
description: Object key inside the bucket.
op:
type: string
enum: [download, upload]
default: download
description: download → GET URL (object must exist); upload → PUT URL (creates/overwrites).
ttl_seconds:
type: integer
minimum: 60
maximum: 3600
default: 900
required: [key]
PresignedURL:
type: object
properties:
url:
type: string
description: Fully-signed URL — call it with the returned method, no auth header needed.
method:
type: string
enum: [GET, PUT]
expires_at:
type: string
format: date-time
required: [url, method, expires_at]
PaginationMetadata:
type: object
properties: