@open-mercato/storage-s3 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7-develop.6869.1.d1ce9999ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/storage_s3/api/post/storage-providers/s3/upload.js +4 -4
- package/dist/modules/storage_s3/api/post/storage-providers/s3/upload.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/storage_s3/api/post/storage-providers/s3/upload.ts +4 -4
- package/src/modules/storage_s3/i18n/de.json +3 -1
- package/src/modules/storage_s3/i18n/en.json +2 -0
- package/src/modules/storage_s3/i18n/es.json +3 -1
- package/src/modules/storage_s3/i18n/ko.json +3 -1
- package/src/modules/storage_s3/i18n/pl.json +3 -1
|
@@ -144,17 +144,17 @@ async function POST(req) {
|
|
|
144
144
|
}
|
|
145
145
|
const code = error?.code;
|
|
146
146
|
if (code === "quota_exceeded") {
|
|
147
|
-
return NextResponse.json({ error: "Attachment storage quota exceeded for this tenant." }, { status: 413 });
|
|
147
|
+
return NextResponse.json({ error: t("storage_s3.errors.quotaExceeded", "Attachment storage quota exceeded for this tenant.") }, { status: 413 });
|
|
148
148
|
}
|
|
149
149
|
if (code === "quota_target_exists") {
|
|
150
|
-
return NextResponse.json({ error: "The target storage key already exists." }, { status: 409 });
|
|
150
|
+
return NextResponse.json({ error: t("storage_s3.errors.quotaTargetExists", "The target storage key already exists.") }, { status: 409 });
|
|
151
151
|
}
|
|
152
|
-
return NextResponse.json({ error: "Storage quota accounting is unavailable." }, { status: 500 });
|
|
152
|
+
return NextResponse.json({ error: t("storage_s3.errors.quotaUnavailable", "Storage quota accounting is unavailable.") }, { status: 500 });
|
|
153
153
|
}
|
|
154
154
|
} else {
|
|
155
155
|
const tenantUsageBytes = await readTenantStorageUsageBytes(driver, auth.tenantId, auth.orgId);
|
|
156
156
|
if (willExceedAttachmentTenantQuota(tenantUsageBytes, buffer.length)) {
|
|
157
|
-
return NextResponse.json({ error: "Attachment storage quota exceeded for this tenant." }, { status: 413 });
|
|
157
|
+
return NextResponse.json({ error: t("storage_s3.errors.quotaExceeded", "Attachment storage quota exceeded for this tenant.") }, { status: 413 });
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/storage_s3/api/post/storage-providers/s3/upload.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n} from '@open-mercato/core/modules/attachments/lib/security'\nimport {\n isMultipartUploadLimitError,\n isMultipartRequestWithinUploadLimit,\n parseMultipartFormDataWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '@open-mercato/core/modules/attachments/lib/upload-limits'\nimport { isS3KeyScopedToTenant } from '../../../../lib/key-scope'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\nimport type { AttachmentQuotaService } from '@open-mercato/core/modules/attachments/lib/quota-service'\nimport { reconcileTenantS3Objects } from '../../../../lib/quota-accounting'\nimport { randomUUID } from 'crypto'\n\nexport const metadata = {\n path: '/storage-providers/s3/upload',\n POST: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst responseSchema = z.object({\n key: z.string(),\n bucket: z.string(),\n size: z.number().int(),\n contentType: z.string().optional(),\n})\n\nfunction sanitizeFileName(name: string): string {\n return name.replace(/[^a-zA-Z0-9._-]/g, '_') || 'upload'\n}\n\nasync function resolveDriver(\n tenantId: string,\n orgId: string,\n): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver({ ...creds, organizationId: orgId, tenantId })\n}\n\nasync function readTenantStorageUsageBytes(\n driver: S3StorageDriver,\n tenantId: string,\n orgId: string,\n): Promise<number> {\n let totalBytes = 0\n let continuationToken: string | undefined\n\n do {\n const page = await driver.listObjects('', 1000, continuationToken)\n for (const file of page.files) {\n if (isS3KeyScopedToTenant(file.key, orgId, tenantId)) {\n totalBytes += file.size\n }\n }\n continuationToken = page.truncated ? page.nextContinuationToken : undefined\n } while (continuationToken)\n\n return totalBytes\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: t('storage_s3.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n\n const contentType = req.headers.get('content-type') ?? ''\n if (!contentType.includes('multipart/form-data')) {\n return NextResponse.json({ error: t('storage_s3.errors.multipartRequired', 'Expected multipart/form-data') }, { status: 400 })\n }\n\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n throw error\n }\n const file = form.get('file') as File | null\n if (!file) {\n return NextResponse.json({ error: t('storage_s3.errors.fileRequired', 'file field is required') }, { status: 400 })\n }\n\n const keyOverride = form.get('key') ? String(form.get('key')) : null\n\n if (keyOverride !== null && !isS3KeyScopedToTenant(keyOverride, auth.orgId, auth.tenantId)) {\n return NextResponse.json(\n { error: t('storage_s3.errors.keyOverrideAccessDenied', 'Access denied: key override is not scoped to this tenant.') },\n { status: 403 },\n )\n }\n\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({ error: t('storage_s3.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.') }, { status: 400 })\n }\n\n const effectiveMaxBytes = resolveAttachmentMaxBytes()\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: t('storage_s3.errors.integrationNotConfigured', 'S3 integration is not configured.') }, { status: 400 })\n }\n\n const buffer = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeFileName(file.name)\n const trustedMimeType = detectAttachmentMimeType(buffer, safeName, file.type || null)\n if (isActiveContentAttachment(buffer, safeName, trustedMimeType)) {\n return NextResponse.json({ error: t('storage_s3.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n\n const key =\n keyOverride ??\n `uploads/org_${auth.orgId}/tenant_${auth.tenantId}/${Date.now()}_${randomUUID().slice(0, 8)}_${safeName}`\n\n const { resolve } = await createRequestContainer()\n let attachmentQuotaService: AttachmentQuotaService | null = null\n let recoveryScheduler: ((\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>) | null = null\n try {\n attachmentQuotaService = resolve('attachmentQuotaService') as AttachmentQuotaService\n recoveryScheduler = resolve('storageS3QuotaRecoveryScheduler') as (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>\n } catch {\n // Backward-compatible fallback below when the attachments quota service is not registered.\n }\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n if (attachmentQuotaService) {\n try {\n await reconcileTenantS3Objects({\n driver,\n quotaService: attachmentQuotaService,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n reservation = await attachmentQuotaService.reserve({\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n bytes: buffer.length,\n source: 'storage_s3_upload',\n storageDriver: 's3',\n storagePath: key,\n })\n if (!recoveryScheduler) throw new Error('Storage quota recovery is unavailable.')\n await recoveryScheduler({\n reservationId: reservation.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => {})\n reservation = null\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') {\n return NextResponse.json({ error: 'Attachment storage quota exceeded for this tenant.' }, { status: 413 })\n }\n if (code === 'quota_target_exists') {\n return NextResponse.json({ error: 'The target storage key already exists.' }, { status: 409 })\n }\n return NextResponse.json({ error: 'Storage quota accounting is unavailable.' }, { status: 500 })\n }\n } else {\n const tenantUsageBytes = await readTenantStorageUsageBytes(driver, auth.tenantId, auth.orgId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, buffer.length)) {\n return NextResponse.json({ error: 'Attachment storage quota exceeded for this tenant.' }, { status: 413 })\n }\n }\n\n try {\n await driver.putObject(key, buffer, trustedMimeType)\n if (reservation) {\n await attachmentQuotaService!.markStored(reservation.id, reservation.leaseToken)\n await attachmentQuotaService!.completeStandalone(reservation.id, reservation.leaseToken, buffer.length)\n }\n } catch (error) {\n if (reservation) {\n try {\n await driver.deleteStrict('', key)\n await attachmentQuotaService!.release(reservation.id, reservation.leaseToken)\n } catch {\n // Retain the reservation when object absence cannot be proven.\n }\n }\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n return NextResponse.json({\n key,\n bucket: driver.getBucket(),\n size: buffer.length,\n contentType: trustedMimeType,\n })\n}\n\nexport default POST\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Upload file to S3',\n methods: {\n POST: {\n summary: 'Upload a file directly to S3',\n description: 'Uploads a file to the configured S3 bucket. Requires storage_providers.manage feature.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: z.object({\n file: z.any().describe('File to upload'),\n key: z.string().optional().describe('Optional S3 key override (must be scoped to org/tenant)'),\n contentType: z.string().optional().describe('Optional client-provided content-type hint; the server derives the trusted MIME type.'),\n }),\n },\n responses: [{ status: 200, description: 'Upload result', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Missing file, blocked file type, or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key override not scoped to this tenant', schema: z.object({ error: z.string() }) },\n { status: 413, description: 'Upload too large or tenant storage quota exceeded', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAEhC,SAAS,gCAAgC;AACzC,SAAS,kBAAkB;AAEpB,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;AAEA,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,KAAK,EAAE,OAAO;AAAA,EACd,QAAQ,EAAE,OAAO;AAAA,EACjB,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,aAAa,EAAE,OAAO,EAAE,SAAS;AACnC,CAAC;AAED,SAAS,iBAAiB,MAAsB;AAC9C,SAAO,KAAK,QAAQ,oBAAoB,GAAG,KAAK;AAClD;AAEA,eAAe,cACb,UACA,OACiC;AACjC,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,qBAAqB,QAAQ,+BAA+B;AAGlE,QAAM,QAAQ,MAAM,mBAAmB,QAAQ,cAAc,EAAE,UAAU,gBAAgB,MAAM,CAAC;AAChG,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,IAAI,gBAAgB,EAAE,GAAG,OAAO,gBAAgB,OAAO,SAAS,CAAC;AAC1E;AAEA,eAAe,4BACb,QACA,UACA,OACiB;AACjB,MAAI,aAAa;AACjB,MAAI;AAEJ,KAAG;AACD,UAAM,OAAO,MAAM,OAAO,YAAY,IAAI,KAAM,iBAAiB;AACjE,eAAW,QAAQ,KAAK,OAAO;AAC7B,UAAI,sBAAsB,KAAK,KAAK,OAAO,QAAQ,GAAG;AACpD,sBAAc,KAAK;AAAA,MACrB;AAAA,IACF;AACA,wBAAoB,KAAK,YAAY,KAAK,wBAAwB;AAAA,EACpE,SAAS;AAET,SAAO;AACT;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,kCAAkC,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1G;AAEA,MAAI,CAAC,oCAAoC,IAAI,QAAQ,IAAI,gBAAgB,CAAC,GAAG;AAC3E,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,mCAAmC,6CAA6C,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1I;AAEA,QAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;AACvD,MAAI,CAAC,YAAY,SAAS,qBAAqB,GAAG;AAChD,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,uCAAuC,8BAA8B,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/H;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,wCAAwC,GAAG;AAAA,EAC1D,SAAS,OAAO;AACd,QAAI,4BAA4B,KAAK,GAAG;AACtC,aAAO,aAAa,KAAK,EAAE,OAAO,EAAE,mCAAmC,6CAA6C,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1I;AACA,UAAM;AAAA,EACR;AACA,QAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,kCAAkC,wBAAwB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpH;AAEA,QAAM,cAAc,KAAK,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,KAAK,CAAC,IAAI;AAEhE,MAAI,gBAAgB,QAAQ,CAAC,sBAAsB,aAAa,KAAK,OAAO,KAAK,QAAQ,GAAG;AAC1F,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,EAAE,6CAA6C,2DAA2D,EAAE;AAAA,MACrH,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,gCAAgC,KAAK,IAAI,GAAG;AAC9C,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,yCAAyC,uDAAuD,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1J;AAEA,QAAM,oBAAoB,0BAA0B;AACpD,MAAI,KAAK,OAAO,mBAAmB;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,mCAAmC,6CAA6C,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1I;AAEA,QAAM,SAAS,MAAM,cAAc,KAAK,UAAU,KAAK,KAAK;AAC5D,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,8CAA8C,mCAAmC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3I;AAEA,QAAM,SAAS,OAAO,KAAK,MAAM,KAAK,YAAY,CAAC;AACnD,QAAM,WAAW,iBAAiB,KAAK,IAAI;AAC3C,QAAM,kBAAkB,yBAAyB,QAAQ,UAAU,KAAK,QAAQ,IAAI;AACpF,MAAI,0BAA0B,QAAQ,UAAU,eAAe,GAAG;AAChE,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,0CAA0C,yCAAyC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7I;AAEA,QAAM,MACJ,eACA,eAAe,KAAK,KAAK,WAAW,KAAK,QAAQ,IAAI,KAAK,IAAI,CAAC,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;AAEzG,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,MAAI,yBAAwD;AAC5D,MAAI,oBAGyB;AAC7B,MAAI;AACF,6BAAyB,QAAQ,wBAAwB;AACzD,wBAAoB,QAAQ,iCAAiC;AAAA,EAI/D,QAAQ;AAAA,EAER;AACA,MAAI,cAA0E;AAC9E,MAAI,wBAAwB;AAC1B,QAAI;AACF,YAAM,yBAAyB;AAAA,QAC7B;AAAA,QACA,cAAc;AAAA,QACd,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,MACvB,CAAC;AACD,oBAAc,MAAM,uBAAuB,QAAQ;AAAA,QACjD,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,QACrB,OAAO,OAAO;AAAA,QACd,QAAQ;AAAA,QACR,eAAe;AAAA,QACf,aAAa;AAAA,MACf,CAAC;AACD,UAAI,CAAC,kBAAmB,OAAM,IAAI,MAAM,wCAAwC;AAChF,YAAM,kBAAkB;AAAA,QACtB,eAAe,YAAY;AAAA,QAC3B,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,MACvB,GAAG,KAAK,IAAI,KAAO,YAAY,UAAU,QAAQ,IAAI,KAAK,IAAI,CAAC,CAAC;AAChE,YAAM,uBAAuB,aAAa,YAAY,IAAI,YAAY,UAAU;AAAA,IAClF,SAAS,OAAO;AACd,UAAI,aAAa;AACf,cAAM,uBAAuB,QAAQ,YAAY,IAAI,YAAY,UAAU,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAC3F,sBAAc;AAAA,MAChB;AACA,YAAM,OAAQ,OAA8B;AAC5C,UAAI,SAAS,kBAAkB;AAC7B,eAAO,aAAa,KAAK,EAAE,OAAO,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n} from '@open-mercato/core/modules/attachments/lib/security'\nimport {\n isMultipartUploadLimitError,\n isMultipartRequestWithinUploadLimit,\n parseMultipartFormDataWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '@open-mercato/core/modules/attachments/lib/upload-limits'\nimport { isS3KeyScopedToTenant } from '../../../../lib/key-scope'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\nimport type { AttachmentQuotaService } from '@open-mercato/core/modules/attachments/lib/quota-service'\nimport { reconcileTenantS3Objects } from '../../../../lib/quota-accounting'\nimport { randomUUID } from 'crypto'\n\nexport const metadata = {\n path: '/storage-providers/s3/upload',\n POST: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst responseSchema = z.object({\n key: z.string(),\n bucket: z.string(),\n size: z.number().int(),\n contentType: z.string().optional(),\n})\n\nfunction sanitizeFileName(name: string): string {\n return name.replace(/[^a-zA-Z0-9._-]/g, '_') || 'upload'\n}\n\nasync function resolveDriver(\n tenantId: string,\n orgId: string,\n): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver({ ...creds, organizationId: orgId, tenantId })\n}\n\nasync function readTenantStorageUsageBytes(\n driver: S3StorageDriver,\n tenantId: string,\n orgId: string,\n): Promise<number> {\n let totalBytes = 0\n let continuationToken: string | undefined\n\n do {\n const page = await driver.listObjects('', 1000, continuationToken)\n for (const file of page.files) {\n if (isS3KeyScopedToTenant(file.key, orgId, tenantId)) {\n totalBytes += file.size\n }\n }\n continuationToken = page.truncated ? page.nextContinuationToken : undefined\n } while (continuationToken)\n\n return totalBytes\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: t('storage_s3.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n\n const contentType = req.headers.get('content-type') ?? ''\n if (!contentType.includes('multipart/form-data')) {\n return NextResponse.json({ error: t('storage_s3.errors.multipartRequired', 'Expected multipart/form-data') }, { status: 400 })\n }\n\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n throw error\n }\n const file = form.get('file') as File | null\n if (!file) {\n return NextResponse.json({ error: t('storage_s3.errors.fileRequired', 'file field is required') }, { status: 400 })\n }\n\n const keyOverride = form.get('key') ? String(form.get('key')) : null\n\n if (keyOverride !== null && !isS3KeyScopedToTenant(keyOverride, auth.orgId, auth.tenantId)) {\n return NextResponse.json(\n { error: t('storage_s3.errors.keyOverrideAccessDenied', 'Access denied: key override is not scoped to this tenant.') },\n { status: 403 },\n )\n }\n\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({ error: t('storage_s3.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.') }, { status: 400 })\n }\n\n const effectiveMaxBytes = resolveAttachmentMaxBytes()\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({ error: t('storage_s3.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.') }, { status: 413 })\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: t('storage_s3.errors.integrationNotConfigured', 'S3 integration is not configured.') }, { status: 400 })\n }\n\n const buffer = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeFileName(file.name)\n const trustedMimeType = detectAttachmentMimeType(buffer, safeName, file.type || null)\n if (isActiveContentAttachment(buffer, safeName, trustedMimeType)) {\n return NextResponse.json({ error: t('storage_s3.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n\n const key =\n keyOverride ??\n `uploads/org_${auth.orgId}/tenant_${auth.tenantId}/${Date.now()}_${randomUUID().slice(0, 8)}_${safeName}`\n\n const { resolve } = await createRequestContainer()\n let attachmentQuotaService: AttachmentQuotaService | null = null\n let recoveryScheduler: ((\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>) | null = null\n try {\n attachmentQuotaService = resolve('attachmentQuotaService') as AttachmentQuotaService\n recoveryScheduler = resolve('storageS3QuotaRecoveryScheduler') as (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>\n } catch {\n // Backward-compatible fallback below when the attachments quota service is not registered.\n }\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n if (attachmentQuotaService) {\n try {\n await reconcileTenantS3Objects({\n driver,\n quotaService: attachmentQuotaService,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n reservation = await attachmentQuotaService.reserve({\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n bytes: buffer.length,\n source: 'storage_s3_upload',\n storageDriver: 's3',\n storagePath: key,\n })\n if (!recoveryScheduler) throw new Error('Storage quota recovery is unavailable.')\n await recoveryScheduler({\n reservationId: reservation.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => {})\n reservation = null\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') {\n return NextResponse.json({ error: t('storage_s3.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.') }, { status: 413 })\n }\n if (code === 'quota_target_exists') {\n return NextResponse.json({ error: t('storage_s3.errors.quotaTargetExists', 'The target storage key already exists.') }, { status: 409 })\n }\n return NextResponse.json({ error: t('storage_s3.errors.quotaUnavailable', 'Storage quota accounting is unavailable.') }, { status: 500 })\n }\n } else {\n const tenantUsageBytes = await readTenantStorageUsageBytes(driver, auth.tenantId, auth.orgId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, buffer.length)) {\n return NextResponse.json({ error: t('storage_s3.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.') }, { status: 413 })\n }\n }\n\n try {\n await driver.putObject(key, buffer, trustedMimeType)\n if (reservation) {\n await attachmentQuotaService!.markStored(reservation.id, reservation.leaseToken)\n await attachmentQuotaService!.completeStandalone(reservation.id, reservation.leaseToken, buffer.length)\n }\n } catch (error) {\n if (reservation) {\n try {\n await driver.deleteStrict('', key)\n await attachmentQuotaService!.release(reservation.id, reservation.leaseToken)\n } catch {\n // Retain the reservation when object absence cannot be proven.\n }\n }\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n return NextResponse.json({\n key,\n bucket: driver.getBucket(),\n size: buffer.length,\n contentType: trustedMimeType,\n })\n}\n\nexport default POST\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Upload file to S3',\n methods: {\n POST: {\n summary: 'Upload a file directly to S3',\n description: 'Uploads a file to the configured S3 bucket. Requires storage_providers.manage feature.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: z.object({\n file: z.any().describe('File to upload'),\n key: z.string().optional().describe('Optional S3 key override (must be scoped to org/tenant)'),\n contentType: z.string().optional().describe('Optional client-provided content-type hint; the server derives the trusted MIME type.'),\n }),\n },\n responses: [{ status: 200, description: 'Upload result', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Missing file, blocked file type, or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key override not scoped to this tenant', schema: z.object({ error: z.string() }) },\n { status: 413, description: 'Upload too large or tenant storage quota exceeded', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/storage-s3",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@aws-sdk/client-s3": "^3.1085.0",
|
|
76
76
|
"@aws-sdk/s3-request-presigner": "^3.1085.0",
|
|
77
|
-
"@open-mercato/core": "0.6.7-develop.
|
|
78
|
-
"@open-mercato/queue": "0.6.7-develop.
|
|
77
|
+
"@open-mercato/core": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
78
|
+
"@open-mercato/queue": "0.6.7-develop.6869.1.d1ce9999ea"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"@mikro-orm/postgresql": "^6.6.10",
|
|
82
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
82
|
+
"@open-mercato/shared": "0.6.7-develop.6869.1.d1ce9999ea"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
85
|
+
"@open-mercato/shared": "0.6.7-develop.6869.1.d1ce9999ea",
|
|
86
86
|
"@types/jest": "^30.0.0",
|
|
87
87
|
"esbuild": "^0.28.1",
|
|
88
88
|
"glob": "^13.0.6",
|
|
@@ -182,17 +182,17 @@ export async function POST(req: Request) {
|
|
|
182
182
|
}
|
|
183
183
|
const code = (error as { code?: unknown })?.code
|
|
184
184
|
if (code === 'quota_exceeded') {
|
|
185
|
-
return NextResponse.json({ error: 'Attachment storage quota exceeded for this tenant.' }, { status: 413 })
|
|
185
|
+
return NextResponse.json({ error: t('storage_s3.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.') }, { status: 413 })
|
|
186
186
|
}
|
|
187
187
|
if (code === 'quota_target_exists') {
|
|
188
|
-
return NextResponse.json({ error: 'The target storage key already exists.' }, { status: 409 })
|
|
188
|
+
return NextResponse.json({ error: t('storage_s3.errors.quotaTargetExists', 'The target storage key already exists.') }, { status: 409 })
|
|
189
189
|
}
|
|
190
|
-
return NextResponse.json({ error: 'Storage quota accounting is unavailable.' }, { status: 500 })
|
|
190
|
+
return NextResponse.json({ error: t('storage_s3.errors.quotaUnavailable', 'Storage quota accounting is unavailable.') }, { status: 500 })
|
|
191
191
|
}
|
|
192
192
|
} else {
|
|
193
193
|
const tenantUsageBytes = await readTenantStorageUsageBytes(driver, auth.tenantId, auth.orgId)
|
|
194
194
|
if (willExceedAttachmentTenantQuota(tenantUsageBytes, buffer.length)) {
|
|
195
|
-
return NextResponse.json({ error: 'Attachment storage quota exceeded for this tenant.' }, { status: 413 })
|
|
195
|
+
return NextResponse.json({ error: t('storage_s3.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.') }, { status: 413 })
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"storage_s3.errors.maxUploadSize": "Der Anhang überschreitet die maximal zulässige Upload-Größe.",
|
|
13
13
|
"storage_s3.errors.multipartRequired": "multipart/form-data wurde erwartet",
|
|
14
14
|
"storage_s3.errors.prefixAccessDenied": "Zugriff verweigert: Das Präfix ist nicht auf diesen Tenant beschränkt.",
|
|
15
|
-
"storage_s3.errors.quotaExceeded": "Das
|
|
15
|
+
"storage_s3.errors.quotaExceeded": "Das Speicherkontingent für Anhänge dieses Mandanten ist erschöpft.",
|
|
16
|
+
"storage_s3.errors.quotaTargetExists": "Der Ziel-Speicherschlüssel existiert bereits.",
|
|
17
|
+
"storage_s3.errors.quotaUnavailable": "Die Kontingentabrechnung des Speichers ist nicht verfügbar.",
|
|
16
18
|
"storage_s3.errors.unauthorized": "Nicht autorisiert"
|
|
17
19
|
}
|
|
@@ -13,5 +13,7 @@
|
|
|
13
13
|
"storage_s3.errors.multipartRequired": "Expected multipart/form-data",
|
|
14
14
|
"storage_s3.errors.prefixAccessDenied": "Access denied: prefix is not scoped to this tenant.",
|
|
15
15
|
"storage_s3.errors.quotaExceeded": "Attachment storage quota exceeded for this tenant.",
|
|
16
|
+
"storage_s3.errors.quotaTargetExists": "The target storage key already exists.",
|
|
17
|
+
"storage_s3.errors.quotaUnavailable": "Storage quota accounting is unavailable.",
|
|
16
18
|
"storage_s3.errors.unauthorized": "Unauthorized"
|
|
17
19
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"storage_s3.errors.maxUploadSize": "El archivo adjunto supera el tamaño máximo de carga permitido.",
|
|
13
13
|
"storage_s3.errors.multipartRequired": "Se esperaba multipart/form-data",
|
|
14
14
|
"storage_s3.errors.prefixAccessDenied": "Acceso denegado: el prefijo no está limitado a este tenant.",
|
|
15
|
-
"storage_s3.errors.quotaExceeded": "Se superó la cuota de almacenamiento de adjuntos para este
|
|
15
|
+
"storage_s3.errors.quotaExceeded": "Se superó la cuota de almacenamiento de adjuntos para este inquilino.",
|
|
16
|
+
"storage_s3.errors.quotaTargetExists": "La clave de almacenamiento de destino ya existe.",
|
|
17
|
+
"storage_s3.errors.quotaUnavailable": "La contabilidad de la cuota de almacenamiento no está disponible.",
|
|
16
18
|
"storage_s3.errors.unauthorized": "No autorizado"
|
|
17
19
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"storage_s3.errors.maxUploadSize": "첨부파일이 최대 업로드 크기를 초과합니다.",
|
|
13
13
|
"storage_s3.errors.multipartRequired": "예상되는 멀티파트/양식 데이터",
|
|
14
14
|
"storage_s3.errors.prefixAccessDenied": "액세스 거부됨: 접두사의 범위가 이 테넌트로 지정되지 않았습니다.",
|
|
15
|
-
"storage_s3.errors.quotaExceeded": "이
|
|
15
|
+
"storage_s3.errors.quotaExceeded": "이 테넌트의 첨부 파일 저장 용량 한도를 초과했습니다.",
|
|
16
|
+
"storage_s3.errors.quotaTargetExists": "대상 저장 키가 이미 존재합니다.",
|
|
17
|
+
"storage_s3.errors.quotaUnavailable": "저장 용량 정산을 사용할 수 없습니다.",
|
|
16
18
|
"storage_s3.errors.unauthorized": "승인되지 않은"
|
|
17
19
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"storage_s3.errors.maxUploadSize": "Załącznik przekracza maksymalny dopuszczalny rozmiar przesyłania.",
|
|
13
13
|
"storage_s3.errors.multipartRequired": "Oczekiwano multipart/form-data",
|
|
14
14
|
"storage_s3.errors.prefixAccessDenied": "Odmowa dostępu: prefiks nie jest ograniczony do tego tenantu.",
|
|
15
|
-
"storage_s3.errors.quotaExceeded": "Przekroczono limit
|
|
15
|
+
"storage_s3.errors.quotaExceeded": "Przekroczono limit przestrzeni na załączniki dla tego najemcy.",
|
|
16
|
+
"storage_s3.errors.quotaTargetExists": "Docelowy klucz w magazynie już istnieje.",
|
|
17
|
+
"storage_s3.errors.quotaUnavailable": "Rozliczanie limitu magazynu jest niedostępne.",
|
|
16
18
|
"storage_s3.errors.unauthorized": "Brak autoryzacji"
|
|
17
19
|
}
|