mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat(storage): add native Azure Blob transport (#2871)
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"@aws-sdk/cloudfront-signer": "^3.998.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.998.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.998.0",
|
||||
"@azure/storage-blob": "^12.31.0",
|
||||
"@bull-board/api": "^6.20.6",
|
||||
"@bull-board/hono": "^6.20.6",
|
||||
"@bull-board/ui": "^6.20.6",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import path from 'node:path';
|
||||
import {
|
||||
BlobSASPermissions,
|
||||
BlobServiceClient,
|
||||
generateBlobSASQueryParameters,
|
||||
StorageSharedKeyCredential,
|
||||
} from '@azure/storage-blob';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
||||
import { ONE_HOUR } from '../../../constants/time';
|
||||
import { alphaid } from '../../id';
|
||||
import type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
|
||||
|
||||
export class AzureBlobProvider implements StorageProvider {
|
||||
private serviceClient: BlobServiceClient;
|
||||
private credential: StorageSharedKeyCredential;
|
||||
private containerName: string;
|
||||
|
||||
constructor() {
|
||||
const accountName = String(env('NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME'));
|
||||
const accountKey = String(env('NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY'));
|
||||
this.containerName = String(env('NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER'));
|
||||
|
||||
this.credential = new StorageSharedKeyCredential(accountName, accountKey);
|
||||
|
||||
const endpointOverride = env('NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT');
|
||||
const url = endpointOverride
|
||||
? `${endpointOverride}/${accountName}`
|
||||
: `https://${accountName}.blob.core.windows.net`;
|
||||
|
||||
this.serviceClient = new BlobServiceClient(url, this.credential);
|
||||
}
|
||||
|
||||
private buildSasUrl(key: string, permissions: BlobSASPermissions): string {
|
||||
const expiresOn = new Date(Date.now() + ONE_HOUR);
|
||||
|
||||
const sasToken = generateBlobSASQueryParameters(
|
||||
{
|
||||
containerName: this.containerName,
|
||||
blobName: key,
|
||||
permissions,
|
||||
expiresOn,
|
||||
},
|
||||
this.credential,
|
||||
).toString();
|
||||
|
||||
const blobClient = this.serviceClient.getContainerClient(this.containerName).getBlobClient(key);
|
||||
|
||||
return `${blobClient.url}?${sasToken}`;
|
||||
}
|
||||
|
||||
async getPresignPostUrl(fileName: string, _contentType: string, userId?: number): Promise<PresignedUrl> {
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
let slugified = slugify(name);
|
||||
if (slugified.length === 0 || slugified.length > 100) {
|
||||
slugified = alphaid(8);
|
||||
}
|
||||
|
||||
let key = `${alphaid(12)}/${slugified}${ext}`;
|
||||
if (userId) {
|
||||
key = `${userId}/${key}`;
|
||||
}
|
||||
|
||||
const url = this.buildSasUrl(key, BlobSASPermissions.parse('cw'));
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl> {
|
||||
const url = this.buildSasUrl(key, BlobSASPermissions.parse('cw'));
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async getPresignGetUrl(key: string): Promise<PresignedUrl> {
|
||||
const url = this.buildSasUrl(key, BlobSASPermissions.parse('r'));
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
||||
const { name, ext } = path.parse(input.name);
|
||||
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
|
||||
|
||||
const containerClient = this.serviceClient.getContainerClient(this.containerName);
|
||||
const blockBlobClient = containerClient.getBlockBlobClient(key);
|
||||
|
||||
const body = input.body instanceof ArrayBuffer ? Buffer.from(input.body) : input.body;
|
||||
|
||||
await blockBlobClient.uploadData(body, {
|
||||
blobHTTPHeaders: { blobContentType: input.type },
|
||||
});
|
||||
|
||||
return { key };
|
||||
}
|
||||
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
const containerClient = this.serviceClient.getContainerClient(this.containerName);
|
||||
await containerClient.deleteBlob(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { AzureBlobProvider } from './azure-blob-provider';
|
||||
import { S3Provider } from './s3-provider';
|
||||
import type { StorageProvider } from './storage-provider';
|
||||
|
||||
export type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
|
||||
|
||||
let cached: StorageProvider | null = null;
|
||||
|
||||
export const getStorageProvider = (): StorageProvider => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const transport = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
switch (transport) {
|
||||
case 's3':
|
||||
cached = new S3Provider();
|
||||
return cached;
|
||||
case 'azure-blob':
|
||||
cached = new AzureBlobProvider();
|
||||
return cached;
|
||||
default:
|
||||
throw new Error(`Invalid object storage transport: "${transport}". Expected "s3" or "azure-blob".`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import path from 'node:path';
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
||||
import { ONE_HOUR, ONE_SECOND } from '../../../constants/time';
|
||||
import { alphaid } from '../../id';
|
||||
import type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
|
||||
|
||||
export class S3Provider implements StorageProvider {
|
||||
private client: S3Client;
|
||||
|
||||
constructor() {
|
||||
const hasCredentials = env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
|
||||
|
||||
this.client = new S3Client({
|
||||
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
|
||||
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
|
||||
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
|
||||
credentials: hasCredentials
|
||||
? {
|
||||
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
|
||||
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async getPresignPostUrl(fileName: string, contentType: string, userId?: number): Promise<PresignedUrl> {
|
||||
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
let slugified = slugify(name);
|
||||
if (slugified.length === 0 || slugified.length > 100) {
|
||||
slugified = alphaid(8);
|
||||
}
|
||||
|
||||
let key = `${alphaid(12)}/${slugified}${ext}`;
|
||||
if (userId) {
|
||||
key = `${userId}/${key}`;
|
||||
}
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl> {
|
||||
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async getPresignGetUrl(key: string): Promise<PresignedUrl> {
|
||||
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
|
||||
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
|
||||
|
||||
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
|
||||
|
||||
const url = getCloudfrontSignedUrl({
|
||||
url: distributionUrl.toString(),
|
||||
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
|
||||
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
|
||||
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
|
||||
});
|
||||
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
async uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
||||
const { name, ext } = path.parse(input.name);
|
||||
|
||||
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
|
||||
|
||||
const body = input.body instanceof ArrayBuffer ? Buffer.from(input.body) : input.body;
|
||||
|
||||
await this.client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: input.type,
|
||||
}),
|
||||
);
|
||||
|
||||
return { key };
|
||||
}
|
||||
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
await this.client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type PresignedUrl = {
|
||||
key: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type UploadFileInput = {
|
||||
name: string;
|
||||
type: string;
|
||||
body: ArrayBuffer | Buffer;
|
||||
};
|
||||
|
||||
export type UploadFileResult = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
export interface StorageProvider {
|
||||
/**
|
||||
* Generate a presigned URL to upload a file by name. The provider chooses the
|
||||
* final object key (typically derived from a slugified file name plus a
|
||||
* random prefix) and returns it along with the signed URL.
|
||||
*/
|
||||
getPresignPostUrl(fileName: string, contentType: string, userId?: number): Promise<PresignedUrl>;
|
||||
|
||||
/**
|
||||
* Generate a presigned URL to upload to an already-known key (used for flows
|
||||
* where the destination has been chosen previously).
|
||||
*/
|
||||
getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl>;
|
||||
|
||||
/**
|
||||
* Generate a presigned URL to download a file by key.
|
||||
*/
|
||||
getPresignGetUrl(key: string): Promise<PresignedUrl>;
|
||||
|
||||
/**
|
||||
* Server-side upload of a file's bytes. Returns the chosen key.
|
||||
*/
|
||||
uploadFile(input: UploadFileInput): Promise<UploadFileResult>;
|
||||
|
||||
/**
|
||||
* Server-side delete of a file by key.
|
||||
*/
|
||||
deleteFile(key: string): Promise<void>;
|
||||
}
|
||||
@@ -77,7 +77,8 @@ export const putFileServerSide = async (file: File) => {
|
||||
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
|
||||
.with('s3', async () => putFileInS3(file))
|
||||
.with('s3', async () => putFileInObjectStorage(file))
|
||||
.with('azure-blob', async () => putFileInObjectStorage(file))
|
||||
.otherwise(async () => putFileInDatabase(file));
|
||||
};
|
||||
|
||||
@@ -94,7 +95,7 @@ const putFileInDatabase = async (file: File) => {
|
||||
};
|
||||
};
|
||||
|
||||
const putFileInS3 = async (file: File) => {
|
||||
const putFileInObjectStorage = async (file: File) => {
|
||||
const buffer = await file.arrayBuffer();
|
||||
|
||||
const blob = new Blob([buffer], { type: file.type });
|
||||
|
||||
@@ -45,7 +45,8 @@ export const putFile = async (file: File) => {
|
||||
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
|
||||
.with('s3', async () => putFileInS3(file))
|
||||
.with('s3', async () => putFileInObjectStorage(file, {}))
|
||||
.with('azure-blob', async () => putFileInObjectStorage(file, { 'x-ms-blob-type': 'BlockBlob' }))
|
||||
.otherwise(async () => putFileInDatabase(file));
|
||||
};
|
||||
|
||||
@@ -62,7 +63,7 @@ const putFileInDatabase = async (file: File) => {
|
||||
};
|
||||
};
|
||||
|
||||
const putFileInS3 = async (file: File) => {
|
||||
const putFileInObjectStorage = async (file: File, extraHeaders: Record<string, string>) => {
|
||||
const getPresignedUrlResponse = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/files/presigned-post-url`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -82,16 +83,17 @@ const putFileInS3 = async (file: File) => {
|
||||
|
||||
const body = await file.arrayBuffer();
|
||||
|
||||
const reponse = await fetch(url, {
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
...extraHeaders,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!reponse.ok) {
|
||||
throw new Error(`Failed to upload file "${file.name}", failed with status code ${reponse.status}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload file "${file.name}", failed with status code ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,154 +1,31 @@
|
||||
import path from 'node:path';
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
||||
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
|
||||
import { alphaid } from '../id';
|
||||
import { getStorageProvider } from './providers';
|
||||
|
||||
export const getPresignPostUrl = async (fileName: string, contentType: string, userId?: number) => {
|
||||
const client = getS3Client();
|
||||
|
||||
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
// Get the basename and extension for the file
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
let slugified = slugify(name);
|
||||
|
||||
// If the slugified name is empty or too long, generate a random string instead
|
||||
//
|
||||
// This is fine since we don't really need the filename in s3 since we store it
|
||||
// in the database and can always get the original filename from there.
|
||||
//
|
||||
// The slugified name can be empty when a string contains only CJK or other
|
||||
// special characters.
|
||||
if (slugified.length === 0 || slugified.length > 100) {
|
||||
slugified = alphaid(8);
|
||||
}
|
||||
|
||||
let key = `${alphaid(12)}/${slugified}${ext}`;
|
||||
|
||||
if (userId) {
|
||||
key = `${userId}/${key}`;
|
||||
}
|
||||
|
||||
const putObjectCommand = new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(client, putObjectCommand, {
|
||||
expiresIn: ONE_HOUR / ONE_SECOND,
|
||||
});
|
||||
|
||||
return { key, url };
|
||||
return getStorageProvider().getPresignPostUrl(fileName, contentType, userId);
|
||||
};
|
||||
|
||||
export const getAbsolutePresignPostUrl = async (key: string) => {
|
||||
const client = getS3Client();
|
||||
|
||||
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
const putObjectCommand = new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const url = await getS3SignedUrl(client, putObjectCommand, {
|
||||
expiresIn: ONE_HOUR / ONE_SECOND,
|
||||
});
|
||||
|
||||
return { key, url };
|
||||
return getStorageProvider().getAbsolutePresignPostUrl(key);
|
||||
};
|
||||
|
||||
export const getPresignGetUrl = async (key: string) => {
|
||||
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
|
||||
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
|
||||
|
||||
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
|
||||
|
||||
const url = getCloudfrontSignedUrl({
|
||||
url: distributionUrl.toString(),
|
||||
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
|
||||
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
|
||||
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
|
||||
});
|
||||
|
||||
return { key, url };
|
||||
}
|
||||
|
||||
const client = getS3Client();
|
||||
|
||||
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
||||
|
||||
const getObjectCommand = new GetObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const url = await getS3SignedUrl(client, getObjectCommand, {
|
||||
expiresIn: ONE_HOUR / ONE_SECOND,
|
||||
});
|
||||
|
||||
return { key, url };
|
||||
return getStorageProvider().getPresignGetUrl(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file to S3.
|
||||
* Uploads a file server-side. Name preserved for backward compatibility with
|
||||
* existing callers; underneath it delegates to the active storage provider.
|
||||
*/
|
||||
export const uploadS3File = async (file: File) => {
|
||||
const client = getS3Client();
|
||||
|
||||
// Get the basename and extension for the file
|
||||
const { name, ext } = path.parse(file.name);
|
||||
|
||||
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const response = await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
Body: Buffer.from(fileBuffer),
|
||||
ContentType: file.type,
|
||||
}),
|
||||
);
|
||||
|
||||
return { key, response };
|
||||
const buffer = await file.arrayBuffer();
|
||||
const { key } = await getStorageProvider().uploadFile({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
body: buffer,
|
||||
});
|
||||
return { key };
|
||||
};
|
||||
|
||||
export const deleteS3File = async (key: string) => {
|
||||
const client = getS3Client();
|
||||
|
||||
await client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getS3Client = () => {
|
||||
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
if (NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
||||
throw new Error('Invalid upload transport');
|
||||
}
|
||||
|
||||
const hasCredentials = env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
|
||||
|
||||
return new S3Client({
|
||||
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
|
||||
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
|
||||
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
|
||||
credentials: hasCredentials
|
||||
? {
|
||||
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
|
||||
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
return getStorageProvider().deleteFile(key);
|
||||
};
|
||||
|
||||
Vendored
+5
-1
@@ -22,7 +22,7 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT?: 'database' | 's3';
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT?: 'database' | 's3' | 'azure-blob';
|
||||
NEXT_PRIVATE_UPLOAD_ENDPOINT?: string;
|
||||
NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE?: string;
|
||||
NEXT_PRIVATE_UPLOAD_REGION?: string;
|
||||
@@ -32,6 +32,10 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN?: string;
|
||||
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID?: string;
|
||||
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS?: string;
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME?: string;
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY?: string;
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER?: string;
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT?: string;
|
||||
|
||||
NEXT_PRIVATE_SIGNING_TRANSPORT?: 'local' | 'http' | 'gcloud-hsm';
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE?: string;
|
||||
|
||||
Reference in New Issue
Block a user