@brandlab-digital/site-next 1.0.13 → 1.2.0

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.
Files changed (42) hide show
  1. package/dist/analytics.d.ts +5 -0
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +7 -3
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/contact/attachments-client.d.ts.map +1 -1
  6. package/dist/contact/attachments-client.js +36 -52
  7. package/dist/contact/attachments-client.js.map +1 -1
  8. package/dist/contact/client.d.ts +0 -2
  9. package/dist/contact/client.d.ts.map +1 -1
  10. package/dist/contact/client.js +1 -16
  11. package/dist/contact/client.js.map +1 -1
  12. package/dist/contact/contract.d.ts.map +1 -1
  13. package/dist/contact/contract.js +2 -2
  14. package/dist/contact/contract.js.map +1 -1
  15. package/dist/contact/image-sanitizer.d.ts +0 -1
  16. package/dist/contact/image-sanitizer.d.ts.map +1 -1
  17. package/dist/contact/image-sanitizer.js +4 -22
  18. package/dist/contact/image-sanitizer.js.map +1 -1
  19. package/dist/contact/index.d.ts +1 -1
  20. package/dist/contact/index.d.ts.map +1 -1
  21. package/dist/contact/index.js +1 -1
  22. package/dist/contact/index.js.map +1 -1
  23. package/dist/contact/pdf-sanitizer.d.ts +5 -0
  24. package/dist/contact/pdf-sanitizer.d.ts.map +1 -0
  25. package/dist/contact/pdf-sanitizer.js +39 -0
  26. package/dist/contact/pdf-sanitizer.js.map +1 -0
  27. package/dist/contact/server.d.ts.map +1 -1
  28. package/dist/contact/server.js +43 -8
  29. package/dist/contact/server.js.map +1 -1
  30. package/dist/instrumentation-client.d.ts +7 -1
  31. package/dist/instrumentation-client.d.ts.map +1 -1
  32. package/dist/instrumentation-client.js +5 -1
  33. package/dist/instrumentation-client.js.map +1 -1
  34. package/dist/ui-client.d.ts +1 -1
  35. package/dist/ui-client.d.ts.map +1 -1
  36. package/dist/ui-client.js +1 -1
  37. package/dist/ui-client.js.map +1 -1
  38. package/package.json +4 -10
  39. package/dist/image-runtime.d.ts +0 -2
  40. package/dist/image-runtime.d.ts.map +0 -1
  41. package/dist/image-runtime.js +0 -22
  42. package/dist/image-runtime.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/contact/contract.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_CONTACT_ATTACHMENT_POLICY,\n defineContactContract,\n isValidAustralianPhoneNumber,\n normalizeAustralianPhoneDigits,\n type BrandlabContactContract,\n type ContactAttachmentPolicy,\n type ContactFieldNormalization,\n} from '@brandlab-digital/site-core/contact'\nimport {\n defineBrandlabContact,\n type BrandlabContactDefinition,\n type ContactFieldKind,\n} from './shared.js'\n\nexport type ContactRuntimeDefinitionOptions = Pick<\n BrandlabContactDefinition,\n 'brandName' | 'siteUrl' | 'deploymentMode' | 'bot'\n> & {\n labels: Readonly<Record<string, string>>\n attachment?: Partial<ContactAttachmentPolicy> & { fieldPrefix?: string }\n email?: NonNullable<BrandlabContactDefinition['email']>\n}\n\nconst KIND_MAP: Record<BrandlabContactContract['fields'][number]['kind'], ContactFieldKind> = {\n text: 'text',\n email: 'email',\n tel: 'phone',\n textarea: 'textarea',\n select: 'select',\n checkbox: 'checkbox',\n}\n\nfunction normalizeAustralianContactPhone(value: string) {\n if (!/^[+()\\d\\s.-]+$/.test(value)) {\n throw new Error('The phone value contains unsupported characters.')\n }\n return normalizeAustralianPhoneDigits(value)\n}\n\nconst NORMALIZERS: Record<ContactFieldNormalization, (value: string) => string> = {\n lowercase: (value) => value.toLowerCase(),\n 'australian-phone-digits': normalizeAustralianContactPhone,\n}\n\nexport function createContactDefinitionFromContract(\n contract: BrandlabContactContract,\n options: ContactRuntimeDefinitionOptions,\n) {\n defineContactContract(contract)\n const { labels, attachment, email, ...runtime } = options\n const fields = contract.fields.map((field) => {\n const label = labels[field.key]?.trim()\n if (!label) throw new Error(`A customer-facing label is required for contact field \"${field.key}\".`)\n return {\n name: field.key,\n label,\n role: field.role,\n kind: KIND_MAP[field.kind],\n required: field.required,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n ...(field.kind === 'checkbox'\n ? { options: [field.checkedValue!] }\n : field.options\n ? { options: field.options }\n : {}),\n ...(field.normalization\n ? { normalize: NORMALIZERS[field.normalization] }\n : {}),\n ...(field.phoneRule === 'australian'\n ? {\n validate: (value: string) =>\n isValidAustralianPhoneNumber(value)\n ? undefined\n : 'Enter a valid Australian phone number.',\n }\n : {}),\n }\n })\n const attachments =\n contract.attachments.mode === 'none'\n ? false\n : {\n ...DEFAULT_CONTACT_ATTACHMENT_POLICY,\n ...attachment,\n required: contract.attachments.mode === 'images-required',\n }\n\n return defineBrandlabContact({\n ...runtime,\n projectId: contract.projectId,\n fields,\n attachments,\n ...(email ? { email } : {}),\n })\n}\n"]}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/contact/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iCAAiC,EACjC,kCAAkC,EAClC,qBAAqB,EACrB,4BAA4B,EAC5B,8BAA8B,GAI/B,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EACL,qBAAqB,GAGtB,MAAM,aAAa,CAAA;AAWpB,MAAM,QAAQ,GAAgF;IAC5F,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,OAAO;IACZ,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,UAAU;CACrB,CAAA;AAED,SAAS,+BAA+B,CAAC,KAAa;IACpD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,8BAA8B,CAAC,KAAK,CAAC,CAAA;AAC9C,CAAC;AAED,MAAM,WAAW,GAAiE;IAChF,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE;IACzC,yBAAyB,EAAE,+BAA+B;CAC3D,CAAA;AAED,MAAM,UAAU,mCAAmC,CACjD,QAAiC,EACjC,OAAwC;IAExC,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IAC/B,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,CAAA;IACzD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAA;QACvC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,KAAK,CAAC,GAAG,IAAI,CAAC,CAAA;QACpG,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,GAAG;YACf,KAAK;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU;gBAC3B,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,YAAa,CAAC,EAAE;gBACpC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACb,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;oBAC5B,CAAC,CAAC,EAAE,CAAC;YACT,GAAG,CAAC,KAAK,CAAC,aAAa;gBACrB,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE;gBACjD,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,YAAY;gBAClC,CAAC,CAAC;oBACE,QAAQ,EAAE,CAAC,KAAa,EAAE,EAAE,CAC1B,4BAA4B,CAAC,KAAK,CAAC;wBACjC,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,wCAAwC;iBAC/C;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAA;IACH,CAAC,CAAC,CAAA;IACF,MAAM,WAAW,GACf,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,MAAM;QAClC,CAAC,CAAC,KAAK;QACP,CAAC,CAAC;YACE,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,0BAA0B,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,iCAAiC,CAAC;YACtI,GAAG,UAAU;YACb,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,iBAAiB;SAC1D,CAAA;IAEP,OAAO,qBAAqB,CAAC;QAC3B,GAAG,OAAO;QACV,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,MAAM;QACN,WAAW;QACX,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5B,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import {\n DEFAULT_CONTACT_ATTACHMENT_POLICY,\n DOCUMENT_CONTACT_ATTACHMENT_POLICY,\n defineContactContract,\n isValidAustralianPhoneNumber,\n normalizeAustralianPhoneDigits,\n type BrandlabContactContract,\n type ContactAttachmentPolicy,\n type ContactFieldNormalization,\n} from '@brandlab-digital/site-core/contact'\nimport {\n defineBrandlabContact,\n type BrandlabContactDefinition,\n type ContactFieldKind,\n} from './shared.js'\n\nexport type ContactRuntimeDefinitionOptions = Pick<\n BrandlabContactDefinition,\n 'brandName' | 'siteUrl' | 'deploymentMode' | 'bot'\n> & {\n labels: Readonly<Record<string, string>>\n attachment?: Partial<ContactAttachmentPolicy> & { fieldPrefix?: string }\n email?: NonNullable<BrandlabContactDefinition['email']>\n}\n\nconst KIND_MAP: Record<BrandlabContactContract['fields'][number]['kind'], ContactFieldKind> = {\n text: 'text',\n email: 'email',\n tel: 'phone',\n textarea: 'textarea',\n select: 'select',\n checkbox: 'checkbox',\n}\n\nfunction normalizeAustralianContactPhone(value: string) {\n if (!/^[+()\\d\\s.-]+$/.test(value)) {\n throw new Error('The phone value contains unsupported characters.')\n }\n return normalizeAustralianPhoneDigits(value)\n}\n\nconst NORMALIZERS: Record<ContactFieldNormalization, (value: string) => string> = {\n lowercase: (value) => value.toLowerCase(),\n 'australian-phone-digits': normalizeAustralianContactPhone,\n}\n\nexport function createContactDefinitionFromContract(\n contract: BrandlabContactContract,\n options: ContactRuntimeDefinitionOptions,\n) {\n defineContactContract(contract)\n const { labels, attachment, email, ...runtime } = options\n const fields = contract.fields.map((field) => {\n const label = labels[field.key]?.trim()\n if (!label) throw new Error(`A customer-facing label is required for contact field \"${field.key}\".`)\n return {\n name: field.key,\n label,\n role: field.role,\n kind: KIND_MAP[field.kind],\n required: field.required,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n ...(field.kind === 'checkbox'\n ? { options: [field.checkedValue!] }\n : field.options\n ? { options: field.options }\n : {}),\n ...(field.normalization\n ? { normalize: NORMALIZERS[field.normalization] }\n : {}),\n ...(field.phoneRule === 'australian'\n ? {\n validate: (value: string) =>\n isValidAustralianPhoneNumber(value)\n ? undefined\n : 'Enter a valid Australian phone number.',\n }\n : {}),\n }\n })\n const attachments =\n contract.attachments.mode === 'none'\n ? false\n : {\n ...(contract.attachments.mode === 'images-and-pdfs-optional' ? DOCUMENT_CONTACT_ATTACHMENT_POLICY : DEFAULT_CONTACT_ATTACHMENT_POLICY),\n ...attachment,\n required: contract.attachments.mode === 'images-required',\n }\n\n return defineBrandlabContact({\n ...runtime,\n projectId: contract.projectId,\n fields,\n attachments,\n ...(email ? { email } : {}),\n })\n}\n"]}
@@ -1,5 +1,4 @@
1
1
  import 'server-only';
2
- import '../image-runtime.js';
3
2
  import type { ContactAttachmentProcessor } from './shared.js';
4
3
  /**
5
4
  * The production image boundary for Contact Contract v4. Every accepted image
@@ -1 +1 @@
1
- {"version":3,"file":"image-sanitizer.d.ts","sourceRoot":"","sources":["../../src/contact/image-sanitizer.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,qBAAqB,CAAA;AAI5B,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,aAAa,CAAA;AA2GpB;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,0BAA0B,CA0BhF"}
1
+ {"version":3,"file":"image-sanitizer.d.ts","sourceRoot":"","sources":["../../src/contact/image-sanitizer.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,aAAa,CAAA;AAqGpB;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,0BAA0B,CAiBhF"}
@@ -1,8 +1,6 @@
1
1
  import 'server-only';
2
- import '../image-runtime.js';
2
+ import { sanitizeContactPdf } from './pdf-sanitizer.js';
3
3
  import sharp, {} from 'sharp';
4
- import { detectContactTransportAttachmentType } from '@brandlab-digital/site-core/contact';
5
- sharp.block({ operation: ['VipsForeignLoadHeif'] });
6
4
  const MAX_INPUT_PIXELS = 50_000_000;
7
5
  const MAX_OUTPUT_DIMENSION = 4_096;
8
6
  const PROCESSING_TIMEOUT_SECONDS = 5;
@@ -12,6 +10,8 @@ const INPUT_FORMAT_BY_MIME = Object.freeze({
12
10
  'image/png': 'png',
13
11
  'image/gif': 'gif',
14
12
  'image/webp': 'webp',
13
+ 'image/heic': 'heif',
14
+ 'image/heif': 'heif',
15
15
  });
16
16
  function expectedInputFormat(contentType) {
17
17
  return INPUT_FORMAT_BY_MIME[contentType.trim().toLowerCase()];
@@ -53,12 +53,6 @@ async function sanitizeOne(attachment) {
53
53
  }
54
54
  // Buffer.from(Uint8Array) copies untrusted request bytes before native work.
55
55
  const input = Buffer.from(attachment.content);
56
- const detected = detectContactTransportAttachmentType(input);
57
- if (!detected || expectedInputFormat(detected) !== expectedFormat) {
58
- throw new Error('The uploaded bytes do not match a supported image type.');
59
- }
60
- // Independent of Next's lazy optimizer initialization. Never unblock a loader.
61
- sharp.block({ operation: ['VipsForeignLoadHeif'] });
62
56
  const image = sharp(input, {
63
57
  animated: false,
64
58
  failOn: 'warning',
@@ -100,22 +94,10 @@ export function createBrandlabContactImageSanitizer() {
100
94
  if (!Array.isArray(attachments))
101
95
  return { ok: false };
102
96
  try {
103
- // Match the existing absolute request envelope; per-form limits are
104
- // enforced by the server before and after processing.
105
- if (attachments.length > 20)
106
- return { ok: false };
107
- let total = 0;
108
- for (const attachment of attachments) {
109
- if (!(attachment?.content instanceof Uint8Array))
110
- return { ok: false };
111
- total += attachment.content.byteLength;
112
- if (attachment.content.byteLength < 1 || attachment.content.byteLength > 4_500_000 || total > 4_500_000)
113
- return { ok: false };
114
- }
115
97
  const sanitized = [];
116
98
  // Sequential processing bounds peak native memory for multiple photos.
117
99
  for (const attachment of attachments) {
118
- sanitized.push(await sanitizeOne(attachment));
100
+ sanitized.push(attachment.contentType === 'application/pdf' ? await sanitizeContactPdf(attachment) : await sanitizeOne(attachment));
119
101
  }
120
102
  return { ok: true, attachments: Object.freeze(sanitized) };
121
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image-sanitizer.js","sourceRoot":"","sources":["../../src/contact/image-sanitizer.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport '../image-runtime.js'\n\nimport sharp, { type Metadata, type Sharp } from 'sharp'\nimport { detectContactTransportAttachmentType } from '@brandlab-digital/site-core/contact'\nimport type {\n ContactAttachmentProcessor,\n ContactDeliveryAttachment,\n} from './shared.js'\n\nsharp.block({ operation: ['VipsForeignLoadHeif'] })\n\nconst MAX_INPUT_PIXELS = 50_000_000\nconst MAX_OUTPUT_DIMENSION = 4_096\nconst PROCESSING_TIMEOUT_SECONDS = 5\n\nconst INPUT_FORMAT_BY_MIME = Object.freeze({\n 'image/jpeg': 'jpeg',\n 'image/jpg': 'jpeg',\n 'image/png': 'png',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n} as const)\n\nfunction expectedInputFormat(contentType: string) {\n return INPUT_FORMAT_BY_MIME[\n contentType.trim().toLowerCase() as keyof typeof INPUT_FORMAT_BY_MIME\n ]\n}\n\nfunction assertSingleImage(metadata: Metadata, expectedFormat: string) {\n if (\n metadata.format !== expectedFormat ||\n !Number.isSafeInteger(metadata.width) ||\n !Number.isSafeInteger(metadata.height) ||\n !metadata.width ||\n !metadata.height ||\n (metadata.pages ?? 1) !== 1\n ) {\n throw new Error('The uploaded file is not one supported still image.')\n }\n}\n\nfunction outputFilename(filename: string) {\n const stem = filename.replace(/\\.[a-z0-9]+$/i, '')\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(stem)) {\n throw new Error('The validated attachment filename is invalid.')\n }\n return `${stem}.jpg`\n}\n\nfunction outputPipeline(image: Sharp) {\n return image\n .autoOrient()\n .resize({\n width: MAX_OUTPUT_DIMENSION,\n height: MAX_OUTPUT_DIMENSION,\n fit: 'inside',\n withoutEnlargement: true,\n })\n .flatten({ background: '#ffffff' })\n .timeout({ seconds: PROCESSING_TIMEOUT_SECONDS })\n .jpeg({ quality: 85, progressive: true, mozjpeg: true })\n}\n\nasync function sanitizeOne(\n attachment: ContactDeliveryAttachment,\n): Promise<ContactDeliveryAttachment> {\n const expectedFormat = expectedInputFormat(attachment.contentType)\n if (!expectedFormat || !(attachment.content instanceof Uint8Array)) {\n throw new Error('The uploaded file type is unsupported.')\n }\n\n // Buffer.from(Uint8Array) copies untrusted request bytes before native work.\n const input = Buffer.from(attachment.content)\n const detected = detectContactTransportAttachmentType(input)\n if (!detected || expectedInputFormat(detected) !== expectedFormat) {\n throw new Error('The uploaded bytes do not match a supported image type.')\n }\n // Independent of Next's lazy optimizer initialization. Never unblock a loader.\n sharp.block({ operation: ['VipsForeignLoadHeif'] })\n const image = sharp(input, {\n animated: false,\n failOn: 'warning',\n limitInputPixels: MAX_INPUT_PIXELS,\n pages: 1,\n sequentialRead: true,\n unlimited: false,\n })\n const metadata = await image.metadata()\n assertSingleImage(metadata, expectedFormat)\n\n const { data, info } = await outputPipeline(image).toBuffer({\n resolveWithObject: true,\n })\n if (\n data.byteLength < 1 ||\n info.format !== 'jpeg' ||\n !Number.isSafeInteger(info.width) ||\n !Number.isSafeInteger(info.height) ||\n info.width < 1 ||\n info.height < 1 ||\n info.width > MAX_OUTPUT_DIMENSION ||\n info.height > MAX_OUTPUT_DIMENSION\n ) {\n throw new Error('The uploaded image could not be normalised safely.')\n }\n\n return Object.freeze({\n filename: outputFilename(attachment.filename),\n content: new Uint8Array(data),\n contentType: 'image/jpeg',\n })\n}\n\n/**\n * The production image boundary for Contact Contract v4. Every accepted image\n * is fully decoded, bounded, auto-oriented and re-encoded without source\n * metadata. Project code cannot replace this processor in production.\n */\nexport function createBrandlabContactImageSanitizer(): ContactAttachmentProcessor {\n return Object.freeze({\n capability: 'sanitizes-contact-images-v1' as const,\n async process({ attachments }: Parameters<ContactAttachmentProcessor['process']>[0]) {\n if (!Array.isArray(attachments)) return { ok: false as const }\n try {\n // Match the existing absolute request envelope; per-form limits are\n // enforced by the server before and after processing.\n if (attachments.length > 20) return { ok: false as const }\n let total = 0\n for (const attachment of attachments) {\n if (!(attachment?.content instanceof Uint8Array)) return { ok: false as const }\n total += attachment.content.byteLength\n if (attachment.content.byteLength < 1 || attachment.content.byteLength > 4_500_000 || total > 4_500_000) return { ok: false as const }\n }\n const sanitized: ContactDeliveryAttachment[] = []\n // Sequential processing bounds peak native memory for multiple photos.\n for (const attachment of attachments) {\n sanitized.push(await sanitizeOne(attachment))\n }\n return { ok: true as const, attachments: Object.freeze(sanitized) }\n } catch {\n return { ok: false as const }\n }\n },\n })\n}\n"]}
1
+ {"version":3,"file":"image-sanitizer.js","sourceRoot":"","sources":["../../src/contact/image-sanitizer.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,KAAK,EAAE,EAA6B,MAAM,OAAO,CAAA;AAMxD,MAAM,gBAAgB,GAAG,UAAU,CAAA;AACnC,MAAM,oBAAoB,GAAG,KAAK,CAAA;AAClC,MAAM,0BAA0B,GAAG,CAAC,CAAA;AAEpC,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC;IACzC,YAAY,EAAE,MAAM;IACpB,WAAW,EAAE,MAAM;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;CACZ,CAAC,CAAA;AAEX,SAAS,mBAAmB,CAAC,WAAmB;IAC9C,OAAO,oBAAoB,CACzB,WAAW,CAAC,IAAI,EAAE,CAAC,WAAW,EAAuC,CACtE,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAkB,EAAE,cAAsB;IACnE,IACE,QAAQ,CAAC,MAAM,KAAK,cAAc;QAClC,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC;QACrC,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtC,CAAC,QAAQ,CAAC,KAAK;QACf,CAAC,QAAQ,CAAC,MAAM;QAChB,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,EAC3B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IACxE,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;IAClD,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IACD,OAAO,GAAG,IAAI,MAAM,CAAA;AACtB,CAAC;AAED,SAAS,cAAc,CAAC,KAAY;IAClC,OAAO,KAAK;SACT,UAAU,EAAE;SACZ,MAAM,CAAC;QACN,KAAK,EAAE,oBAAoB;QAC3B,MAAM,EAAE,oBAAoB;QAC5B,GAAG,EAAE,QAAQ;QACb,kBAAkB,EAAE,IAAI;KACzB,CAAC;SACD,OAAO,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;SAClC,OAAO,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;SAChD,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;AAC5D,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,UAAqC;IAErC,MAAM,cAAc,GAAG,mBAAmB,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IAClE,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,YAAY,UAAU,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;IAC3D,CAAC;IAED,6EAA6E;IAC7E,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,SAAS;QACjB,gBAAgB,EAAE,gBAAgB;QAClC,KAAK,EAAE,CAAC;QACR,cAAc,EAAE,IAAI;QACpB,SAAS,EAAE,KAAK;KACjB,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAA;IACvC,iBAAiB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAA;IAE3C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC;QAC1D,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAA;IACF,IACE,IAAI,CAAC,UAAU,GAAG,CAAC;QACnB,IAAI,CAAC,MAAM,KAAK,MAAM;QACtB,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACjC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC;QAClC,IAAI,CAAC,KAAK,GAAG,CAAC;QACd,IAAI,CAAC,MAAM,GAAG,CAAC;QACf,IAAI,CAAC,KAAK,GAAG,oBAAoB;QACjC,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAClC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;IACvE,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,QAAQ,CAAC;QAC7C,OAAO,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC;QAC7B,WAAW,EAAE,YAAY;KAC1B,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mCAAmC;IACjD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,UAAU,EAAE,6BAAsC;QAClD,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,EAAwD;YACjF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,CAAA;YAC9D,IAAI,CAAC;gBACH,MAAM,SAAS,GAAgC,EAAE,CAAA;gBACjD,uEAAuE;gBACvE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;oBACrC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,KAAK,iBAAiB,CAAC,CAAC,CAAC,MAAM,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,UAAU,CAAC,CAAC,CAAA;gBACrI,CAAC;gBACD,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAA;YACrE,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,CAAA;YAC/B,CAAC;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import 'server-only'\n\nimport { sanitizeContactPdf } from './pdf-sanitizer.js'\nimport sharp, { type Metadata, type Sharp } from 'sharp'\nimport type {\n ContactAttachmentProcessor,\n ContactDeliveryAttachment,\n} from './shared.js'\n\nconst MAX_INPUT_PIXELS = 50_000_000\nconst MAX_OUTPUT_DIMENSION = 4_096\nconst PROCESSING_TIMEOUT_SECONDS = 5\n\nconst INPUT_FORMAT_BY_MIME = Object.freeze({\n 'image/jpeg': 'jpeg',\n 'image/jpg': 'jpeg',\n 'image/png': 'png',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/heic': 'heif',\n 'image/heif': 'heif',\n} as const)\n\nfunction expectedInputFormat(contentType: string) {\n return INPUT_FORMAT_BY_MIME[\n contentType.trim().toLowerCase() as keyof typeof INPUT_FORMAT_BY_MIME\n ]\n}\n\nfunction assertSingleImage(metadata: Metadata, expectedFormat: string) {\n if (\n metadata.format !== expectedFormat ||\n !Number.isSafeInteger(metadata.width) ||\n !Number.isSafeInteger(metadata.height) ||\n !metadata.width ||\n !metadata.height ||\n (metadata.pages ?? 1) !== 1\n ) {\n throw new Error('The uploaded file is not one supported still image.')\n }\n}\n\nfunction outputFilename(filename: string) {\n const stem = filename.replace(/\\.[a-z0-9]+$/i, '')\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(stem)) {\n throw new Error('The validated attachment filename is invalid.')\n }\n return `${stem}.jpg`\n}\n\nfunction outputPipeline(image: Sharp) {\n return image\n .autoOrient()\n .resize({\n width: MAX_OUTPUT_DIMENSION,\n height: MAX_OUTPUT_DIMENSION,\n fit: 'inside',\n withoutEnlargement: true,\n })\n .flatten({ background: '#ffffff' })\n .timeout({ seconds: PROCESSING_TIMEOUT_SECONDS })\n .jpeg({ quality: 85, progressive: true, mozjpeg: true })\n}\n\nasync function sanitizeOne(\n attachment: ContactDeliveryAttachment,\n): Promise<ContactDeliveryAttachment> {\n const expectedFormat = expectedInputFormat(attachment.contentType)\n if (!expectedFormat || !(attachment.content instanceof Uint8Array)) {\n throw new Error('The uploaded file type is unsupported.')\n }\n\n // Buffer.from(Uint8Array) copies untrusted request bytes before native work.\n const input = Buffer.from(attachment.content)\n const image = sharp(input, {\n animated: false,\n failOn: 'warning',\n limitInputPixels: MAX_INPUT_PIXELS,\n pages: 1,\n sequentialRead: true,\n unlimited: false,\n })\n const metadata = await image.metadata()\n assertSingleImage(metadata, expectedFormat)\n\n const { data, info } = await outputPipeline(image).toBuffer({\n resolveWithObject: true,\n })\n if (\n data.byteLength < 1 ||\n info.format !== 'jpeg' ||\n !Number.isSafeInteger(info.width) ||\n !Number.isSafeInteger(info.height) ||\n info.width < 1 ||\n info.height < 1 ||\n info.width > MAX_OUTPUT_DIMENSION ||\n info.height > MAX_OUTPUT_DIMENSION\n ) {\n throw new Error('The uploaded image could not be normalised safely.')\n }\n\n return Object.freeze({\n filename: outputFilename(attachment.filename),\n content: new Uint8Array(data),\n contentType: 'image/jpeg',\n })\n}\n\n/**\n * The production image boundary for Contact Contract v4. Every accepted image\n * is fully decoded, bounded, auto-oriented and re-encoded without source\n * metadata. Project code cannot replace this processor in production.\n */\nexport function createBrandlabContactImageSanitizer(): ContactAttachmentProcessor {\n return Object.freeze({\n capability: 'sanitizes-contact-images-v1' as const,\n async process({ attachments }: Parameters<ContactAttachmentProcessor['process']>[0]) {\n if (!Array.isArray(attachments)) return { ok: false as const }\n try {\n const sanitized: ContactDeliveryAttachment[] = []\n // Sequential processing bounds peak native memory for multiple photos.\n for (const attachment of attachments) {\n sanitized.push(attachment.contentType === 'application/pdf' ? await sanitizeContactPdf(attachment) : await sanitizeOne(attachment))\n }\n return { ok: true as const, attachments: Object.freeze(sanitized) }\n } catch {\n return { ok: false as const }\n }\n },\n })\n}\n"]}
@@ -1,4 +1,4 @@
1
1
  export * from './shared.js';
2
2
  export * from './contract.js';
3
- export { createContactHttpSubmissionController, normalizeContactUploadFile, normalizeContactConvertedUploadFile, parseContactActionResult, shouldTrackContactResult, trackDeliveredContactResult, type ContactHttpSubmissionControllerOptions, type ContactSubmissionController, type ContactSubmissionPhase, type ContactSubmissionState, } from './client.js';
3
+ export { createContactHttpSubmissionController, normalizeContactUploadFile, parseContactActionResult, shouldTrackContactResult, trackDeliveredContactResult, type ContactHttpSubmissionControllerOptions, type ContactSubmissionController, type ContactSubmissionPhase, type ContactSubmissionState, } from './client.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contact/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,OAAO,EACL,qCAAqC,EACrC,0BAA0B,EAC1B,mCAAmC,EACnC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,KAAK,sCAAsC,EAC3C,KAAK,2BAA2B,EAChC,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contact/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,OAAO,EACL,qCAAqC,EACrC,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,KAAK,sCAAsC,EAC3C,KAAK,2BAA2B,EAChC,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA"}
@@ -1,4 +1,4 @@
1
1
  export * from './shared.js';
2
2
  export * from './contract.js';
3
- export { createContactHttpSubmissionController, normalizeContactUploadFile, normalizeContactConvertedUploadFile, parseContactActionResult, shouldTrackContactResult, trackDeliveredContactResult, } from './client.js';
3
+ export { createContactHttpSubmissionController, normalizeContactUploadFile, parseContactActionResult, shouldTrackContactResult, trackDeliveredContactResult, } from './client.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contact/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,OAAO,EACL,qCAAqC,EACrC,0BAA0B,EAC1B,mCAAmC,EACnC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,GAK5B,MAAM,aAAa,CAAA","sourcesContent":["export * from './shared.js'\nexport * from './contract.js'\nexport {\n createContactHttpSubmissionController,\n normalizeContactUploadFile,\n normalizeContactConvertedUploadFile,\n parseContactActionResult,\n shouldTrackContactResult,\n trackDeliveredContactResult,\n type ContactHttpSubmissionControllerOptions,\n type ContactSubmissionController,\n type ContactSubmissionPhase,\n type ContactSubmissionState,\n} from './client.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/contact/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,OAAO,EACL,qCAAqC,EACrC,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,GAK5B,MAAM,aAAa,CAAA","sourcesContent":["export * from './shared.js'\nexport * from './contract.js'\nexport {\n createContactHttpSubmissionController,\n normalizeContactUploadFile,\n parseContactActionResult,\n shouldTrackContactResult,\n trackDeliveredContactResult,\n type ContactHttpSubmissionControllerOptions,\n type ContactSubmissionController,\n type ContactSubmissionPhase,\n type ContactSubmissionState,\n} from './client.js'\n"]}
@@ -0,0 +1,5 @@
1
+ import type { ContactDeliveryAttachment } from './shared.js';
2
+ /** Accept bounded, static plans only. Rebuild page graphics without annotations,
3
+ * document actions, attachments, forms, links or source metadata. */
4
+ export declare function sanitizeContactPdf(attachment: ContactDeliveryAttachment): Promise<ContactDeliveryAttachment>;
5
+ //# sourceMappingURL=pdf-sanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-sanitizer.d.ts","sourceRoot":"","sources":["../../src/contact/pdf-sanitizer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAG5D;qEACqE;AACrE,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,yBAAyB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAwBlH"}
@@ -0,0 +1,39 @@
1
+ import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib';
2
+ const REJECTED_KEYS = new Set(['JS', 'JavaScript', 'AA', 'OpenAction', 'Launch', 'EmbeddedFiles', 'EmbeddedFile', 'XFA', 'RichMedia', 'SubmitForm', 'ImportData', 'GoToR', 'GoToE', 'AcroForm', 'AF']);
3
+ /** Accept bounded, static plans only. Rebuild page graphics without annotations,
4
+ * document actions, attachments, forms, links or source metadata. */
5
+ export async function sanitizeContactPdf(attachment) {
6
+ if (!(attachment.content instanceof Uint8Array) || attachment.content.byteLength > 4_000_000 || !/^document-\d+\.pdf$/.test(attachment.filename))
7
+ throw new Error('Invalid document attachment.');
8
+ const input = await PDFDocument.load(attachment.content, { ignoreEncryption: false, updateMetadata: false, throwOnInvalidObject: true });
9
+ if (input.isEncrypted || input.getPageCount() < 1 || input.getPageCount() > 100)
10
+ throw new Error('Unsupported PDF document.');
11
+ for (const [, object] of input.context.enumerateIndirectObjects()) {
12
+ if (object instanceof PDFDict) {
13
+ for (const [key, value] of object.entries()) {
14
+ if (key.decodeText() === 'Annots') {
15
+ const annotations = input.context.lookup(value);
16
+ if (!(annotations instanceof PDFArray) || annotations.size() > 0)
17
+ throw new Error('Annotated PDF documents must be flattened before upload.');
18
+ }
19
+ if (REJECTED_KEYS.has(key.decodeText()) || (value instanceof PDFName && REJECTED_KEYS.has(value.decodeText())))
20
+ throw new Error('Interactive PDF documents are not supported.');
21
+ }
22
+ }
23
+ }
24
+ const output = await PDFDocument.create({ updateMetadata: false });
25
+ for (const page of input.getPages()) {
26
+ const { width, height } = page.getSize();
27
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > 20_000 || height > 20_000)
28
+ throw new Error('Unsupported PDF page dimensions.');
29
+ const embedded = await output.embedPage(page);
30
+ const copied = output.addPage([width, height]);
31
+ copied.drawPage(embedded);
32
+ copied.setRotation(page.getRotation());
33
+ }
34
+ const content = await output.save({ useObjectStreams: true, addDefaultPage: false });
35
+ if (!content.byteLength || content.byteLength > 4_000_000)
36
+ throw new Error('PDF output exceeds the attachment limit.');
37
+ return Object.freeze({ filename: attachment.filename, content, contentType: 'application/pdf' });
38
+ }
39
+ //# sourceMappingURL=pdf-sanitizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf-sanitizer.js","sourceRoot":"","sources":["../../src/contact/pdf-sanitizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGjE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAC,YAAY,EAAC,IAAI,EAAC,YAAY,EAAC,QAAQ,EAAC,eAAe,EAAC,cAAc,EAAC,KAAK,EAAC,WAAW,EAAC,YAAY,EAAC,YAAY,EAAC,OAAO,EAAC,OAAO,EAAC,UAAU,EAAC,IAAI,CAAC,CAAC,CAAA;AACxL;qEACqE;AACrE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAqC;IAC5E,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,YAAY,UAAU,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,GAAG,SAAS,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;IACjM,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAA;IACxI,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,YAAY,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,EAAE,GAAG,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;IAC7H,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC;QAClE,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC5C,IAAI,GAAG,CAAC,UAAU,EAAE,KAAK,QAAQ,EAAE,CAAC;oBAAC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBAAC,IAAI,CAAC,CAAC,WAAW,YAAY,QAAQ,CAAC,IAAI,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC;wBAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;gBAAC,CAAC;gBACrO,IAAI,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,KAAK,YAAY,OAAO,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;YACjL,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAA;IAClE,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpC,MAAM,EAAC,KAAK,EAAC,MAAM,EAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAA;QACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,KAAK,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;QAC9K,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAC,MAAM,CAAC,CAAC,CAAA;QAC7C,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACzB,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACxC,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAA;IACpF,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IACtH,OAAO,MAAM,CAAC,MAAM,CAAC,EAAC,QAAQ,EAAC,UAAU,CAAC,QAAQ,EAAC,OAAO,EAAC,WAAW,EAAC,iBAAiB,EAAC,CAAC,CAAA;AAC5F,CAAC","sourcesContent":["import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib'\nimport type { ContactDeliveryAttachment } from './shared.js'\n\nconst REJECTED_KEYS = new Set(['JS','JavaScript','AA','OpenAction','Launch','EmbeddedFiles','EmbeddedFile','XFA','RichMedia','SubmitForm','ImportData','GoToR','GoToE','AcroForm','AF'])\n/** Accept bounded, static plans only. Rebuild page graphics without annotations,\n * document actions, attachments, forms, links or source metadata. */\nexport async function sanitizeContactPdf(attachment: ContactDeliveryAttachment): Promise<ContactDeliveryAttachment> {\n if (!(attachment.content instanceof Uint8Array) || attachment.content.byteLength > 4_000_000 || !/^document-\\d+\\.pdf$/.test(attachment.filename)) throw new Error('Invalid document attachment.')\n const input = await PDFDocument.load(attachment.content, { ignoreEncryption: false, updateMetadata: false, throwOnInvalidObject: true })\n if (input.isEncrypted || input.getPageCount() < 1 || input.getPageCount() > 100) throw new Error('Unsupported PDF document.')\n for (const [, object] of input.context.enumerateIndirectObjects()) {\n if (object instanceof PDFDict) {\n for (const [key, value] of object.entries()) {\n if (key.decodeText() === 'Annots') { const annotations = input.context.lookup(value); if (!(annotations instanceof PDFArray) || annotations.size() > 0) throw new Error('Annotated PDF documents must be flattened before upload.') }\n if (REJECTED_KEYS.has(key.decodeText()) || (value instanceof PDFName && REJECTED_KEYS.has(value.decodeText()))) throw new Error('Interactive PDF documents are not supported.')\n }\n }\n }\n const output = await PDFDocument.create({ updateMetadata: false })\n for (const page of input.getPages()) {\n const {width,height} = page.getSize()\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > 20_000 || height > 20_000) throw new Error('Unsupported PDF page dimensions.')\n const embedded = await output.embedPage(page)\n const copied = output.addPage([width,height])\n copied.drawPage(embedded)\n copied.setRotation(page.getRotation())\n }\n const content = await output.save({ useObjectStreams: true, addDefaultPage: false })\n if (!content.byteLength || content.byteLength > 4_000_000) throw new Error('PDF output exceeds the attachment limit.')\n return Object.freeze({filename:attachment.filename,content,contentType:'application/pdf'})\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/contact/server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAOL,KAAK,uBAAuB,EAI7B,MAAM,qCAAqC,CAAA;AAI5C,OAAO,EACL,mCAAmC,EACnC,KAAK,+BAA+B,EACrC,MAAM,eAAe,CAAA;AAGtB,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAIL,KAAK,yBAAyB,EAC9B,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAE/B,KAAK,eAAe,EAIpB,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,mCAAmC,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAE5E,eAAO,MAAM,wBAAwB,EAAG,qBAA8B,CAAA;AACtE,eAAO,MAAM,4BAA4B,EAAG,gBAAyB,CAAA;AACrE,eAAO,MAAM,kCAAkC,UAAY,CAAA;AAE3D,wBAAgB,mCAAmC,CACjD,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,6BAA6B,CAK/B;AA0BD,MAAM,MAAM,uCAAuC,GAAG,IAAI,CACxD,+BAA+B,EAC/B,gBAAgB,CACjB,CAAA;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,uBAAuB,CAAA;IACjC,OAAO,EAAE,uCAAuC,CAAA;IAChD,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,cAAc,CAAC,EAAE,6BAA6B,CAAA;IAC9C,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAA;IACvD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,OAAO,wBAAwB,CAAA;IAC5C,UAAU,EAAE,UAAU,CAAC,OAAO,mCAAmC,CAAC,CAAA;IAClE,mBAAmB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7D;AAED,UAAU,0BAA0B;IAClC,UAAU,EAAE,yBAAyB,CAAA;IACrC,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAC5B,mBAAmB,CAAC,EAAE,0BAA0B,CAAA;IAChD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AA4ID,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,yBAAyB;;;;EAQ5E;AAwjBD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,IAa7D,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA6BnD;AAkCD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,sBAAsB,CAmCxB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/contact/server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAKL,KAAK,uBAAuB,EAI7B,MAAM,qCAAqC,CAAA;AAI5C,OAAO,EACL,mCAAmC,EACnC,KAAK,+BAA+B,EACrC,MAAM,eAAe,CAAA;AAGtB,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAIL,KAAK,yBAAyB,EAC9B,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAE/B,KAAK,eAAe,EAIpB,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,mCAAmC,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAE5E,eAAO,MAAM,wBAAwB,EAAG,qBAA8B,CAAA;AACtE,eAAO,MAAM,4BAA4B,EAAG,gBAAyB,CAAA;AACrE,eAAO,MAAM,kCAAkC,UAAY,CAAA;AAE3D,wBAAgB,mCAAmC,CACjD,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,6BAA6B,CAK/B;AA2BD,MAAM,MAAM,uCAAuC,GAAG,IAAI,CACxD,+BAA+B,EAC/B,gBAAgB,CACjB,CAAA;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,uBAAuB,CAAA;IACjC,OAAO,EAAE,uCAAuC,CAAA;IAChD,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,cAAc,CAAC,EAAE,6BAA6B,CAAA;IAC9C,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAA;IACvD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,OAAO,wBAAwB,CAAA;IAC5C,UAAU,EAAE,UAAU,CAAC,OAAO,mCAAmC,CAAC,CAAA;IAClE,mBAAmB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7D;AAED,UAAU,0BAA0B;IAClC,UAAU,EAAE,yBAAyB,CAAA;IACrC,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAC5B,mBAAmB,CAAC,EAAE,0BAA0B,CAAA;IAChD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AA4ID,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,yBAAyB;;;;EAQ5E;AAylBD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,IAa7D,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA6BnD;AAkCD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,sBAAsB,CAmCxB"}
@@ -1,5 +1,5 @@
1
1
  import 'server-only';
2
- import { DEFAULT_CONTACT_ATTACHMENT_POLICY, deriveContactAttachmentTransportPolicy, detectContactTransportAttachmentType, createContactAttachmentFilename, defineContactContract, parseContactEmailRecipients, } from '@brandlab-digital/site-core/contact';
2
+ import { DEFAULT_CONTACT_ATTACHMENT_POLICY, createContactAttachmentFilename, defineContactContract, parseContactEmailRecipients, } from '@brandlab-digital/site-core/contact';
3
3
  import { isBrandlabCanonicalEntityId } from '@brandlab-digital/site-core/entity-id';
4
4
  import { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes';
5
5
  import { normalizeContactAttributionSummary } from './attribution.js';
@@ -35,6 +35,7 @@ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1_000;
35
35
  const DEFAULT_MAX_ENTRIES = 64;
36
36
  const DEFAULT_MAX_STRING_BYTES = 64 * 1_024;
37
37
  const SUPPORTED_ATTACHMENT_MIME_TYPES = new Set([
38
+ 'application/pdf',
38
39
  'image/jpeg',
39
40
  'image/jpg',
40
41
  'image/png',
@@ -158,10 +159,10 @@ function assertDefinition(definition) {
158
159
  }
159
160
  }
160
161
  function attachmentPolicy(definition) {
161
- return deriveContactAttachmentTransportPolicy({
162
+ return {
162
163
  ...DEFAULT_CONTACT_ATTACHMENT_POLICY,
163
164
  ...(definition.attachments || {}),
164
- });
165
+ };
165
166
  }
166
167
  export function getContactRequestLimits(definition) {
167
168
  return {
@@ -266,6 +267,7 @@ function validateFields(definition, formData) {
266
267
  return { values, fieldErrors };
267
268
  }
268
269
  const EXTENSION_TYPES = {
270
+ pdf: 'application/pdf',
269
271
  jpg: 'image/jpeg',
270
272
  jpeg: 'image/jpeg',
271
273
  png: 'image/png',
@@ -278,6 +280,39 @@ function normalizeMimeType(value) {
278
280
  const normalized = value.trim().toLowerCase();
279
281
  return normalized === 'image/jpg' ? 'image/jpeg' : normalized;
280
282
  }
283
+ function startsWithBytes(bytes, signature) {
284
+ return signature.every((byte, index) => bytes[index] === byte);
285
+ }
286
+ function ascii(bytes, start, length) {
287
+ return String.fromCharCode(...bytes.slice(start, start + length));
288
+ }
289
+ function detectAttachmentType(bytes) {
290
+ if (ascii(bytes, 0, 5) === '%PDF-')
291
+ return 'application/pdf';
292
+ if (startsWithBytes(bytes, [0xff, 0xd8, 0xff]))
293
+ return 'image/jpeg';
294
+ if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
295
+ return 'image/png';
296
+ }
297
+ const gif = ascii(bytes, 0, 6);
298
+ if (gif === 'GIF87a' || gif === 'GIF89a')
299
+ return 'image/gif';
300
+ if (ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WEBP')
301
+ return 'image/webp';
302
+ if (ascii(bytes, 4, 4) === 'ftyp') {
303
+ const brands = new Set();
304
+ for (let offset = 8; offset + 4 <= Math.min(bytes.length, 64); offset += 4) {
305
+ brands.add(ascii(bytes, offset, 4));
306
+ }
307
+ if (['heic', 'heix', 'hevc', 'hevx'].some((brand) => brands.has(brand))) {
308
+ return 'image/heic';
309
+ }
310
+ if (['heif', 'mif1', 'msf1'].some((brand) => brands.has(brand))) {
311
+ return 'image/heif';
312
+ }
313
+ }
314
+ return undefined;
315
+ }
281
316
  async function readValidatedAttachments(definition, formData) {
282
317
  if (!definition.attachments)
283
318
  return [];
@@ -310,11 +345,11 @@ async function readValidatedAttachments(definition, formData) {
310
345
  if (totalBytes > policy.maxTotalSizeBytes)
311
346
  return undefined;
312
347
  const content = new Uint8Array(await file.arrayBuffer());
313
- if (normalizeMimeType(detectContactTransportAttachmentType(content) ?? '') !== declaredType) {
348
+ if (normalizeMimeType(detectAttachmentType(content.slice(0, 64)) ?? '') !== declaredType) {
314
349
  return undefined;
315
350
  }
316
351
  attachments.push({
317
- filename: createContactAttachmentFilename({ index }),
352
+ filename: createContactAttachmentFilename({ index, contentType: declaredType }),
318
353
  content,
319
354
  contentType: declaredType,
320
355
  });
@@ -330,12 +365,12 @@ function validateProcessedAttachments(definition, attachments) {
330
365
  return false;
331
366
  return attachments.every((attachment, index) => {
332
367
  totalBytes += attachment.content.byteLength;
333
- return (attachment.filename === createContactAttachmentFilename({ index }) &&
334
- attachment.contentType === 'image/jpeg' &&
368
+ return (attachment.filename === createContactAttachmentFilename({ index, contentType: attachment.contentType }) &&
369
+ (attachment.contentType === 'image/jpeg' || (attachment.contentType === 'application/pdf' && policy.allowedTypes.includes('application/pdf'))) &&
335
370
  attachment.content.byteLength > 0 &&
336
371
  attachment.content.byteLength <= policy.maxFileSizeBytes &&
337
372
  totalBytes <= policy.maxTotalSizeBytes &&
338
- detectContactTransportAttachmentType(attachment.content.slice(0, 64)) === 'image/jpeg');
373
+ detectAttachmentType(attachment.content.slice(0, 64)) === attachment.contentType);
339
374
  });
340
375
  }
341
376
  function validationResult(fieldErrors) {