Skip to content

Commit 6b871fc

Browse files
committed
feat: make presigned URL expiry configurable via BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS
- Add `BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS` to env.ts Zod schema (default 7200s / 2 hours, range 60–604800) - Replace hardcoded `DEFAULT_PRESIGNED_EXPIRY_SECONDS` constant in s3-provider.ts with env var lookup - Update tests to use env var in mocks and verify new default - Add env var to .env.example files and deployment docs
1 parent 33883b1 commit 6b871fc

File tree

6 files changed

+35
-9
lines changed

6 files changed

+35
-9
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,6 @@ BLOB_STORAGE_LOCAL_PATH=.blob-storage
212212
# Custom endpoint URL. Often required for non-AWS providers (R2, MinIO, B2, Spaces, etc.).
213213
# BLOB_STORAGE_S3_ENDPOINT=
214214
# Path-style URLs. Default false for AWS S3. Some S3-compatible providers require true.
215-
# BLOB_STORAGE_S3_FORCE_PATH_STYLE=false
215+
# BLOB_STORAGE_S3_FORCE_PATH_STYLE=false
216+
# Expiry in seconds for S3 presigned media URLs. Default 7200 (2 hours). Range: 60–604800.
217+
# BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS=7200

agents-api/src/domains/run/services/__tests__/s3-provider.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ describe('S3BlobStorageProvider', () => {
118118
BLOB_STORAGE_S3_ACCESS_KEY_ID: 'key',
119119
BLOB_STORAGE_S3_SECRET_ACCESS_KEY: 'secret',
120120
BLOB_STORAGE_S3_FORCE_PATH_STYLE: true,
121+
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS: 7200,
121122
},
122123
}));
123124

@@ -129,7 +130,7 @@ describe('S3BlobStorageProvider', () => {
129130

130131
expect(url).toBe('https://bucket.s3.us-east-1.amazonaws.com/key?signed=true');
131132
expect(getSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
132-
expiresIn: 3600,
133+
expiresIn: 7200,
133134
});
134135
});
135136

@@ -142,17 +143,18 @@ describe('S3BlobStorageProvider', () => {
142143
BLOB_STORAGE_S3_ACCESS_KEY_ID: 'key',
143144
BLOB_STORAGE_S3_SECRET_ACCESS_KEY: 'secret',
144145
BLOB_STORAGE_S3_FORCE_PATH_STYLE: true,
146+
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS: 7200,
145147
},
146148
}));
147149

148150
const { S3BlobStorageProvider } = await import('../blob-storage/s3-provider');
149151
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
150152
const provider = new S3BlobStorageProvider();
151153

152-
await provider.getPresignedUrl('v1/t_tenant/media/file.png', 7200);
154+
await provider.getPresignedUrl('v1/t_tenant/media/file.png', 900);
153155

154156
expect(getSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
155-
expiresIn: 7200,
157+
expiresIn: 900,
156158
});
157159
});
158160

@@ -165,6 +167,7 @@ describe('S3BlobStorageProvider', () => {
165167
BLOB_STORAGE_S3_ACCESS_KEY_ID: 'key',
166168
BLOB_STORAGE_S3_SECRET_ACCESS_KEY: 'secret',
167169
BLOB_STORAGE_S3_FORCE_PATH_STYLE: true,
170+
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS: 7200,
168171
},
169172
}));
170173

agents-api/src/domains/run/services/blob-storage/s3-provider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {
1515
} from './types';
1616

1717
const logger = getLogger('S3BlobStorageProvider');
18-
const DEFAULT_PRESIGNED_EXPIRY_SECONDS = 3600;
1918

2019
export class S3BlobStorageProvider implements BlobStorageProvider {
2120
private client: S3Client;
@@ -120,7 +119,7 @@ export class S3BlobStorageProvider implements BlobStorageProvider {
120119
}
121120

122121
async getPresignedUrl(key: string, expiresInSeconds?: number): Promise<string> {
123-
const expiry = expiresInSeconds ?? DEFAULT_PRESIGNED_EXPIRY_SECONDS;
122+
const expiry = expiresInSeconds ?? env.BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS;
124123
logger.debug({ key, expiry }, 'Generating presigned URL');
125124
try {
126125
return await getSignedUrl(

agents-api/src/env.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,16 @@ const envSchema = z
294294
.describe(
295295
'Force path-style S3 URLs: false for AWS S3 (default), true for path-style/self-hosted S3-compatible.'
296296
),
297+
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS: z.coerce
298+
.number()
299+
.int()
300+
.min(60)
301+
.max(604800)
302+
.optional()
303+
.default(7200)
304+
.describe(
305+
'Expiry in seconds for S3 presigned media URLs. Must be between 60 and 604800 (7 days). Default 7200 (2 hours).'
306+
),
297307
})
298308
.superRefine((data, ctx) => {
299309
const hasS3Bucket =

agents-docs/content/deployment/add-other-services/s3-blob-storage.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The Inkeep Agent Framework stores file attachments (images, PDFs, documents) upl
1313
When S3 is configured, media URLs in conversation responses are **presigned S3 URLs** — clients fetch files directly from S3 with no proxy overhead. This provides:
1414
- **Zero-proxy delivery** — clients talk directly to S3, no function invocations for media reads
1515
- **Domain isolation** — media is served from `*.s3.amazonaws.com`, separate from your API domain
16-
- **Time-limited access** — presigned URLs expire after 1 hour
16+
- **Time-limited access** — presigned URLs expire after 2 hours (configurable via `BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS`)
1717

1818
When S3 is not configured, the framework falls back to the local filesystem with a server-side media proxy.
1919

@@ -68,6 +68,16 @@ BLOB_STORAGE_S3_ENDPOINT=https://your-custom-endpoint
6868
BLOB_STORAGE_S3_FORCE_PATH_STYLE=true
6969
```
7070

71+
### Optional: Presigned URL Expiry
72+
73+
By default, presigned URLs expire after 2 hours (7200 seconds). To customize:
74+
75+
```bash
76+
BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS=900 # 15 minutes
77+
```
78+
79+
Accepted range: 60–604800 (1 minute to 7 days).
80+
7181
## Storage Backend Priority
7282

7383
The framework selects a storage backend based on which environment variables are set:
@@ -86,5 +96,5 @@ Changing the storage backend after files have been uploaded will make previously
8696

8797
1. When a user sends a file attachment in a chat message, the file is uploaded to S3 using the configured credentials
8898
2. The file's location is stored in the database as an internal blob URI
89-
3. When conversation history is retrieved, blob URIs are resolved to presigned S3 URLs with a 1-hour expiry
99+
3. When conversation history is retrieved, blob URIs are resolved to presigned S3 URLs (default 2-hour expiry, configurable via `BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS`)
90100
4. Clients fetch files directly from S3 using the presigned URL — no proxy needed

create-agents-template/.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,6 @@ BLOB_STORAGE_LOCAL_PATH=.blob-storage
143143
# Custom endpoint URL. Often required for non-AWS providers (R2, MinIO, B2, Spaces, etc.).
144144
# BLOB_STORAGE_S3_ENDPOINT=
145145
# Path-style URLs. Default false for AWS S3. Some S3-compatible providers require true.
146-
# BLOB_STORAGE_S3_FORCE_PATH_STYLE=false
146+
# BLOB_STORAGE_S3_FORCE_PATH_STYLE=false
147+
# Expiry in seconds for S3 presigned media URLs. Default 7200 (2 hours). Range: 60–604800.
148+
# BLOB_STORAGE_PRESIGNED_URL_EXPIRY_SECONDS=7200

0 commit comments

Comments
 (0)