openapi: 3.0.3 info: title: Insforge Storage API version: 2.0.0 description: Bucket-based storage system similar to S3 paths: /api/storage/buckets: get: summary: List All Buckets tags: - Admin security: - apiKey: [] responses: '200': description: List of bucket names content: application/json: schema: type: object properties: buckets: type: array items: type: string example: ["avatars", "documents", "uploads"] example: buckets: ["avatars", "documents", "uploads", "public", "private"] post: summary: Create New Bucket tags: - Admin security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object required: - bucketName properties: bucketName: type: string pattern: '^[a-zA-Z0-9_-]+$' description: Bucket name (alphanumeric, underscore, and hyphen only) example: avatars isPublic: type: boolean description: Whether the bucket is publicly accessible default: true example: true responses: '201': description: Bucket created successfully content: application/json: schema: type: object properties: message: type: string example: "Bucket created successfully" bucketName: type: string example: avatars example: message: "Bucket created successfully" bucket: "avatars" '400': description: Invalid bucket name content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "INVALID_BUCKET_NAME" message: "Bucket name must contain only alphanumeric characters, underscores, and hyphens" statusCode: 400 nextActions: "Use a valid bucket name format" '409': description: Bucket already exists content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "BUCKET_EXISTS" message: "Bucket 'avatars' already exists" statusCode: 409 nextActions: "Choose a different bucket name" /api/storage/buckets/{bucketName}: delete: summary: Delete Bucket description: Delete an entire bucket and all its objects tags: - Admin security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars responses: '200': description: Bucket deleted successfully content: application/json: schema: type: object properties: message: type: string nextActions: type: string example: message: "Bucket deleted successfully" nextActions: "You can use POST /api/storage/buckets to create a new bucket, and GET /api/storage/buckets/:bucketName/objects to list the objects in the bucket." '404': description: Bucket not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "BUCKET_NOT_FOUND" message: "Bucket 'nonexistent' does not exist" statusCode: 404 nextActions: "Check bucket name and try again" patch: summary: Update Bucket tags: - Admin security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars requestBody: required: true content: application/json: schema: type: object required: - is_public properties: isPublic: type: boolean description: Whether the bucket should be publicly accessible example: true responses: '200': description: Bucket visibility updated successfully content: application/json: schema: type: object properties: message: type: string example: "Bucket visibility updated" bucket: type: string example: avatars isPublic: type: boolean example: true example: message: "Bucket visibility updated" bucket: "avatars" isPublic: true '404': description: Bucket not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "BUCKET_NOT_FOUND" message: "Bucket 'nonexistent' does not exist" statusCode: 404 nextActions: "Check bucket name and try again" /api/storage/buckets/{bucketName}/objects: get: summary: List Objects in Bucket tags: - Admin security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: prefix in: query required: false schema: type: string description: Filter objects by key prefix example: users/ - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 1000 default: 100 - name: offset in: query required: false schema: type: integer minimum: 0 default: 0 - name: search in: query required: false schema: type: string description: Search objects by key (partial match) example: profile responses: '200': description: List of objects in bucket content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/StoredFile' pagination: type: object properties: offset: type: integer example: 0 limit: type: integer example: 100 total: type: integer example: 2 nextActions: type: string example: "You can use PUT /api/storage/buckets/:bucketName/objects/:objectKey to upload with a specific key, or POST /api/storage/buckets/:bucketName/objects to upload with auto-generated key, and GET /api/storage/buckets/:bucketName/objects/:objectKey to download an object." example: data: - bucket: "avatars" key: "users/user123.jpg" size: 102400 mimeType: "image/jpeg" uploadedAt: "2024-01-15T10:30:00Z" url: "/api/storage/buckets/avatars/objects/users/user123.jpg" - bucket: "avatars" key: "users/user456.png" size: 204800 mimeType: "image/png" uploadedAt: "2024-01-16T11:00:00Z" url: "/api/storage/buckets/avatars/objects/users/user456.png" pagination: offset: 0 limit: 100 total: 2 nextActions: "You can use PUT /api/storage/buckets/:bucketName/objects/:objectKey to upload with a specific key, or POST /api/storage/buckets/:bucketName/objects to upload with auto-generated key, and GET /api/storage/buckets/:bucketName/objects/:objectKey to download an object." post: summary: Upload Object with Auto-Generated Key tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: File to upload required: - file responses: '201': description: Object uploaded successfully with auto-generated key content: application/json: schema: $ref: '#/components/schemas/StoredFile' example: bucket: "avatars" key: "image-1737546841234-a3f2b1.jpg" size: 102400 mimeType: "image/jpeg" uploadedAt: "2024-01-21T10:30:00Z" url: "/api/storage/buckets/avatars/objects/image-1737546841234-a3f2b1.jpg" '400': description: Invalid bucket name or file content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "INVALID_FILE" message: "No file provided in the request" statusCode: 400 nextActions: "Include a file in the multipart form data" '404': description: Bucket not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "BUCKET_NOT_FOUND" message: "Bucket 'nonexistent' does not exist" statusCode: 404 nextActions: "Create the bucket first" delete: summary: Delete Multiple Objects description: Deletes up to 1000 objects from a bucket for the authenticated caller. Object row deletion is authorized through storage RLS, and the response reports per-key outcomes for rows deleted from the database, keys not visible or missing, and provider-level failures. tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars requestBody: required: true content: application/json: schema: type: object required: - keys properties: keys: type: array minItems: 1 maxItems: 1000 items: type: string minLength: 1 example: keys: - users/user123.jpg - users/user456.png responses: '200': description: Batch delete result content: application/json: schema: type: object required: - results properties: results: type: array items: type: object required: - key - status properties: key: type: string status: type: string enum: - deleted - notFound - failed message: type: string description: Present when status is failed. example: results: - key: users/user123.jpg status: deleted - key: users/missing.png status: notFound - key: users/locked.png status: failed message: Failed to delete object '400': description: Invalid bucket name or object keys content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Permission denied content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/buckets/{bucketName}/upload-strategy: post: summary: Get Upload Strategy (Direct or Presigned URL) description: Returns upload strategy based on storage backend (S3 returns presigned URLs, local returns direct upload endpoints) tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars requestBody: required: true content: application/json: schema: type: object required: - filename properties: filename: type: string description: Original filename for generating unique key example: profile-photo.jpg contentType: type: string description: MIME type of the file example: image/jpeg size: type: integer description: File size in bytes example: 102400 responses: '200': description: Upload strategy details content: application/json: schema: $ref: '#/components/schemas/UploadStrategy' examples: s3: summary: S3 Backend Response value: method: presigned uploadUrl: https://s3-bucket.amazonaws.com/ fields: bucket: my-s3-bucket key: app-key/avatars/profile-photo-1234567890-abc123.jpg X-Amz-Algorithm: AWS4-HMAC-SHA256 X-Amz-Credential: AKIA.../20250905/us-east-2/s3/aws4_request X-Amz-Date: 20250905T000000Z Policy: eyJ... X-Amz-Signature: abc123... key: profile-photo-1234567890-abc123.jpg confirmRequired: true confirmUrl: /api/storage/buckets/avatars/objects/profile-photo-1234567890-abc123.jpg/confirm-upload expiresAt: "2025-09-05T01:00:00Z" local: summary: Local Storage Response value: method: direct uploadUrl: /api/storage/buckets/avatars/objects/profile-photo-1234567890-abc123.jpg key: profile-photo-1234567890-abc123.jpg confirmRequired: false '404': description: Bucket not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/buckets/{bucketName}/objects/{objectKey}: put: summary: Upload Object tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: objectKey in: path required: true schema: type: string example: user123.jpg description: Object key (can include forward slashes for pseudo-folders) requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: File to upload required: - file responses: '201': description: Object uploaded successfully content: application/json: schema: $ref: '#/components/schemas/StoredFile' example: bucket: "avatars" key: "user123.jpg" size: 102400 mimeType: "image/jpeg" uploadedAt: "2024-01-21T10:30:00Z" url: "/api/storage/buckets/avatars/objects/user123.jpg" '400': description: Invalid bucket name, key, or file content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "INVALID_FILE" message: "No file provided in the request" statusCode: 400 nextActions: "Include a file in the multipart form data" '404': description: Bucket not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "BUCKET_NOT_FOUND" message: "Bucket 'nonexistent' does not exist" statusCode: 404 nextActions: "Create the bucket first" get: summary: Download Object tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: key in: path required: true schema: type: string example: user123.jpg responses: '200': description: File content content: '*/*': schema: type: string format: binary headers: Content-Type: schema: type: string description: MIME type of the file Content-Length: schema: type: integer description: Size of the file in bytes '404': description: Object not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "OBJECT_NOT_FOUND" message: "Object 'user123.jpg' not found in bucket 'avatars'" statusCode: 404 nextActions: "Check the bucket and key combination" delete: summary: Delete Object tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: key in: path required: true schema: type: string example: user123.jpg responses: '200': description: Object deleted successfully content: application/json: schema: type: object properties: message: type: string example: message: "Object deleted successfully" '404': description: Object not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "OBJECT_NOT_FOUND" message: "Object 'user123.jpg' not found in bucket 'avatars'" statusCode: 404 nextActions: "Check the bucket and key combination" /api/storage/buckets/{bucketName}/objects/{objectKey}/confirm-upload: post: summary: Confirm Presigned Upload description: Confirms that a file was successfully uploaded to S3 using presigned URL tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: objectKey in: path required: true schema: type: string example: profile-photo-1234567890-abc123.jpg requestBody: required: true content: application/json: schema: type: object required: - size properties: size: type: integer description: File size in bytes example: 102400 contentType: type: string description: MIME type of the file example: image/jpeg etag: type: string description: S3 ETag of the uploaded object (optional) example: "9bb58f26192e4ba00f01e2e7b136bbd8" responses: '201': description: Upload confirmed successfully content: application/json: schema: $ref: '#/components/schemas/StoredFile' '404': description: Upload not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "UPLOAD_NOT_FOUND" message: "Upload not found for key 'profile-photo.jpg' in bucket 'avatars'" statusCode: 404 '409': description: Already confirmed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: "ALREADY_CONFIRMED" message: "File 'profile-photo.jpg' already confirmed in bucket 'avatars'" statusCode: 409 /api/storage/buckets/{bucketName}/download-strategy/objects/{objectKey}: get: summary: Get Download Strategy (Direct or Presigned URL) description: | Returns download strategy based on storage backend and bucket visibility. - S3 with public bucket: Direct URLs (no presigning, better performance) - S3 with private bucket: Presigned URLs with expiration - Local storage: Always direct endpoints Expiry (when presigned) is auto-calculated server-side from bucket visibility; no request body is accepted. Object keys may contain `/` (pseudo-folders); the path is matched with a wildcard, so callers should NOT percent-encode `/` inside `objectKey`. tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: objectKey in: path required: true schema: type: string example: profile-photo.jpg responses: '200': description: Download strategy details content: application/json: schema: $ref: '#/components/schemas/DownloadStrategy' examples: s3-public: summary: S3 Public Bucket Response value: method: direct url: https://s3-bucket.s3.us-east-2.amazonaws.com/app-key/public-assets/logo.png s3-private: summary: S3 Private Bucket Response value: method: presigned url: https://s3-bucket.s3.us-east-2.amazonaws.com/app-key/avatars/profile.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=... expiresAt: "2025-09-05T01:00:00Z" local: summary: Local Storage Response value: method: direct url: /api/storage/buckets/avatars/objects/profile-photo.jpg '404': description: Object not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/buckets/{bucketName}/objects/{objectKey}/download-strategy: post: deprecated: true summary: Get Download Strategy (Deprecated — use GET on the canonical path) description: | Deprecated alias of `GET /api/storage/buckets/{bucketName}/download-strategy/objects/{objectKey}`. Retained at the original path/method for backward compatibility with SDK releases that already shipped against this POST endpoint. Any request body is ignored. tags: - Client security: - apiKey: [] parameters: - name: bucketName in: path required: true schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: avatars - name: objectKey in: path required: true schema: type: string example: profile-photo.jpg responses: '200': description: Download strategy details content: application/json: schema: $ref: '#/components/schemas/DownloadStrategy' '404': description: Object not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/s3/config: get: summary: Get S3 Gateway Config description: | Return the externally-reachable endpoint URL and the SigV4 signing region for the S3 protocol gateway. The endpoint is assembled from the server's `VITE_API_BASE_URL` env var plus `/storage/v1/s3`, and the region is `AWS_REGION` (default `us-east-2`). Intended for Dashboard display so the UI doesn't make any backend-topology assumptions client-side. tags: - S3 Access Keys security: - apiKey: [] responses: '200': description: Gateway config content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/S3GatewayConfig' example: data: endpoint: https://proj.insforge.app/storage/v1/s3 region: us-east-2 '401': description: Missing or invalid API key / admin token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/s3/access-keys: get: summary: List S3 Access Keys description: | Return every S3 access key configured for this project. Plaintext secrets are **never** returned here — the secret is only shown once in the response of `POST /api/storage/s3/access-keys`. tags: - S3 Access Keys security: - apiKey: [] responses: '200': description: List of access keys (without secrets) content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/S3AccessKey' '401': description: Missing or invalid API key / admin token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded (20 requests / 15 min / IP) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: summary: Create S3 Access Key description: | Mint a new S3 credential pair usable against the `/storage/v1/s3` protocol gateway. The plaintext `secretAccessKey` in the response is returned **exactly once** — it is encrypted at rest and can never be retrieved again. If you lose it, revoke and re-create. Limits: - 50 keys per project (hard cap, enforced transactionally). - Rate-limited to 20 management requests per 15 min per IP. tags: - S3 Access Keys security: - apiKey: [] requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/CreateS3AccessKeyRequest' responses: '201': description: Access key created content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/S3AccessKeyWithSecret' example: data: id: 11111111-1111-1111-1111-111111111111 accessKeyId: INSFABC123DEF456GH78 secretAccessKey: x7K2-a_pL9qRs4N8vYzWcE1fH5gJ3mUtBoD6ViXk description: backup-script createdAt: "2026-04-22T00:00:00Z" lastUsedAt: null '400': description: Validation error or 50-key cap reached content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid API key / admin token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded (20 requests / 15 min / IP) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /api/storage/s3/access-keys/{id}: delete: summary: Revoke S3 Access Key description: | Revoke an S3 access key by its id. The in-memory LRU cache is invalidated synchronously so subsequent S3 protocol requests with this credential return `403 InvalidAccessKeyId`. tags: - S3 Access Keys security: - apiKey: [] parameters: - name: id in: path required: true description: UUID of the access key to revoke (from the create/list response) schema: type: string format: uuid responses: '204': description: Access key revoked '401': description: Missing or invalid API key / admin token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Access key not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded (20 requests / 15 min / IP) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /storage/v1/s3/{path}: parameters: - name: path in: path required: true description: | Standard path-style S3 path, e.g. `my-bucket/object-key`, `my-bucket` (bucket-level ops), or empty for ListBuckets. schema: type: string description: | **AWS S3-compatible protocol gateway.** Speak the S3 protocol directly from any SigV4 client (AWS SDKs, `aws` CLI, `rclone`, Terraform, etc.) against this endpoint. Only path-style URLs are supported; SDKs must be configured with `forcePathStyle: true` and the signing region `us-east-2` (or the value of `AWS_REGION` on this backend). Supported operations: ListBuckets, CreateBucket, DeleteBucket, HeadBucket, ListObjectsV2, PutObject (streaming + UNSIGNED + pre-hashed), GetObject (incl. Range), HeadObject, DeleteObject, DeleteObjects (batch), CopyObject, CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListParts, plus `GetBucketLocation` / `GetBucketVersioning` stubs. Buckets and objects are shared with the REST API — an object uploaded via S3 protocol appears immediately in `GET /api/storage/buckets/{bucket}/objects` and vice versa. Example (AWS SDK v3): ``` new S3Client({ endpoint: 'https:///storage/v1/s3', region: 'us-east-2', forcePathStyle: true, credentials: { accessKeyId, secretAccessKey }, }); ``` Errors use S3-style XML (`......`). get: summary: S3 protocol (GET) description: Dispatched to ListBuckets / ListObjectsV2 / GetObject / HeadObject / ListParts / GetBucketLocation / GetBucketVersioning based on path shape and query. tags: - S3 Protocol security: - s3SigV4: [] responses: '200': description: Success '206': description: Partial content (Range GetObject) '403': description: SignatureDoesNotMatch / InvalidAccessKeyId / RequestTimeTooSkewed '404': description: NoSuchBucket / NoSuchKey '501': description: NotImplemented (backend is not S3-compatible, or op unsupported) put: summary: S3 protocol (PUT) description: Dispatched to PutObject / UploadPart / CopyObject / CreateBucket based on path shape, query, and headers. tags: - S3 Protocol security: - s3SigV4: [] responses: '200': description: Success '400': description: EntityTooLarge / InvalidArgument / InvalidBucketName / MalformedXML '403': description: SignatureDoesNotMatch / AuthorizationHeaderMalformed '409': description: BucketAlreadyOwnedByYou post: summary: S3 protocol (POST) description: Dispatched to CreateMultipartUpload / CompleteMultipartUpload / DeleteObjects based on query. tags: - S3 Protocol security: - s3SigV4: [] responses: '200': description: Success delete: summary: S3 protocol (DELETE) description: Dispatched to DeleteObject / DeleteBucket / AbortMultipartUpload based on path and query. tags: - S3 Protocol security: - s3SigV4: [] responses: '204': description: Success '409': description: BucketNotEmpty head: summary: S3 protocol (HEAD) description: Dispatched to HeadBucket / HeadObject based on path shape. tags: - S3 Protocol security: - s3SigV4: [] responses: '200': description: Success '404': description: NoSuchBucket / NoSuchKey components: securitySchemes: apiKey: type: apiKey in: header name: x-api-key s3SigV4: type: http scheme: aws4-hmac-sha256 description: | AWS SigV4 authentication using S3 access keys minted by `POST /api/storage/s3/access-keys`. Region must match the server's `AWS_REGION` (default `us-east-2`); service scope must be `s3`. schemas: StoredFile: type: object properties: bucket: type: string example: avatars description: Name of the bucket containing the object key: type: string example: user123.jpg description: Unique key identifying the object within the bucket size: type: integer example: 102400 description: Size of the file in bytes mimeType: type: string example: image/jpeg description: MIME type of the file uploadedAt: type: string format: date-time example: "2024-01-15T10:30:00Z" description: ISO timestamp when the file was uploaded url: type: string example: "/api/storage/buckets/avatars/objects/user123.jpg" description: URL to download the file required: - bucket - key - size - uploadedAt - url Pagination: type: object properties: limit: type: integer example: 100 offset: type: integer example: 0 total: type: integer example: 1 required: - limit - offset - total UploadStrategy: type: object required: - method - uploadUrl - key - confirmRequired properties: method: type: string enum: [presigned, direct] description: Upload method - presigned for S3, direct for local storage example: presigned uploadUrl: type: string description: URL to upload the file to example: https://s3-bucket.amazonaws.com/ fields: type: object description: Form fields for presigned POST (S3 only) additionalProperties: true example: bucket: my-s3-bucket key: app-key/avatars/profile.jpg X-Amz-Algorithm: AWS4-HMAC-SHA256 key: type: string description: Generated unique key for the file example: profile-photo-1234567890-abc123.jpg confirmRequired: type: boolean description: Whether upload confirmation is required example: true confirmUrl: type: string description: URL to confirm the upload (if confirmRequired is true) example: /api/storage/buckets/avatars/objects/profile.jpg/confirm-upload expiresAt: type: string format: date-time description: Expiration time for presigned URL (S3 only) example: "2025-09-05T01:00:00Z" DownloadStrategy: type: object required: - method - url properties: method: type: string enum: [presigned, direct] description: | Download method: - `direct`: Direct URL access (S3 public buckets or local storage) - `presigned`: Secure URL with signature and expiration (S3 private buckets) example: direct url: type: string description: URL to download the file from example: https://s3-bucket.s3.us-east-2.amazonaws.com/app-key/public-assets/logo.png expiresAt: type: string format: date-time description: Expiration time for presigned URLs (only present when method is 'presigned') example: "2025-09-05T01:00:00Z" headers: type: object description: Optional headers to include in the download request additionalProperties: true ErrorResponse: type: object required: - error - message - statusCode properties: error: type: string description: Error code for programmatic handling example: "VALIDATION_ERROR" message: type: string description: Human-readable error message example: "Invalid request" statusCode: type: integer description: HTTP status code example: 400 nextActions: type: string description: Suggested action to resolve the error example: "Check your request parameters" S3GatewayConfig: type: object description: Read-only config of the S3 protocol gateway. required: - endpoint - region properties: endpoint: type: string description: Full URL clients should configure as their S3 endpoint. example: https://proj.insforge.app/storage/v1/s3 region: type: string description: SigV4 signing region the gateway validates incoming requests against. example: us-east-2 S3AccessKey: type: object description: An S3 access key record (without the plaintext secret). required: - id - accessKeyId - description - createdAt - lastUsedAt properties: id: type: string format: uuid description: Internal UUID of the access key record example: 11111111-1111-1111-1111-111111111111 accessKeyId: type: string pattern: '^INSF[A-Z0-9]{16}$' description: 20-character access key id with fixed `INSF` prefix example: INSFABC123DEF456GH78 description: type: string nullable: true description: User-supplied label (max 200 chars) example: backup-script createdAt: type: string format: date-time example: "2026-04-22T00:00:00Z" lastUsedAt: type: string format: date-time nullable: true description: Updated asynchronously after each successful SigV4 verification example: null S3AccessKeyWithSecret: allOf: - $ref: '#/components/schemas/S3AccessKey' - type: object required: - secretAccessKey properties: secretAccessKey: type: string description: | 40-character base64url secret. **Returned only once** in the create response — retrieve, store, and keep it secret. example: x7K2-a_pL9qRs4N8vYzWcE1fH5gJ3mUtBoD6ViXk CreateS3AccessKeyRequest: type: object properties: description: type: string maxLength: 200 description: Optional label to help you identify the key later example: backup-script