@open-mercato/core 0.10.1-develop.7907.1.28240ddda3 → 0.10.1-develop.7917.1.c74e12c1d2

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 6262 entry points
1
+ [build:core] found 6263 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 271 entry points
4
4
  [build:core:generated] built successfully
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/forms/frontend/embed/%5Bslug%5D/page.meta.ts"],
4
- "sourcesContent": ["import type { PageMetadata } from '@open-mercato/shared/modules/registry'\n\n/**\n * External iframe host page for an embeddable OPEN distribution (spec\n * `2026-05-21-forms-render-surfaces.md`, S4 / D4\u2013D6). Unauthenticated by design\n * \u2014 the slug is the bearer of access, and the anonymous public runtime enforces\n * availability / cap / CAPTCHA exactly as `/f/:slug`.\n *\n * SECURITY (R-RS-1) \u2014 HOST PLUMBING NOT WIRED IN THIS REPO.\n *\n * The design calls for this route to be served with a per-distribution\n * `Content-Security-Policy: frame-ancestors <allowedDomains>` header and no\n * `X-Frame-Options`, with the app's global frame protection carving `/embed/`\n * out so the dynamic header is the sole authority, resolved via\n * `GET /api/forms/public/distributions/:slug/embed-policy`\n * (\u2192 `buildFrameAncestorsCsp` in `lib/embed-frame-policy.ts`).\n *\n * Neither half exists yet. `apps/mercato/next.config.ts` applies\n * `frame-ancestors 'self'` plus `X-Frame-Options: SAMEORIGIN` to `/:path*` with\n * no `/embed/` carve-out, and `apps/mercato/src/proxy.ts` never calls the\n * embed-policy endpoint. It was absent from the module's original repository\n * too \u2014 that sandbox app declared no `headers()` at all, so `/embed/:slug` was\n * framable there by default rather than by policy.\n *\n * Consequence: cross-origin framing of this page is currently BLOCKED by the\n * app's global policy. That is the fail-closed direction, so the page is safe\n * and the embed surface is inert. Everything below the header is complete and\n * tested \u2014 availability / cap / CAPTCHA enforcement, theme application, the\n * allowlist normalizer, the `embed-policy` endpoint. Only the header is missing.\n *\n * Both halves are load-bearing: a carve-out without the dynamic header would\n * leave this page framable by anyone. A per-request response header cannot come\n * from a React Server Component, so it belongs in the proxy. Tracked in\n * `.ai/specs/2026-09-30-forms-module-into-core.md` \u00A7 Known limitations.\n */\nexport const metadata: PageMetadata = {\n requireAuth: false,\n titleKey: 'forms.runner.loading',\n title: 'Form',\n}\n\nexport default metadata\n"],
5
- "mappings": "AAmCO,MAAM,WAAyB;AAAA,EACpC,aAAa;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AACT;AAEA,IAAO,oBAAQ;",
4
+ "sourcesContent": ["import type { PageMetadata } from '@open-mercato/shared/modules/registry'\n\n/**\n * External iframe host page for an embeddable OPEN distribution (spec\n * `2026-05-21-forms-render-surfaces.md`, S4 / D4\u2013D6). Unauthenticated by design\n * \u2014 the slug is the bearer of access, and the anonymous public runtime enforces\n * availability / cap / CAPTCHA exactly as `/f/:slug`.\n *\n * SECURITY (R-RS-1) \u2014 HOST PLUMBING NOT WIRED IN THIS REPO.\n *\n * The design calls for this route to be served with a per-distribution\n * `Content-Security-Policy: frame-ancestors <allowedDomains>` header and no\n * `X-Frame-Options`, with the app's global frame protection carving `/embed/`\n * out so the dynamic header is the sole authority, resolved via\n * `GET /api/forms/public/distributions/:slug/embed-policy`\n * (\u2192 `buildFrameAncestorsCsp` in `lib/embed-frame-policy.ts`).\n *\n * Neither half exists yet. `apps/mercato/next.config.ts` applies\n * `frame-ancestors 'self'` plus `X-Frame-Options: SAMEORIGIN` to `/:path*` with\n * no `/embed/` carve-out (`next dev` additionally admits loopback origins so\n * local tools can preview the app), and `apps/mercato/src/proxy.ts` never calls the\n * embed-policy endpoint. It was absent from the module's original repository\n * too \u2014 that sandbox app declared no `headers()` at all, so `/embed/:slug` was\n * framable there by default rather than by policy.\n *\n * Consequence: cross-origin framing of this page is currently BLOCKED by the\n * app's global policy. That is the fail-closed direction, so the page is safe\n * and the embed surface is inert. Everything below the header is complete and\n * tested \u2014 availability / cap / CAPTCHA enforcement, theme application, the\n * allowlist normalizer, the `embed-policy` endpoint. Only the header is missing.\n *\n * Both halves are load-bearing: a carve-out without the dynamic header would\n * leave this page framable by anyone. A per-request response header cannot come\n * from a React Server Component, so it belongs in the proxy. Tracked in\n * `.ai/specs/2026-09-30-forms-module-into-core.md` \u00A7 Known limitations.\n */\nexport const metadata: PageMetadata = {\n requireAuth: false,\n titleKey: 'forms.runner.loading',\n title: 'Form',\n}\n\nexport default metadata\n"],
5
+ "mappings": "AAoCO,MAAM,WAAyB;AAAA,EACpC,aAAa;AAAA,EACb,UAAU;AAAA,EACV,OAAO;AACT;AAEA,IAAO,oBAAQ;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,8 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
6
6
  import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
7
7
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
8
+ import { checkRateLimit, rateLimitErrorSchema, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from "@open-mercato/shared/lib/ratelimit/helpers";
9
+ import { resolveAttachmentsUploadRateLimitConfig } from "@open-mercato/core/modules/attachments/lib/ocrLimits";
8
10
  import { createLogger } from "@open-mercato/shared/lib/logger";
9
11
  import { Attachment, AttachmentPartition } from "@open-mercato/core/modules/attachments/data/entities";
10
12
  import { buildAttachmentImageUrl, slugifyAttachmentFileName } from "@open-mercato/core/modules/attachments/lib/imageUrls";
@@ -307,6 +309,21 @@ async function handleUpload(req, replacement) {
307
309
  if (contextOrResponse instanceof Response) return contextOrResponse;
308
310
  const context = contextOrResponse;
309
311
  const { t, translate } = await resolveTranslations();
312
+ let rateLimiterService = null;
313
+ try {
314
+ rateLimiterService = context.container.resolve("rateLimiterService") ?? null;
315
+ } catch {
316
+ logger.warn("Portal attachment upload rate limiter unavailable; throttle skipped");
317
+ }
318
+ if (rateLimiterService) {
319
+ const rateLimitResponse = await checkRateLimit(
320
+ rateLimiterService,
321
+ resolveAttachmentsUploadRateLimitConfig(),
322
+ `portal:${context.tenantId}:${context.auth.sub}`,
323
+ t(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK)
324
+ );
325
+ if (rateLimitResponse) return rateLimitResponse;
326
+ }
310
327
  const contentType = req.headers.get("content-type") || "";
311
328
  if (!contentType.toLowerCase().includes("multipart/form-data")) {
312
329
  return NextResponse.json({ ok: false, error: translate("warranty_claims.errors.invalidInput", "Invalid input") }, { status: 400 });
@@ -457,6 +474,7 @@ const openApi = {
457
474
  },
458
475
  POST: {
459
476
  summary: "Upload an attachment for an owned claim",
477
+ errors: [{ status: 429, description: "Attachment upload rate limit exceeded", schema: rateLimitErrorSchema }],
460
478
  requestBody: { contentType: "multipart/form-data", schema: uploadBodySchema },
461
479
  responses: [
462
480
  {
@@ -468,6 +486,7 @@ const openApi = {
468
486
  },
469
487
  PUT: {
470
488
  summary: "Replace an attachment on an owned claim",
489
+ errors: [{ status: 429, description: "Attachment upload rate limit exceeded", schema: rateLimitErrorSchema }],
471
490
  requestBody: { contentType: "multipart/form-data", schema: replaceBodySchema },
472
491
  responses: [
473
492
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/warranty_claims/api/portal/attachments/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { StorageDriverFactory } from '@open-mercato/core/modules/attachments/lib/drivers'\nimport { checkAttachmentAccess } from '@open-mercato/core/modules/attachments/lib/access'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '@open-mercato/core/modules/attachments/lib/crud'\nimport { readAttachmentMetadata } from '@open-mercato/core/modules/attachments/lib/metadata'\nimport { clearAttachmentThumbnailCache } from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { removeAttachmentRecord } from '@open-mercato/core/modules/attachments/lib/storageReferences'\nimport {\n isMultipartUploadLimitError,\n parseMultipartFormDataWithinUploadLimit,\n resolveDefaultAttachmentMaxUploadBytes,\n} from '@open-mercato/core/modules/attachments/lib/upload-limits'\nimport {\n isScopedAttachmentUploadError,\n type ScopedAttachmentUploadErrorCode,\n} from '@open-mercato/core/modules/attachments/lib/scoped-upload-service'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { WarrantyClaim } from '../../../data/entities'\nimport { WARRANTY_CLAIM_RESOURCE_KIND } from '../../../commands/shared'\nimport { CUSTOMER_VISIBLE_ATTACHMENT_TAG, isCustomerVisibleAttachment } from '../../../lib/attachmentVisibility'\nimport { loadPortalOwnedClaim } from '../../../lib/portalClaimAccess'\nimport { resolvePortalAttachmentUploadService } from '../../../lib/portalAttachmentUpload'\nimport { resolveLinkedCustomerAuth } from '../../../lib/portalAuthGuard'\n\nconst CLAIM_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim'\nconst logger = createLogger('warranty_claims').child({ route: 'portal-attachments' })\nconst ATTACHMENT_ERROR_TRANSLATIONS: Partial<Record<ScopedAttachmentUploadErrorCode, readonly [string, string]>> = {\n dangerous_executable: ['attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'],\n max_upload_size: ['attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'],\n active_content: ['attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.'],\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n PUT: { requireAuth: false },\n DELETE: { requireAuth: false },\n}\n\nconst attachmentQuerySchema = z.object({\n claimId: z.string().uuid(),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n})\n\nconst uploadBodySchema = z.object({\n claimId: z.string().uuid(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n})\n\nconst replaceBodySchema = uploadBodySchema.extend({\n attachmentId: z.string().uuid(),\n})\n\nconst deleteQuerySchema = z.object({\n attachmentId: z.string().uuid(),\n})\n\ntype PortalContext = {\n auth: CustomerAuthContext\n customerId: string\n tenantId: string\n organizationId: string\n em: EntityManager\n container: Awaited<ReturnType<typeof createRequestContainer>>\n}\n\nfunction toIso(value: Date | string | null | undefined): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date.toISOString()\n}\n\nasync function resolvePortalContext(req: Request): Promise<PortalContext | Response> {\n const authOrResponse = await resolveLinkedCustomerAuth(req)\n if (authOrResponse instanceof Response) return authOrResponse\n const auth = authOrResponse\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return {\n auth,\n customerId: auth.customerEntityId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n em,\n container,\n }\n}\n\nfunction attachmentAuth(context: PortalContext): NonNullable<AuthContext> {\n return {\n sub: context.auth.sub,\n tenantId: context.tenantId,\n orgId: context.organizationId,\n email: context.auth.email,\n }\n}\n\nasync function loadOwnedClaim(context: PortalContext, claimId: string): Promise<WarrantyClaim | null> {\n return loadPortalOwnedClaim(\n context.em,\n {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n customerId: context.customerId,\n },\n claimId,\n )\n}\n\nasync function runPortalAttachmentGuard(\n req: Request,\n context: PortalContext,\n claimId: string,\n operation: 'create' | 'update' | 'delete',\n mutationPayload: Record<string, unknown>,\n): Promise<RouteMutationGuardResult> {\n return runRouteMutationGuards({\n container: context.container,\n req,\n auth: {\n userId: context.auth.sub,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: [],\n },\n input: {\n resourceKind: WARRANTY_CLAIM_RESOURCE_KIND,\n resourceId: claimId,\n operation,\n mutationPayload,\n },\n })\n}\n\nfunction serializeAttachment(attachment: Attachment) {\n const metadata = readAttachmentMetadata(attachment.storageMetadata)\n return {\n id: attachment.id,\n url: attachment.url,\n downloadUrl: `/api/warranty_claims/portal/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,\n fileName: attachment.fileName,\n fileSize: attachment.fileSize,\n mimeType: attachment.mimeType ?? null,\n partitionCode: attachment.partitionCode,\n content: attachment.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(attachment.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(attachment.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n createdAt: toIso(attachment.createdAt),\n }\n}\n\nasync function resolveStorageDriverFactory(context: PortalContext): Promise<StorageDriverFactory> {\n try {\n const resolved = context.container.resolve('storageDriverFactory') as StorageDriverFactory | null\n return resolved ?? new StorageDriverFactory(context.em)\n } catch {\n return new StorageDriverFactory(context.em)\n }\n}\n\nasync function loadOwnedVisibleAttachment(\n context: PortalContext,\n attachmentId: string,\n): Promise<{ attachment: Attachment; claim: WarrantyClaim } | null> {\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const attachment = await findOneWithDecryption(\n context.em,\n Attachment,\n {\n id: attachmentId,\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n {},\n scope,\n )\n if (!attachment) return null\n const claim = await loadOwnedClaim(context, attachment.recordId)\n if (!claim) return null\n if (!isCustomerVisibleAttachment(readAttachmentMetadata(attachment.storageMetadata).tags)) return null\n return { attachment, claim }\n}\n\nasync function deletePortalAttachment(context: PortalContext, attachment: Attachment): Promise<void> {\n const removal = await removeAttachmentRecord(context.em, attachment)\n if (!removal.removed) return\n await clearAttachmentThumbnailCache(attachment.partitionCode, attachment.id).catch((error) => {\n logger.error('Failed to clean portal attachment thumbnails', { err: error, attachmentId: attachment.id })\n })\n if (attachment.storagePath && removal.releaseStorage) {\n try {\n const driver = await (await resolveStorageDriverFactory(context)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? context.tenantId,\n organizationId: attachment.organizationId ?? context.organizationId,\n })\n await driver.delete(attachment.partitionCode, attachment.storagePath)\n } catch (error) {\n logger.error('Failed to remove portal attachment storage', { err: error, attachmentId: attachment.id })\n }\n }\n\n let dataEngine: DataEngine | null = null\n try {\n dataEngine = context.container.resolve<DataEngine>('dataEngine') ?? null\n } catch {\n dataEngine = null\n }\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n}\n\nasync function streamOwnedAttachment(\n context: PortalContext,\n attachmentId: string,\n translate: (key: string, fallback?: string) => string,\n): Promise<Response> {\n const owned = await loadOwnedVisibleAttachment(context, attachmentId)\n if (!owned) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const { attachment } = owned\n const partition = await context.em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data.') }, { status: 500 })\n }\n const access = checkAttachmentAccess(attachmentAuth(context), attachment, partition, { requireAuthForPublic: true })\n if (!access.ok) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const driver = await (await resolveStorageDriverFactory(context)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n let buffer: Buffer\n try {\n buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer\n } catch {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const headers: Record<string, string> = {\n 'Cache-Control': 'private, max-age=60',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n 'Content-Type': 'application/octet-stream',\n 'Content-Disposition': `attachment; filename=\"${slugifyAttachmentFileName(attachment.fileName)}\"`,\n 'X-Content-Type-Options': 'nosniff',\n }\n if (attachment.fileSize > 0) headers['Content-Length'] = String(attachment.fileSize)\n return new NextResponse(new Uint8Array(buffer), { status: 200, headers })\n}\n\nexport async function GET(req: Request) {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { translate } = await resolveTranslations()\n const url = new URL(req.url)\n const attachmentId = url.searchParams.get('attachmentId')\n if (attachmentId) {\n const attachmentIdParsed = z.string().uuid().safeParse(attachmentId)\n if (!attachmentIdParsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n return streamOwnedAttachment(context, attachmentIdParsed.data, translate)\n }\n const parsed = attachmentQuerySchema.safeParse({\n claimId: url.searchParams.get('claimId') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const claim = await loadOwnedClaim(context, parsed.data.claimId)\n if (!claim) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const filter = {\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n recordId: claim.id,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n }\n const page = parsed.data.page ?? 1\n const pageSize = parsed.data.pageSize ?? 100\n const attachments = await findWithDecryption(\n context.em,\n Attachment,\n filter,\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n const partitionCodes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)))\n const partitions = partitionCodes.length\n ? await context.em.find(AttachmentPartition, { code: { $in: partitionCodes } })\n : []\n const partitionsByCode = new Map(partitions.map((partition) => [partition.code, partition]))\n const auth = attachmentAuth(context)\n const visible = attachments.filter((attachment) => {\n const partition = partitionsByCode.get(attachment.partitionCode)\n if (!partition) return false\n if (!isCustomerVisibleAttachment(readAttachmentMetadata(attachment.storageMetadata).tags)) return false\n return checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok\n })\n\n const pagedVisible = visible.slice((page - 1) * pageSize, page * pageSize)\n\n return NextResponse.json({\n items: pagedVisible.map(serializeAttachment),\n total: visible.length,\n page,\n pageSize,\n totalPages: Math.max(1, Math.ceil(visible.length / pageSize)),\n })\n}\n\nfunction uploadTooLargeResponse(t: Awaited<ReturnType<typeof resolveTranslations>>['t']): Response {\n return NextResponse.json({\n ok: false,\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n code: 'max_upload_size',\n }, { status: 413 })\n}\n\nasync function handleUpload(req: Request, replacement: boolean): Promise<Response> {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { t, translate } = await resolveTranslations()\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) return uploadTooLargeResponse(t)\n throw error\n }\n const parsed = uploadBodySchema.safeParse({\n claimId: form.get('claimId') ?? undefined,\n file: form.get('file') instanceof File ? (form.get('file') as File).name : '',\n })\n const parsedAttachmentId = replacement\n ? z.string().uuid().safeParse(form.get('attachmentId') ?? undefined)\n : null\n if (!parsed.success || (replacement && !parsedAttachmentId?.success)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const claim = await loadOwnedClaim(context, parsed.data.claimId)\n if (!claim) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const file = form.get('file')\n if (!(file instanceof File)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.fileRequired', 'Choose a file before uploading.') }, { status: 400 })\n }\n if (file.size > resolveDefaultAttachmentMaxUploadBytes()) return uploadTooLargeResponse(t)\n const buffer = Buffer.from(await file.arrayBuffer())\n\n const attachmentId = replacement && parsedAttachmentId?.success ? parsedAttachmentId.data : null\n const replacementTarget = attachmentId\n ? await loadOwnedVisibleAttachment(context, attachmentId)\n : null\n if (replacement && (!replacementTarget || replacementTarget.claim.id !== claim.id)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n\n const guarded = await runPortalAttachmentGuard(req, context, claim.id, replacement ? 'update' : 'create', {\n claimId: claim.id,\n attachmentId,\n fileName: file.name,\n fileSize: file.size,\n mimeType: file.type,\n })\n if (!guarded.ok) {\n return guarded.response\n }\n\n const uploadService = resolvePortalAttachmentUploadService(context.container)\n if (!uploadService) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.') }, { status: 503 })\n }\n\n let attachment: Attachment\n try {\n attachment = await uploadService.upload({\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n recordId: claim.id,\n fileName: file.name,\n buffer,\n declaredMimeType: file.type,\n tags: [CUSTOMER_VISIBLE_ATTACHMENT_TAG],\n })\n } catch (error) {\n if (isScopedAttachmentUploadError(error)) {\n const [key, fallback] = ATTACHMENT_ERROR_TRANSLATIONS[error.code]\n ?? ['warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.']\n return NextResponse.json(\n { ok: false, error: t(key, fallback), code: error.code },\n { status: error.status },\n )\n }\n throw error\n }\n\n if (replacementTarget) {\n await deletePortalAttachment(context, replacementTarget.attachment)\n }\n await guarded.runAfterSuccess()\n\n return NextResponse.json({\n ok: true,\n item: {\n ...serializeAttachment(attachment),\n entityType: 'attachments:attachment',\n },\n })\n}\n\nexport async function POST(req: Request) {\n return handleUpload(req, false)\n}\n\nexport async function PUT(req: Request) {\n return handleUpload(req, true)\n}\n\nexport async function DELETE(req: Request) {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { translate } = await resolveTranslations()\n const url = new URL(req.url)\n const parsed = deleteQuerySchema.safeParse({\n attachmentId: url.searchParams.get('attachmentId') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const owned = await loadOwnedVisibleAttachment(context, parsed.data.attachmentId)\n if (!owned) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const guarded = await runPortalAttachmentGuard(req, context, owned.claim.id, 'delete', {\n claimId: owned.claim.id,\n attachmentId: owned.attachment.id,\n })\n if (!guarded.ok) return guarded.response\n\n await deletePortalAttachment(context, owned.attachment)\n await guarded.runAfterSuccess()\n return NextResponse.json({ ok: true })\n}\n\nconst assignmentSchema = z.object({\n type: z.string(),\n id: z.string(),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string(),\n url: z.string(),\n downloadUrl: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n mimeType: z.string().nullable(),\n partitionCode: z.string(),\n content: z.string().nullable(),\n thumbnailUrl: z.string(),\n tags: z.array(z.string()),\n assignments: z.array(assignmentSchema),\n createdAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims Portal',\n summary: 'Customer portal warranty claim attachments',\n methods: {\n GET: {\n summary: 'List attachments for an owned claim',\n query: attachmentQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Claim attachments',\n schema: z.object({ items: z.array(attachmentItemSchema) }),\n },\n ],\n },\n POST: {\n summary: 'Upload an attachment for an owned claim',\n requestBody: { contentType: 'multipart/form-data', schema: uploadBodySchema },\n responses: [\n {\n status: 200,\n description: 'Attachment uploaded',\n schema: z.object({ ok: z.boolean(), item: attachmentItemSchema.extend({ entityType: z.string() }) }),\n },\n ],\n },\n PUT: {\n summary: 'Replace an attachment on an owned claim',\n requestBody: { contentType: 'multipart/form-data', schema: replaceBodySchema },\n responses: [\n {\n status: 200,\n description: 'Attachment replaced',\n schema: z.object({ ok: z.boolean(), item: attachmentItemSchema.extend({ entityType: z.string() }) }),\n },\n ],\n },\n DELETE: {\n summary: 'Delete an attachment from an owned claim',\n query: deleteQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Attachment deleted',\n schema: z.object({ ok: z.literal(true) }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { checkRateLimit, rateLimitErrorSchema, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { resolveAttachmentsUploadRateLimitConfig } from '@open-mercato/core/modules/attachments/lib/ocrLimits'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\nimport { StorageDriverFactory } from '@open-mercato/core/modules/attachments/lib/drivers'\nimport { checkAttachmentAccess } from '@open-mercato/core/modules/attachments/lib/access'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '@open-mercato/core/modules/attachments/lib/crud'\nimport { readAttachmentMetadata } from '@open-mercato/core/modules/attachments/lib/metadata'\nimport { clearAttachmentThumbnailCache } from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { removeAttachmentRecord } from '@open-mercato/core/modules/attachments/lib/storageReferences'\nimport {\n isMultipartUploadLimitError,\n parseMultipartFormDataWithinUploadLimit,\n resolveDefaultAttachmentMaxUploadBytes,\n} from '@open-mercato/core/modules/attachments/lib/upload-limits'\nimport {\n isScopedAttachmentUploadError,\n type ScopedAttachmentUploadErrorCode,\n} from '@open-mercato/core/modules/attachments/lib/scoped-upload-service'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { WarrantyClaim } from '../../../data/entities'\nimport { WARRANTY_CLAIM_RESOURCE_KIND } from '../../../commands/shared'\nimport { CUSTOMER_VISIBLE_ATTACHMENT_TAG, isCustomerVisibleAttachment } from '../../../lib/attachmentVisibility'\nimport { loadPortalOwnedClaim } from '../../../lib/portalClaimAccess'\nimport { resolvePortalAttachmentUploadService } from '../../../lib/portalAttachmentUpload'\nimport { resolveLinkedCustomerAuth } from '../../../lib/portalAuthGuard'\n\nconst CLAIM_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim'\nconst logger = createLogger('warranty_claims').child({ route: 'portal-attachments' })\nconst ATTACHMENT_ERROR_TRANSLATIONS: Partial<Record<ScopedAttachmentUploadErrorCode, readonly [string, string]>> = {\n dangerous_executable: ['attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'],\n max_upload_size: ['attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'],\n active_content: ['attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.'],\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n PUT: { requireAuth: false },\n DELETE: { requireAuth: false },\n}\n\nconst attachmentQuerySchema = z.object({\n claimId: z.string().uuid(),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n})\n\nconst uploadBodySchema = z.object({\n claimId: z.string().uuid(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n})\n\nconst replaceBodySchema = uploadBodySchema.extend({\n attachmentId: z.string().uuid(),\n})\n\nconst deleteQuerySchema = z.object({\n attachmentId: z.string().uuid(),\n})\n\ntype PortalContext = {\n auth: CustomerAuthContext\n customerId: string\n tenantId: string\n organizationId: string\n em: EntityManager\n container: Awaited<ReturnType<typeof createRequestContainer>>\n}\n\nfunction toIso(value: Date | string | null | undefined): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date.toISOString()\n}\n\nasync function resolvePortalContext(req: Request): Promise<PortalContext | Response> {\n const authOrResponse = await resolveLinkedCustomerAuth(req)\n if (authOrResponse instanceof Response) return authOrResponse\n const auth = authOrResponse\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return {\n auth,\n customerId: auth.customerEntityId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n em,\n container,\n }\n}\n\nfunction attachmentAuth(context: PortalContext): NonNullable<AuthContext> {\n return {\n sub: context.auth.sub,\n tenantId: context.tenantId,\n orgId: context.organizationId,\n email: context.auth.email,\n }\n}\n\nasync function loadOwnedClaim(context: PortalContext, claimId: string): Promise<WarrantyClaim | null> {\n return loadPortalOwnedClaim(\n context.em,\n {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n customerId: context.customerId,\n },\n claimId,\n )\n}\n\nasync function runPortalAttachmentGuard(\n req: Request,\n context: PortalContext,\n claimId: string,\n operation: 'create' | 'update' | 'delete',\n mutationPayload: Record<string, unknown>,\n): Promise<RouteMutationGuardResult> {\n return runRouteMutationGuards({\n container: context.container,\n req,\n auth: {\n userId: context.auth.sub,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: [],\n },\n input: {\n resourceKind: WARRANTY_CLAIM_RESOURCE_KIND,\n resourceId: claimId,\n operation,\n mutationPayload,\n },\n })\n}\n\nfunction serializeAttachment(attachment: Attachment) {\n const metadata = readAttachmentMetadata(attachment.storageMetadata)\n return {\n id: attachment.id,\n url: attachment.url,\n downloadUrl: `/api/warranty_claims/portal/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,\n fileName: attachment.fileName,\n fileSize: attachment.fileSize,\n mimeType: attachment.mimeType ?? null,\n partitionCode: attachment.partitionCode,\n content: attachment.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(attachment.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(attachment.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n createdAt: toIso(attachment.createdAt),\n }\n}\n\nasync function resolveStorageDriverFactory(context: PortalContext): Promise<StorageDriverFactory> {\n try {\n const resolved = context.container.resolve('storageDriverFactory') as StorageDriverFactory | null\n return resolved ?? new StorageDriverFactory(context.em)\n } catch {\n return new StorageDriverFactory(context.em)\n }\n}\n\nasync function loadOwnedVisibleAttachment(\n context: PortalContext,\n attachmentId: string,\n): Promise<{ attachment: Attachment; claim: WarrantyClaim } | null> {\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const attachment = await findOneWithDecryption(\n context.em,\n Attachment,\n {\n id: attachmentId,\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n {},\n scope,\n )\n if (!attachment) return null\n const claim = await loadOwnedClaim(context, attachment.recordId)\n if (!claim) return null\n if (!isCustomerVisibleAttachment(readAttachmentMetadata(attachment.storageMetadata).tags)) return null\n return { attachment, claim }\n}\n\nasync function deletePortalAttachment(context: PortalContext, attachment: Attachment): Promise<void> {\n const removal = await removeAttachmentRecord(context.em, attachment)\n if (!removal.removed) return\n await clearAttachmentThumbnailCache(attachment.partitionCode, attachment.id).catch((error) => {\n logger.error('Failed to clean portal attachment thumbnails', { err: error, attachmentId: attachment.id })\n })\n if (attachment.storagePath && removal.releaseStorage) {\n try {\n const driver = await (await resolveStorageDriverFactory(context)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? context.tenantId,\n organizationId: attachment.organizationId ?? context.organizationId,\n })\n await driver.delete(attachment.partitionCode, attachment.storagePath)\n } catch (error) {\n logger.error('Failed to remove portal attachment storage', { err: error, attachmentId: attachment.id })\n }\n }\n\n let dataEngine: DataEngine | null = null\n try {\n dataEngine = context.container.resolve<DataEngine>('dataEngine') ?? null\n } catch {\n dataEngine = null\n }\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n}\n\nasync function streamOwnedAttachment(\n context: PortalContext,\n attachmentId: string,\n translate: (key: string, fallback?: string) => string,\n): Promise<Response> {\n const owned = await loadOwnedVisibleAttachment(context, attachmentId)\n if (!owned) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const { attachment } = owned\n const partition = await context.em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.load_failed', 'Failed to load warranty claim data.') }, { status: 500 })\n }\n const access = checkAttachmentAccess(attachmentAuth(context), attachment, partition, { requireAuthForPublic: true })\n if (!access.ok) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const driver = await (await resolveStorageDriverFactory(context)).resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n let buffer: Buffer\n try {\n buffer = (await driver.read(attachment.partitionCode, attachment.storagePath)).buffer\n } catch {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const headers: Record<string, string> = {\n 'Cache-Control': 'private, max-age=60',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n 'Content-Type': 'application/octet-stream',\n 'Content-Disposition': `attachment; filename=\"${slugifyAttachmentFileName(attachment.fileName)}\"`,\n 'X-Content-Type-Options': 'nosniff',\n }\n if (attachment.fileSize > 0) headers['Content-Length'] = String(attachment.fileSize)\n return new NextResponse(new Uint8Array(buffer), { status: 200, headers })\n}\n\nexport async function GET(req: Request) {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { translate } = await resolveTranslations()\n const url = new URL(req.url)\n const attachmentId = url.searchParams.get('attachmentId')\n if (attachmentId) {\n const attachmentIdParsed = z.string().uuid().safeParse(attachmentId)\n if (!attachmentIdParsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n return streamOwnedAttachment(context, attachmentIdParsed.data, translate)\n }\n const parsed = attachmentQuerySchema.safeParse({\n claimId: url.searchParams.get('claimId') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const claim = await loadOwnedClaim(context, parsed.data.claimId)\n if (!claim) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const filter = {\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n recordId: claim.id,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n }\n const page = parsed.data.page ?? 1\n const pageSize = parsed.data.pageSize ?? 100\n const attachments = await findWithDecryption(\n context.em,\n Attachment,\n filter,\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: context.tenantId, organizationId: context.organizationId },\n )\n const partitionCodes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)))\n const partitions = partitionCodes.length\n ? await context.em.find(AttachmentPartition, { code: { $in: partitionCodes } })\n : []\n const partitionsByCode = new Map(partitions.map((partition) => [partition.code, partition]))\n const auth = attachmentAuth(context)\n const visible = attachments.filter((attachment) => {\n const partition = partitionsByCode.get(attachment.partitionCode)\n if (!partition) return false\n if (!isCustomerVisibleAttachment(readAttachmentMetadata(attachment.storageMetadata).tags)) return false\n return checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok\n })\n\n const pagedVisible = visible.slice((page - 1) * pageSize, page * pageSize)\n\n return NextResponse.json({\n items: pagedVisible.map(serializeAttachment),\n total: visible.length,\n page,\n pageSize,\n totalPages: Math.max(1, Math.ceil(visible.length / pageSize)),\n })\n}\n\nfunction uploadTooLargeResponse(t: Awaited<ReturnType<typeof resolveTranslations>>['t']): Response {\n return NextResponse.json({\n ok: false,\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n code: 'max_upload_size',\n }, { status: 413 })\n}\n\nasync function handleUpload(req: Request, replacement: boolean): Promise<Response> {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { t, translate } = await resolveTranslations()\n let rateLimiterService: RateLimiterService | null = null\n try {\n rateLimiterService = context.container.resolve<RateLimiterService>('rateLimiterService') ?? null\n } catch {\n logger.warn('Portal attachment upload rate limiter unavailable; throttle skipped')\n }\n if (rateLimiterService) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n resolveAttachmentsUploadRateLimitConfig(),\n `portal:${context.tenantId}:${context.auth.sub}`,\n t(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) return uploadTooLargeResponse(t)\n throw error\n }\n const parsed = uploadBodySchema.safeParse({\n claimId: form.get('claimId') ?? undefined,\n file: form.get('file') instanceof File ? (form.get('file') as File).name : '',\n })\n const parsedAttachmentId = replacement\n ? z.string().uuid().safeParse(form.get('attachmentId') ?? undefined)\n : null\n if (!parsed.success || (replacement && !parsedAttachmentId?.success)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const claim = await loadOwnedClaim(context, parsed.data.claimId)\n if (!claim) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const file = form.get('file')\n if (!(file instanceof File)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.fileRequired', 'Choose a file before uploading.') }, { status: 400 })\n }\n if (file.size > resolveDefaultAttachmentMaxUploadBytes()) return uploadTooLargeResponse(t)\n const buffer = Buffer.from(await file.arrayBuffer())\n\n const attachmentId = replacement && parsedAttachmentId?.success ? parsedAttachmentId.data : null\n const replacementTarget = attachmentId\n ? await loadOwnedVisibleAttachment(context, attachmentId)\n : null\n if (replacement && (!replacementTarget || replacementTarget.claim.id !== claim.id)) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n\n const guarded = await runPortalAttachmentGuard(req, context, claim.id, replacement ? 'update' : 'create', {\n claimId: claim.id,\n attachmentId,\n fileName: file.name,\n fileSize: file.size,\n mimeType: file.type,\n })\n if (!guarded.ok) {\n return guarded.response\n }\n\n const uploadService = resolvePortalAttachmentUploadService(context.container)\n if (!uploadService) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.') }, { status: 503 })\n }\n\n let attachment: Attachment\n try {\n attachment = await uploadService.upload({\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n entityId: CLAIM_ATTACHMENT_ENTITY_ID,\n recordId: claim.id,\n fileName: file.name,\n buffer,\n declaredMimeType: file.type,\n tags: [CUSTOMER_VISIBLE_ATTACHMENT_TAG],\n })\n } catch (error) {\n if (isScopedAttachmentUploadError(error)) {\n const [key, fallback] = ATTACHMENT_ERROR_TRANSLATIONS[error.code]\n ?? ['warranty_claims.portal.detail.attachmentError', 'Attachment upload failed.']\n return NextResponse.json(\n { ok: false, error: t(key, fallback), code: error.code },\n { status: error.status },\n )\n }\n throw error\n }\n\n if (replacementTarget) {\n await deletePortalAttachment(context, replacementTarget.attachment)\n }\n await guarded.runAfterSuccess()\n\n return NextResponse.json({\n ok: true,\n item: {\n ...serializeAttachment(attachment),\n entityType: 'attachments:attachment',\n },\n })\n}\n\nexport async function POST(req: Request) {\n return handleUpload(req, false)\n}\n\nexport async function PUT(req: Request) {\n return handleUpload(req, true)\n}\n\nexport async function DELETE(req: Request) {\n const contextOrResponse = await resolvePortalContext(req)\n if (contextOrResponse instanceof Response) return contextOrResponse\n const context = contextOrResponse\n const { translate } = await resolveTranslations()\n const url = new URL(req.url)\n const parsed = deleteQuerySchema.safeParse({\n attachmentId: url.searchParams.get('attachmentId') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n const owned = await loadOwnedVisibleAttachment(context, parsed.data.attachmentId)\n if (!owned) {\n return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.notFound', 'Claim not found.') }, { status: 404 })\n }\n const guarded = await runPortalAttachmentGuard(req, context, owned.claim.id, 'delete', {\n claimId: owned.claim.id,\n attachmentId: owned.attachment.id,\n })\n if (!guarded.ok) return guarded.response\n\n await deletePortalAttachment(context, owned.attachment)\n await guarded.runAfterSuccess()\n return NextResponse.json({ ok: true })\n}\n\nconst assignmentSchema = z.object({\n type: z.string(),\n id: z.string(),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string(),\n url: z.string(),\n downloadUrl: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n mimeType: z.string().nullable(),\n partitionCode: z.string(),\n content: z.string().nullable(),\n thumbnailUrl: z.string(),\n tags: z.array(z.string()),\n assignments: z.array(assignmentSchema),\n createdAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims Portal',\n summary: 'Customer portal warranty claim attachments',\n methods: {\n GET: {\n summary: 'List attachments for an owned claim',\n query: attachmentQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Claim attachments',\n schema: z.object({ items: z.array(attachmentItemSchema) }),\n },\n ],\n },\n POST: {\n summary: 'Upload an attachment for an owned claim',\n errors: [{ status: 429, description: 'Attachment upload rate limit exceeded', schema: rateLimitErrorSchema }],\n requestBody: { contentType: 'multipart/form-data', schema: uploadBodySchema },\n responses: [\n {\n status: 200,\n description: 'Attachment uploaded',\n schema: z.object({ ok: z.boolean(), item: attachmentItemSchema.extend({ entityType: z.string() }) }),\n },\n ],\n },\n PUT: {\n summary: 'Replace an attachment on an owned claim',\n errors: [{ status: 429, description: 'Attachment upload rate limit exceeded', schema: rateLimitErrorSchema }],\n requestBody: { contentType: 'multipart/form-data', schema: replaceBodySchema },\n responses: [\n {\n status: 200,\n description: 'Attachment replaced',\n schema: z.object({ ok: z.boolean(), item: attachmentItemSchema.extend({ entityType: z.string() }) }),\n },\n ],\n },\n DELETE: {\n summary: 'Delete an attachment from an owned claim',\n query: deleteQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Attachment deleted',\n schema: z.object({ ok: z.literal(true) }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metadata"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7907.1.28240ddda3",
3
+ "version": "0.10.1-develop.7917.1.c74e12c1d2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.6.6"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7907.1.28240ddda3",
270
- "@open-mercato/shared": "0.10.1-develop.7907.1.28240ddda3",
271
- "@open-mercato/ui": "0.10.1-develop.7907.1.28240ddda3",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7917.1.c74e12c1d2",
270
+ "@open-mercato/shared": "0.10.1-develop.7917.1.c74e12c1d2",
271
+ "@open-mercato/ui": "0.10.1-develop.7917.1.c74e12c1d2",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7907.1.28240ddda3",
277
- "@open-mercato/shared": "0.10.1-develop.7907.1.28240ddda3",
278
- "@open-mercato/ui": "0.10.1-develop.7907.1.28240ddda3",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7917.1.c74e12c1d2",
277
+ "@open-mercato/shared": "0.10.1-develop.7917.1.c74e12c1d2",
278
+ "@open-mercato/ui": "0.10.1-develop.7917.1.c74e12c1d2",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -17,7 +17,8 @@ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
17
17
  *
18
18
  * Neither half exists yet. `apps/mercato/next.config.ts` applies
19
19
  * `frame-ancestors 'self'` plus `X-Frame-Options: SAMEORIGIN` to `/:path*` with
20
- * no `/embed/` carve-out, and `apps/mercato/src/proxy.ts` never calls the
20
+ * no `/embed/` carve-out (`next dev` additionally admits loopback origins so
21
+ * local tools can preview the app), and `apps/mercato/src/proxy.ts` never calls the
21
22
  * embed-policy endpoint. It was absent from the module's original repository
22
23
  * too — that sandbox app declared no `headers()` at all, so `/embed/:slug` was
23
24
  * framable there by default rather than by policy.
@@ -8,6 +8,9 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
8
  import { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'
9
9
  import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
10
10
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
+ import { checkRateLimit, rateLimitErrorSchema, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'
12
+ import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
13
+ import { resolveAttachmentsUploadRateLimitConfig } from '@open-mercato/core/modules/attachments/lib/ocrLimits'
11
14
  import { createLogger } from '@open-mercato/shared/lib/logger'
12
15
  import type { CustomerAuthContext } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
13
16
  import { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'
@@ -360,6 +363,21 @@ async function handleUpload(req: Request, replacement: boolean): Promise<Respons
360
363
  if (contextOrResponse instanceof Response) return contextOrResponse
361
364
  const context = contextOrResponse
362
365
  const { t, translate } = await resolveTranslations()
366
+ let rateLimiterService: RateLimiterService | null = null
367
+ try {
368
+ rateLimiterService = context.container.resolve<RateLimiterService>('rateLimiterService') ?? null
369
+ } catch {
370
+ logger.warn('Portal attachment upload rate limiter unavailable; throttle skipped')
371
+ }
372
+ if (rateLimiterService) {
373
+ const rateLimitResponse = await checkRateLimit(
374
+ rateLimiterService,
375
+ resolveAttachmentsUploadRateLimitConfig(),
376
+ `portal:${context.tenantId}:${context.auth.sub}`,
377
+ t(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),
378
+ )
379
+ if (rateLimitResponse) return rateLimitResponse
380
+ }
363
381
  const contentType = req.headers.get('content-type') || ''
364
382
  if (!contentType.toLowerCase().includes('multipart/form-data')) {
365
383
  return NextResponse.json({ ok: false, error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })
@@ -528,6 +546,7 @@ export const openApi: OpenApiRouteDoc = {
528
546
  },
529
547
  POST: {
530
548
  summary: 'Upload an attachment for an owned claim',
549
+ errors: [{ status: 429, description: 'Attachment upload rate limit exceeded', schema: rateLimitErrorSchema }],
531
550
  requestBody: { contentType: 'multipart/form-data', schema: uploadBodySchema },
532
551
  responses: [
533
552
  {
@@ -539,6 +558,7 @@ export const openApi: OpenApiRouteDoc = {
539
558
  },
540
559
  PUT: {
541
560
  summary: 'Replace an attachment on an owned claim',
561
+ errors: [{ status: 429, description: 'Attachment upload rate limit exceeded', schema: rateLimitErrorSchema }],
542
562
  requestBody: { contentType: 'multipart/form-data', schema: replaceBodySchema },
543
563
  responses: [
544
564
  {