feat(server): improve blob sync (#15367)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Standardized `usePresignedURL` configuration for AWS S3 and Cloudflare
R2 (including `enabled`, `urlPrefix`, and `signKey`).
- Storage upload URL generation now supports both direct provider
presigning and server-mediated proxying based on configuration.
- **Bug Fixes**
- Tightened upload and multipart validation (content type/length checks,
header vs query consistency, and stricter expiration handling).
- Improved fallback behavior when direct upload URL initialization
fails.
- **Tests**
- Updated R2 storage proxy end-to-end coverage to match the new
URL/token behavior.
- **Documentation**
  - Refreshed self-hosted JSON schema guidance for upload URL settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-28 23:43:43 +08:00
committed by GitHub
parent b6fc0a2192
commit 00576e1e78
7 changed files with 490 additions and 347 deletions
@@ -27,6 +27,11 @@ export interface S3StorageConfig {
expiresInSeconds?: number;
signContentTypeForPut?: boolean;
};
usePresignedURL?: {
enabled: boolean;
urlPrefix?: string;
signKey?: string;
};
}
export const R2_JURISDICTIONS = ['default', 'eu'] as const;
@@ -34,11 +39,6 @@ export const R2_JURISDICTIONS = ['default', 'eu'] as const;
export interface R2StorageConfig extends Omit<S3StorageConfig, 'endpoint'> {
accountId: string;
jurisdiction?: (typeof R2_JURISDICTIONS)[number];
usePresignedURL?: {
enabled: boolean;
urlPrefix?: string;
signKey?: string;
};
}
export type StorageProviderConfig = { bucket: string } & (
@@ -115,6 +115,27 @@ const S3ConfigSchema: JSONSchema = {
},
},
},
usePresignedURL: {
type: 'object',
description:
'Controls browser upload URLs. Disabled or unavailable modes fall back to server uploads.',
properties: {
enabled: {
type: 'boolean',
description: 'Whether browser upload URLs are enabled.',
},
urlPrefix: {
type: 'string',
description:
'Optional custom origin for browser upload URLs. Provider presigned URLs also use this origin when signKey is not configured.',
},
signKey: {
type: 'string',
description:
'Optional HMAC key for signed upload URLs. Without urlPrefix, upload URLs use the server origin.',
},
},
},
},
};
@@ -189,28 +210,6 @@ export const StorageJSONSchema: JSONSchema = {
description:
'Optional jurisdiction for the cloudflare r2 endpoint. Set to "eu" for EU buckets.',
},
usePresignedURL: {
type: 'object' as const,
description:
'The presigned url config for the cloudflare r2 storage provider.',
properties: {
enabled: {
type: 'boolean' as const,
description:
'Whether to use presigned url for the cloudflare r2 storage provider.',
},
urlPrefix: {
type: 'string' as const,
description:
'The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: "https://storage.example.com"\nExample rule: is_timed_hmac_valid_v0("your_secret", http.request.uri, 10800, http.request.timestamp.sec, 6)',
},
signKey: {
type: 'string' as const,
description:
'The presigned key for the cloudflare r2 storage provider.',
},
},
},
},
},
},
@@ -1,3 +1,4 @@
import { createHmac } from 'node:crypto';
import { Readable } from 'node:stream';
import type { Response } from 'express';
@@ -68,3 +69,20 @@ export const SIGNED_URL_EXPIRED = 60 * 60; // 1 hour
export const STORAGE_PROXY_ROOT = '/api/storage';
export const PROXY_UPLOAD_PATH = `${STORAGE_PROXY_ROOT}/upload`;
export const PROXY_MULTIPART_PATH = `${STORAGE_PROXY_ROOT}/multipart`;
export function createStorageUploadToken(
path: string,
fields: (string | number)[],
expiresAt: number,
signKey: string
) {
const canonical = JSON.stringify([
'affine-storage-upload',
1,
'PUT',
path,
...fields,
expiresAt,
]);
return createHmac('sha256', signKey).update(canonical).digest('base64url');
}