@ttt-productions/file-input 0.11.15 → 0.11.17

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 (67) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/image-utils.d.ts +3 -4
  6. package/dist/lib/image-utils.d.ts.map +1 -1
  7. package/dist/lib/image-utils.js +11 -12
  8. package/dist/lib/image-utils.js.map +1 -1
  9. package/dist/lib/infer-content-type.d.ts +1 -24
  10. package/dist/lib/infer-content-type.d.ts.map +1 -1
  11. package/dist/lib/infer-content-type.js +31 -85
  12. package/dist/lib/infer-content-type.js.map +1 -1
  13. package/dist/lib/read-media-meta.d.ts +1 -1
  14. package/dist/lib/read-media-meta.d.ts.map +1 -1
  15. package/dist/lib/read-media-meta.js +20 -13
  16. package/dist/lib/read-media-meta.js.map +1 -1
  17. package/dist/lib/validate-media-duration.d.ts.map +1 -1
  18. package/dist/lib/validate-media-duration.js +5 -10
  19. package/dist/lib/validate-media-duration.js.map +1 -1
  20. package/dist/lib/validate-upload-kind.d.ts +0 -34
  21. package/dist/lib/validate-upload-kind.d.ts.map +1 -1
  22. package/dist/lib/validate-upload-kind.js +0 -17
  23. package/dist/lib/validate-upload-kind.js.map +1 -1
  24. package/dist/progress-bar.d.ts +2 -0
  25. package/dist/progress-bar.d.ts.map +1 -0
  26. package/dist/progress-bar.js +2 -0
  27. package/dist/progress-bar.js.map +1 -0
  28. package/dist/react/components/audio-input.d.ts +2 -2
  29. package/dist/react/components/audio-input.js +2 -2
  30. package/dist/react/components/audio-input.js.map +1 -1
  31. package/dist/react/components/file-input.d.ts +1 -1
  32. package/dist/react/components/file-input.d.ts.map +1 -1
  33. package/dist/react/components/file-input.js +59 -44
  34. package/dist/react/components/file-input.js.map +1 -1
  35. package/dist/react/components/image-cropper-modal.d.ts +3 -3
  36. package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
  37. package/dist/react/components/image-cropper-modal.js +6 -8
  38. package/dist/react/components/image-cropper-modal.js.map +1 -1
  39. package/dist/react/components/image-input.d.ts +2 -2
  40. package/dist/react/components/image-input.js +2 -2
  41. package/dist/react/components/image-input.js.map +1 -1
  42. package/dist/react/components/media-constraints-hint.d.ts +1 -1
  43. package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
  44. package/dist/react/components/media-constraints-hint.js +26 -25
  45. package/dist/react/components/media-constraints-hint.js.map +1 -1
  46. package/dist/react/components/media-input.d.ts +2 -2
  47. package/dist/react/components/media-input.d.ts.map +1 -1
  48. package/dist/react/components/media-input.js +111 -158
  49. package/dist/react/components/media-input.js.map +1 -1
  50. package/dist/react/components/photo-capture-modal.d.ts +1 -1
  51. package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
  52. package/dist/react/components/photo-capture-modal.js +16 -20
  53. package/dist/react/components/photo-capture-modal.js.map +1 -1
  54. package/dist/react/components/record-dialog.d.ts +3 -7
  55. package/dist/react/components/record-dialog.d.ts.map +1 -1
  56. package/dist/react/components/record-dialog.js +60 -138
  57. package/dist/react/components/record-dialog.js.map +1 -1
  58. package/dist/react/components/video-input.d.ts +2 -2
  59. package/dist/react/components/video-input.js +2 -2
  60. package/dist/react/components/video-input.js.map +1 -1
  61. package/dist/react/index.d.ts +8 -8
  62. package/dist/react/index.js +8 -8
  63. package/dist/react/index.js.map +1 -1
  64. package/dist/types.d.ts +3 -59
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from "./types.js";
2
- export * from "./lib/infer-content-type.js";
3
- export * from "./lib/validate-upload-kind.js";
4
- export declare const DEFAULT_PROGRESS_BAR_MIN_BYTES: number;
1
+ export * from './types.js';
2
+ export * from './lib/infer-content-type.js';
3
+ export * from './lib/validate-upload-kind.js';
4
+ export { DEFAULT_PROGRESS_BAR_MIN_BYTES } from './progress-bar.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,eAAO,MAAM,8BAA8B,QAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export * from "./types.js";
2
- export * from "./lib/infer-content-type.js";
3
- export * from "./lib/validate-upload-kind.js";
4
- export const DEFAULT_PROGRESS_BAR_MIN_BYTES = 512 * 1024;
1
+ export * from './types.js';
2
+ export * from './lib/infer-content-type.js';
3
+ export * from './lib/validate-upload-kind.js';
4
+ export { DEFAULT_PROGRESS_BAR_MIN_BYTES } from './progress-bar.js';
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,GAAG,IAAI,CAAC","sourcesContent":["export * from \"./types.js\";\nexport * from \"./lib/infer-content-type.js\";\nexport * from \"./lib/validate-upload-kind.js\";\nexport const DEFAULT_PROGRESS_BAR_MIN_BYTES = 512 * 1024;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["export * from './types.js';\nexport * from './lib/infer-content-type.js';\nexport * from './lib/validate-upload-kind.js';\nexport { DEFAULT_PROGRESS_BAR_MIN_BYTES } from './progress-bar.js';\n"]}
@@ -1,7 +1,6 @@
1
- import type { Area } from "react-easy-crop";
2
- import { type MediaCropSpec } from "@ttt-productions/media-schemas";
3
- /** How a crop is encoded; the fields an origin's crop spec declares. */
4
- export type CropEncoding = Pick<MediaCropSpec, "format" | "quality">;
1
+ import type { Area } from 'react-easy-crop';
2
+ import { type MediaCropSpec } from '@ttt-productions/media-schemas';
3
+ export type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;
5
4
  export declare function getCroppedImg(imageSrc: string, pixelCrop: Area, rotation?: number, flip?: {
6
5
  horizontal: boolean;
7
6
  vertical: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;AA0BrE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,QAAQ,SAAI,EACZ,IAAI;;;CAAyC,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAiDtB"}
1
+ {"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;AA0BrE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,QAAQ,SAAI,EACZ,IAAI;;;CAAyC,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAgDtB"}
@@ -1,11 +1,11 @@
1
- import { MEDIA_FORMATS } from "@ttt-productions/media-schemas";
2
- const DEFAULT_CROP_FORMAT = "jpeg";
1
+ import { MEDIA_FORMATS } from '@ttt-productions/media-schemas';
2
+ const DEFAULT_CROP_FORMAT = 'jpeg';
3
3
  const DEFAULT_CROP_QUALITY = 80;
4
4
  const createImage = (url) => new Promise((resolve, reject) => {
5
5
  const image = new Image();
6
- image.addEventListener("load", () => resolve(image));
7
- image.addEventListener("error", (error) => reject(error));
8
- image.setAttribute("crossOrigin", "anonymous");
6
+ image.addEventListener('load', () => resolve(image));
7
+ image.addEventListener('error', (error) => reject(error));
8
+ image.setAttribute('crossOrigin', 'anonymous');
9
9
  image.src = url;
10
10
  });
11
11
  function getRadianAngle(deg) {
@@ -20,8 +20,8 @@ function rotateSize(width, height, rotation) {
20
20
  }
21
21
  export async function getCroppedImg(imageSrc, pixelCrop, rotation = 0, flip = { horizontal: false, vertical: false }, outputWidth, outputHeight, encoding = {}) {
22
22
  const image = await createImage(imageSrc);
23
- const canvas = document.createElement("canvas");
24
- const ctx = canvas.getContext("2d");
23
+ const canvas = document.createElement('canvas');
24
+ const ctx = canvas.getContext('2d');
25
25
  if (!ctx)
26
26
  return null;
27
27
  const rotRad = getRadianAngle(rotation);
@@ -33,19 +33,18 @@ export async function getCroppedImg(imageSrc, pixelCrop, rotation = 0, flip = {
33
33
  ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
34
34
  ctx.translate(-image.width / 2, -image.height / 2);
35
35
  ctx.drawImage(image, 0, 0);
36
- const croppedCanvas = document.createElement("canvas");
37
- const croppedCtx = croppedCanvas.getContext("2d");
36
+ const croppedCanvas = document.createElement('canvas');
37
+ const croppedCtx = croppedCanvas.getContext('2d');
38
38
  if (!croppedCtx)
39
39
  return null;
40
40
  croppedCanvas.width = outputWidth || pixelCrop.width;
41
41
  croppedCanvas.height = outputHeight || pixelCrop.height;
42
42
  croppedCtx.drawImage(canvas, pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, 0, 0, croppedCanvas.width, croppedCanvas.height);
43
- // This is the first lossy encode the image gets, so it uses the origin's declared format and
44
- // quality; a browser that cannot encode the format returns PNG, and the blob's type says so.
43
+ // A browser that cannot encode the requested format returns PNG; the blob's type says which.
45
44
  const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all[0];
46
45
  const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;
47
46
  return new Promise((resolve, reject) => {
48
- croppedCanvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("Canvas to Blob conversion failed"))), mime, quality);
47
+ croppedCanvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Canvas to Blob conversion failed'))), mime, quality);
49
48
  });
50
49
  }
51
50
  //# sourceMappingURL=image-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Area } from \"react-easy-crop\";\nimport { MEDIA_FORMATS, type MediaCropSpec } from \"@ttt-productions/media-schemas\";\n\n/** How a crop is encoded; the fields an origin's crop spec declares. */\nexport type CropEncoding = Pick<MediaCropSpec, \"format\" | \"quality\">;\n\nconst DEFAULT_CROP_FORMAT: NonNullable<MediaCropSpec[\"format\"]> = \"jpeg\";\nconst DEFAULT_CROP_QUALITY = 80;\n\nconst createImage = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener(\"load\", () => resolve(image));\n image.addEventListener(\"error\", (error) => reject(error));\n image.setAttribute(\"crossOrigin\", \"anonymous\");\n image.src = url;\n });\n\nfunction getRadianAngle(deg: number) {\n return (deg * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number) {\n const rotRad = getRadianAngle(rotation);\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n };\n}\n\nexport async function getCroppedImg(\n imageSrc: string,\n pixelCrop: Area,\n rotation = 0,\n flip = { horizontal: false, vertical: false },\n outputWidth?: number,\n outputHeight?: number,\n encoding: CropEncoding = {}\n): Promise<Blob | null> {\n const image = await createImage(imageSrc);\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const rotRad = getRadianAngle(rotation);\n const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);\n\n canvas.width = bBoxWidth;\n canvas.height = bBoxHeight;\n\n ctx.translate(bBoxWidth / 2, bBoxHeight / 2);\n ctx.rotate(rotRad);\n ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);\n ctx.translate(-image.width / 2, -image.height / 2);\n ctx.drawImage(image, 0, 0);\n\n const croppedCanvas = document.createElement(\"canvas\");\n const croppedCtx = croppedCanvas.getContext(\"2d\");\n if (!croppedCtx) return null;\n\n croppedCanvas.width = outputWidth || pixelCrop.width;\n croppedCanvas.height = outputHeight || pixelCrop.height;\n\n croppedCtx.drawImage(\n canvas,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n croppedCanvas.width,\n croppedCanvas.height\n );\n\n // This is the first lossy encode the image gets, so it uses the origin's declared format and\n // quality; a browser that cannot encode the format returns PNG, and the blob's type says so.\n const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all![0];\n const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;\n\n return new Promise((resolve, reject) => {\n croppedCanvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Canvas to Blob conversion failed\"))),\n mime,\n quality\n );\n });\n}\n"]}
1
+ {"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Area } from 'react-easy-crop';\nimport { MEDIA_FORMATS, type MediaCropSpec } from '@ttt-productions/media-schemas';\n\nexport type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;\n\nconst DEFAULT_CROP_FORMAT: NonNullable<MediaCropSpec['format']> = 'jpeg';\nconst DEFAULT_CROP_QUALITY = 80;\n\nconst createImage = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener('load', () => resolve(image));\n image.addEventListener('error', (error) => reject(error));\n image.setAttribute('crossOrigin', 'anonymous');\n image.src = url;\n });\n\nfunction getRadianAngle(deg: number) {\n return (deg * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number) {\n const rotRad = getRadianAngle(rotation);\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n };\n}\n\nexport async function getCroppedImg(\n imageSrc: string,\n pixelCrop: Area,\n rotation = 0,\n flip = { horizontal: false, vertical: false },\n outputWidth?: number,\n outputHeight?: number,\n encoding: CropEncoding = {},\n): Promise<Blob | null> {\n const image = await createImage(imageSrc);\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n const rotRad = getRadianAngle(rotation);\n const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);\n\n canvas.width = bBoxWidth;\n canvas.height = bBoxHeight;\n\n ctx.translate(bBoxWidth / 2, bBoxHeight / 2);\n ctx.rotate(rotRad);\n ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);\n ctx.translate(-image.width / 2, -image.height / 2);\n ctx.drawImage(image, 0, 0);\n\n const croppedCanvas = document.createElement('canvas');\n const croppedCtx = croppedCanvas.getContext('2d');\n if (!croppedCtx) return null;\n\n croppedCanvas.width = outputWidth || pixelCrop.width;\n croppedCanvas.height = outputHeight || pixelCrop.height;\n\n croppedCtx.drawImage(\n canvas,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n croppedCanvas.width,\n croppedCanvas.height,\n );\n\n // A browser that cannot encode the requested format returns PNG; the blob's type says which.\n const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all![0];\n const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;\n\n return new Promise((resolve, reject) => {\n croppedCanvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Canvas to Blob conversion failed'))),\n mime,\n quality,\n );\n });\n}\n"]}
@@ -1,27 +1,4 @@
1
- export type InferKind = "image" | "video" | "audio";
2
- /**
3
- * Returns a valid, parameter-less media MIME for the given File.
4
- *
5
- * Precedence:
6
- * 1. The base of file.type (parameters stripped, lowercased) when valid —
7
- * unless `fallbackKind` is supplied and disagrees with the declared
8
- * top-level kind. A caller-declared kind (e.g. the record dialog's chosen
9
- * recording kind) is authoritative context, so a conflicting declared type
10
- * is treated as untrustworthy and resolution falls through to the
11
- * extension.
12
- * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
13
- * and video) a supplied `fallbackKind` picks the matching side; unambiguous
14
- * extensions keep their single mapping.
15
- * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
16
- * unknown picker file is never rewritten into a false media fact.
17
- */
1
+ export type InferKind = 'image' | 'video' | 'audio';
18
2
  export declare function inferContentType(file: File, fallbackKind?: InferKind): string;
19
- /**
20
- * Returns a File whose .type is exactly the valid, parameter-less media MIME
21
- * from inferContentType. If the original file already carries that exact
22
- * type, it is returned unchanged; otherwise (missing type, parameterized
23
- * type, wrong case, or a type overridden by fallbackKind precedence) the
24
- * bytes are wrapped in a new File with the normalized type.
25
- */
26
3
  export declare function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File;
27
4
  //# sourceMappingURL=infer-content-type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAoEpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAgB7E;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAIpF"}
1
+ {"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAuDpD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAc7E;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAIpF"}
@@ -1,100 +1,55 @@
1
- // Defense-in-depth: guarantee every File we emit has a NORMALIZED, honest
2
- // content type. If file.type is missing or unusable, infer from the extension;
3
- // with a caller-declared kind (recorder/capture — the caller KNOWS what it
4
- // produced) fall back to that kind's default container.
5
- //
6
- // MediaRecorder (and some pickers) report parameterized types such as
7
- // "audio/webm;codecs=opus". Downstream storage rules validate a
8
- // parameter-less base type, so we always emit the base ("audio/webm"),
9
- // never the parameterized form.
10
- //
11
- // UNKNOWN PICKER METADATA IS NOT FABRICATED (canonical-upload-content-
12
- // classification, 2026-07-31): when there is no declared kind, no usable MIME,
13
- // and no known extension, the file passes through as the NEUTRAL
14
- // `application/octet-stream` so the server's canonical byte inspector — the
15
- // only classification authority — sees "unknown" as unknown. The previous
16
- // behavior defaulted such files to `image/jpeg`, which invented a false media
17
- // fact the whole pipeline then trusted. upload-core accepts the neutral type
18
- // only via its explicit opt-in (`allowNeutralContentType`).
19
- import { NEUTRAL_CONTENT_TYPE } from "@ttt-productions/media-schemas";
1
+ // MediaRecorder and some pickers report parameterized types ("audio/webm;codecs=opus"), and
2
+ // downstream checks validate the parameter-less base, so only the base is ever emitted.
3
+ import { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';
20
4
  const EXT_TO_MIME = {
21
- // image
22
- jpg: "image/jpeg",
23
- jpeg: "image/jpeg",
24
- png: "image/png",
25
- gif: "image/gif",
26
- webp: "image/webp",
27
- avif: "image/avif",
28
- heic: "image/heic",
29
- heif: "image/heif",
30
- // video
31
- mp4: "video/mp4",
32
- m4v: "video/mp4",
33
- mov: "video/quicktime",
34
- webm: "video/webm",
35
- mkv: "video/x-matroska",
36
- // audio
37
- mp3: "audio/mpeg",
38
- m4a: "audio/mp4",
39
- aac: "audio/aac",
40
- wav: "audio/wav",
41
- ogg: "audio/ogg",
42
- oga: "audio/ogg",
43
- flac: "audio/flac",
5
+ jpg: 'image/jpeg',
6
+ jpeg: 'image/jpeg',
7
+ png: 'image/png',
8
+ gif: 'image/gif',
9
+ webp: 'image/webp',
10
+ avif: 'image/avif',
11
+ heic: 'image/heic',
12
+ heif: 'image/heif',
13
+ mp4: 'video/mp4',
14
+ m4v: 'video/mp4',
15
+ mov: 'video/quicktime',
16
+ webm: 'video/webm',
17
+ mkv: 'video/x-matroska',
18
+ mp3: 'audio/mpeg',
19
+ m4a: 'audio/mp4',
20
+ aac: 'audio/aac',
21
+ wav: 'audio/wav',
22
+ ogg: 'audio/ogg',
23
+ oga: 'audio/ogg',
24
+ flac: 'audio/flac',
44
25
  };
45
- // Containers that legitimately hold either audio or video. When the caller
46
- // declares a kind (e.g. the record dialog's chosen recording kind), that kind
47
- // picks the matching side; without a kind we keep the historical single-kind
48
- // guess in EXT_TO_MIME (webm → video, ogg → audio).
49
26
  const AMBIGUOUS_EXT_TO_MIME = {
50
- webm: { audio: "audio/webm", video: "video/webm" },
51
- ogg: { audio: "audio/ogg", video: "video/ogg" },
27
+ webm: { audio: 'audio/webm', video: 'video/webm' },
28
+ ogg: { audio: 'audio/ogg', video: 'video/ogg' },
52
29
  };
53
30
  const KIND_DEFAULT = {
54
- image: "image/jpeg",
55
- video: "video/webm",
56
- audio: "audio/webm",
31
+ image: 'image/jpeg',
32
+ video: 'video/webm',
33
+ audio: 'audio/webm',
57
34
  };
58
35
  const BASE_MEDIA_MIME_RE = /^(image|video|audio)\/[a-z0-9.+-]+$/;
59
- /**
60
- * Extracts the parameter-less base MIME ("audio/webm" from
61
- * "audio/webm;codecs=opus"), lowercased, when it is a usable media type.
62
- * Returns null for empty values, application/octet-stream, and anything
63
- * whose base is not image/*, video/*, or audio/*.
64
- */
65
36
  function parseBaseMime(mime) {
66
37
  if (!mime)
67
38
  return null;
68
- const base = mime.split(";")[0].trim().toLowerCase();
39
+ const base = mime.split(';')[0].trim().toLowerCase();
69
40
  if (base === NEUTRAL_CONTENT_TYPE)
70
41
  return null;
71
42
  return BASE_MEDIA_MIME_RE.test(base) ? base : null;
72
43
  }
73
44
  function kindOfMime(base) {
74
- return base.slice(0, base.indexOf("/"));
45
+ return base.slice(0, base.indexOf('/'));
75
46
  }
76
47
  function extOf(name) {
77
- const dot = name.lastIndexOf(".");
48
+ const dot = name.lastIndexOf('.');
78
49
  if (dot < 0 || dot === name.length - 1)
79
50
  return null;
80
51
  return name.slice(dot + 1).toLowerCase();
81
52
  }
82
- /**
83
- * Returns a valid, parameter-less media MIME for the given File.
84
- *
85
- * Precedence:
86
- * 1. The base of file.type (parameters stripped, lowercased) when valid —
87
- * unless `fallbackKind` is supplied and disagrees with the declared
88
- * top-level kind. A caller-declared kind (e.g. the record dialog's chosen
89
- * recording kind) is authoritative context, so a conflicting declared type
90
- * is treated as untrustworthy and resolution falls through to the
91
- * extension.
92
- * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
93
- * and video) a supplied `fallbackKind` picks the matching side; unambiguous
94
- * extensions keep their single mapping.
95
- * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
96
- * unknown picker file is never rewritten into a false media fact.
97
- */
98
53
  export function inferContentType(file, fallbackKind) {
99
54
  const base = parseBaseMime(file.type);
100
55
  if (base && (!fallbackKind || kindOfMime(base) === fallbackKind))
@@ -109,17 +64,8 @@ export function inferContentType(file, fallbackKind) {
109
64
  }
110
65
  if (fallbackKind)
111
66
  return KIND_DEFAULT[fallbackKind];
112
- // No declared kind, no usable MIME, no known extension: NEUTRAL, never a
113
- // fabricated media type. The server inspector owns classification.
114
67
  return NEUTRAL_CONTENT_TYPE;
115
68
  }
116
- /**
117
- * Returns a File whose .type is exactly the valid, parameter-less media MIME
118
- * from inferContentType. If the original file already carries that exact
119
- * type, it is returned unchanged; otherwise (missing type, parameterized
120
- * type, wrong case, or a type overridden by fallbackKind precedence) the
121
- * bytes are wrapped in a new File with the normalized type.
122
- */
123
69
  export function ensureFileWithContentType(file, fallbackKind) {
124
70
  const type = inferContentType(file, fallbackKind);
125
71
  if (file.type === type)
@@ -1 +1 @@
1
- {"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"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","sourcesContent":["// Defense-in-depth: guarantee every File we emit has a NORMALIZED, honest\n// content type. If file.type is missing or unusable, infer from the extension;\n// with a caller-declared kind (recorder/capture — the caller KNOWS what it\n// produced) fall back to that kind's default container.\n//\n// MediaRecorder (and some pickers) report parameterized types such as\n// \"audio/webm;codecs=opus\". Downstream storage rules validate a\n// parameter-less base type, so we always emit the base (\"audio/webm\"),\n// never the parameterized form.\n//\n// UNKNOWN PICKER METADATA IS NOT FABRICATED (canonical-upload-content-\n// classification, 2026-07-31): when there is no declared kind, no usable MIME,\n// and no known extension, the file passes through as the NEUTRAL\n// `application/octet-stream` so the server's canonical byte inspector — the\n// only classification authority — sees \"unknown\" as unknown. The previous\n// behavior defaulted such files to `image/jpeg`, which invented a false media\n// fact the whole pipeline then trusted. upload-core accepts the neutral type\n// only via its explicit opt-in (`allowNeutralContentType`).\n\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\n\nexport type InferKind = \"image\" | \"video\" | \"audio\";\n\nconst EXT_TO_MIME: Record<string, string> = {\n // image\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n avif: \"image/avif\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n // video\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n webm: \"video/webm\",\n mkv: \"video/x-matroska\",\n // audio\n mp3: \"audio/mpeg\",\n m4a: \"audio/mp4\",\n aac: \"audio/aac\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n oga: \"audio/ogg\",\n flac: \"audio/flac\",\n};\n\n// Containers that legitimately hold either audio or video. When the caller\n// declares a kind (e.g. the record dialog's chosen recording kind), that kind\n// picks the matching side; without a kind we keep the historical single-kind\n// guess in EXT_TO_MIME (webm → video, ogg → audio).\nconst AMBIGUOUS_EXT_TO_MIME: Record<string, Partial<Record<InferKind, string>>> = {\n webm: { audio: \"audio/webm\", video: \"video/webm\" },\n ogg: { audio: \"audio/ogg\", video: \"video/ogg\" },\n};\n\nconst KIND_DEFAULT: Record<InferKind, string> = {\n image: \"image/jpeg\",\n video: \"video/webm\",\n audio: \"audio/webm\",\n};\n\nconst BASE_MEDIA_MIME_RE = /^(image|video|audio)\\/[a-z0-9.+-]+$/;\n\n/**\n * Extracts the parameter-less base MIME (\"audio/webm\" from\n * \"audio/webm;codecs=opus\"), lowercased, when it is a usable media type.\n * Returns null for empty values, application/octet-stream, and anything\n * whose base is not image/*, video/*, or audio/*.\n */\nfunction parseBaseMime(mime: string | undefined | null): string | null {\n if (!mime) return null;\n const base = mime.split(\";\")[0].trim().toLowerCase();\n if (base === NEUTRAL_CONTENT_TYPE) return null;\n return BASE_MEDIA_MIME_RE.test(base) ? base : null;\n}\n\nfunction kindOfMime(base: string): InferKind {\n return base.slice(0, base.indexOf(\"/\")) as InferKind;\n}\n\nfunction extOf(name: string): string | null {\n const dot = name.lastIndexOf(\".\");\n if (dot < 0 || dot === name.length - 1) return null;\n return name.slice(dot + 1).toLowerCase();\n}\n\n/**\n * Returns a valid, parameter-less media MIME for the given File.\n *\n * Precedence:\n * 1. The base of file.type (parameters stripped, lowercased) when valid —\n * unless `fallbackKind` is supplied and disagrees with the declared\n * top-level kind. A caller-declared kind (e.g. the record dialog's chosen\n * recording kind) is authoritative context, so a conflicting declared type\n * is treated as untrustworthy and resolution falls through to the\n * extension.\n * 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio\n * and video) a supplied `fallbackKind` picks the matching side; unambiguous\n * extensions keep their single mapping.\n * 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an\n * unknown picker file is never rewritten into a false media fact.\n */\nexport function inferContentType(file: File, fallbackKind?: InferKind): string {\n const base = parseBaseMime(file.type);\n if (base && (!fallbackKind || kindOfMime(base) === fallbackKind)) return base;\n\n const ext = extOf(file.name);\n if (ext) {\n const byKind = fallbackKind ? AMBIGUOUS_EXT_TO_MIME[ext]?.[fallbackKind] : undefined;\n if (byKind) return byKind;\n if (EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];\n }\n\n if (fallbackKind) return KIND_DEFAULT[fallbackKind];\n\n // No declared kind, no usable MIME, no known extension: NEUTRAL, never a\n // fabricated media type. The server inspector owns classification.\n return NEUTRAL_CONTENT_TYPE;\n}\n\n/**\n * Returns a File whose .type is exactly the valid, parameter-less media MIME\n * from inferContentType. If the original file already carries that exact\n * type, it is returned unchanged; otherwise (missing type, parameterized\n * type, wrong case, or a type overridden by fallbackKind precedence) the\n * bytes are wrapped in a new File with the normalized type.\n */\nexport function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File {\n const type = inferContentType(file, fallbackKind);\n if (file.type === type) return file;\n return new File([file], file.name, { type, lastModified: file.lastModified });\n}\n"]}
1
+ {"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"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","sourcesContent":["// MediaRecorder and some pickers report parameterized types (\"audio/webm;codecs=opus\"), and\n// downstream checks validate the parameter-less base, so only the base is ever emitted.\n\nimport { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';\n\nexport type InferKind = 'image' | 'video' | 'audio';\n\nconst EXT_TO_MIME: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n heic: 'image/heic',\n heif: 'image/heif',\n mp4: 'video/mp4',\n m4v: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n mkv: 'video/x-matroska',\n mp3: 'audio/mpeg',\n m4a: 'audio/mp4',\n aac: 'audio/aac',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n oga: 'audio/ogg',\n flac: 'audio/flac',\n};\n\nconst AMBIGUOUS_EXT_TO_MIME: Record<string, Partial<Record<InferKind, string>>> = {\n webm: { audio: 'audio/webm', video: 'video/webm' },\n ogg: { audio: 'audio/ogg', video: 'video/ogg' },\n};\n\nconst KIND_DEFAULT: Record<InferKind, string> = {\n image: 'image/jpeg',\n video: 'video/webm',\n audio: 'audio/webm',\n};\n\nconst BASE_MEDIA_MIME_RE = /^(image|video|audio)\\/[a-z0-9.+-]+$/;\n\nfunction parseBaseMime(mime: string | undefined | null): string | null {\n if (!mime) return null;\n const base = mime.split(';')[0].trim().toLowerCase();\n if (base === NEUTRAL_CONTENT_TYPE) return null;\n return BASE_MEDIA_MIME_RE.test(base) ? base : null;\n}\n\nfunction kindOfMime(base: string): InferKind {\n return base.slice(0, base.indexOf('/')) as InferKind;\n}\n\nfunction extOf(name: string): string | null {\n const dot = name.lastIndexOf('.');\n if (dot < 0 || dot === name.length - 1) return null;\n return name.slice(dot + 1).toLowerCase();\n}\n\nexport function inferContentType(file: File, fallbackKind?: InferKind): string {\n const base = parseBaseMime(file.type);\n if (base && (!fallbackKind || kindOfMime(base) === fallbackKind)) return base;\n\n const ext = extOf(file.name);\n if (ext) {\n const byKind = fallbackKind ? AMBIGUOUS_EXT_TO_MIME[ext]?.[fallbackKind] : undefined;\n if (byKind) return byKind;\n if (EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];\n }\n\n if (fallbackKind) return KIND_DEFAULT[fallbackKind];\n\n return NEUTRAL_CONTENT_TYPE;\n}\n\nexport function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File {\n const type = inferContentType(file, fallbackKind);\n if (file.type === type) return file;\n return new File([file], file.name, { type, lastModified: file.lastModified });\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { MediaKind, VideoOrientation } from "@ttt-productions/media-schemas";
1
+ import type { MediaKind, VideoOrientation } from '@ttt-productions/media-schemas';
2
2
  export interface ReadMediaMetaResult {
3
3
  kind: MediaKind;
4
4
  mime?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"read-media-meta.d.ts","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAElF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA2ED,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B5E"}
1
+ {"version":3,"file":"read-media-meta.d.ts","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAElF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAoFD,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B5E"}
@@ -1,15 +1,15 @@
1
- import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
1
+ import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';
2
2
  function simplifiedToKind(s) {
3
- if (s === "image" || s === "video" || s === "audio")
3
+ if (s === 'image' || s === 'video' || s === 'audio')
4
4
  return s;
5
- return "file";
5
+ return 'file';
6
6
  }
7
7
  function computeOrientation(width, height) {
8
8
  if (!width || !height)
9
9
  return undefined;
10
10
  if (width === height)
11
- return "any";
12
- return height > width ? "vertical" : "horizontal";
11
+ return 'any';
12
+ return height > width ? 'vertical' : 'horizontal';
13
13
  }
14
14
  async function readImage(file) {
15
15
  return new Promise((resolve) => {
@@ -37,18 +37,25 @@ async function readImage(file) {
37
37
  }
38
38
  async function readAV(file) {
39
39
  return new Promise((resolve) => {
40
- const isVideo = file.type.startsWith("video/");
41
- const el = document.createElement(isVideo ? "video" : "audio");
42
- el.preload = "metadata";
40
+ const isVideo = file.type.startsWith('video/');
41
+ const el = document.createElement(isVideo ? 'video' : 'audio');
42
+ el.preload = 'metadata';
43
43
  const url = URL.createObjectURL(file);
44
44
  el.onloadedmetadata = () => {
45
45
  try {
46
- const durationSec = Number.isFinite(el.duration) ? el.duration : undefined;
46
+ const durationSec = Number.isFinite(el.duration)
47
+ ? el.duration
48
+ : undefined;
47
49
  if (isVideo) {
48
50
  const v = el;
49
51
  const width = v.videoWidth || undefined;
50
52
  const height = v.videoHeight || undefined;
51
- resolve({ width, height, durationSec, aspectRatio: height ? width / height : undefined });
53
+ resolve({
54
+ width,
55
+ height,
56
+ durationSec,
57
+ aspectRatio: height ? width / height : undefined,
58
+ });
52
59
  }
53
60
  else {
54
61
  resolve({ durationSec });
@@ -79,7 +86,7 @@ export async function readMediaMeta(file) {
79
86
  mime: file.type || undefined,
80
87
  sizeBytes: file.size,
81
88
  };
82
- if (kind === "image") {
89
+ if (kind === 'image') {
83
90
  const img = await readImage(file);
84
91
  return {
85
92
  ...base,
@@ -87,12 +94,12 @@ export async function readMediaMeta(file) {
87
94
  orientation: computeOrientation(img.width, img.height),
88
95
  };
89
96
  }
90
- if (kind === "video" || kind === "audio") {
97
+ if (kind === 'video' || kind === 'audio') {
91
98
  const av = await readAV(file);
92
99
  return {
93
100
  ...base,
94
101
  ...av,
95
- orientation: kind === "video" ? computeOrientation(av.width, av.height) : undefined,
102
+ orientation: kind === 'video' ? computeOrientation(av.width, av.height) : undefined,
96
103
  };
97
104
  }
98
105
  return base;
@@ -1 +1 @@
1
- {"version":3,"file":"read-media-meta.js","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"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","sourcesContent":["import { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\nimport type { MediaKind, VideoOrientation } from \"@ttt-productions/media-schemas\";\n\nexport interface ReadMediaMetaResult {\n kind: MediaKind;\n mime?: string;\n sizeBytes: number;\n width?: number;\n height?: number;\n durationSec?: number;\n orientation?: VideoOrientation;\n aspectRatio?: number;\n}\n\nfunction simplifiedToKind(s: ReturnType<typeof getSimplifiedMediaType>): MediaKind {\n if (s === \"image\" || s === \"video\" || s === \"audio\") return s;\n return \"file\";\n}\n\nfunction computeOrientation(width?: number, height?: number): VideoOrientation | undefined {\n if (!width || !height) return undefined;\n if (width === height) return \"any\";\n return height > width ? \"vertical\" : \"horizontal\";\n}\n\nasync function readImage(file: File): Promise<Pick<ReadMediaMetaResult, \"width\" | \"height\" | \"aspectRatio\">> {\n return new Promise((resolve) => {\n const url = URL.createObjectURL(file);\n const img = new Image();\n img.onload = () => {\n try {\n const width = img.naturalWidth;\n const height = img.naturalHeight;\n resolve({ width, height, aspectRatio: height ? width / height : undefined });\n } finally {\n URL.revokeObjectURL(url);\n }\n };\n img.onerror = () => {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n resolve({});\n };\n img.src = url;\n });\n}\n\nasync function readAV(\n file: File\n): Promise<Pick<ReadMediaMetaResult, \"width\" | \"height\" | \"durationSec\" | \"aspectRatio\">> {\n return new Promise((resolve) => {\n const isVideo = file.type.startsWith(\"video/\");\n const el = document.createElement(isVideo ? \"video\" : \"audio\");\n el.preload = \"metadata\";\n\n const url = URL.createObjectURL(file);\n\n el.onloadedmetadata = () => {\n try {\n const durationSec = Number.isFinite((el as any).duration) ? (el as any).duration : undefined;\n if (isVideo) {\n const v = el as HTMLVideoElement;\n const width = v.videoWidth || undefined;\n const height = v.videoHeight || undefined;\n resolve({ width, height, durationSec, aspectRatio: height ? width! / height : undefined });\n } else {\n resolve({ durationSec });\n }\n } finally {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n }\n };\n\n el.onerror = () => {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n resolve({});\n };\n\n el.src = url;\n });\n}\n\nexport async function readMediaMeta(file: File): Promise<ReadMediaMetaResult> {\n const simplified = getSimplifiedMediaType(file);\n const kind = simplifiedToKind(simplified);\n\n const base: ReadMediaMetaResult = {\n kind,\n mime: file.type || undefined,\n sizeBytes: file.size,\n };\n\n if (kind === \"image\") {\n const img = await readImage(file);\n return {\n ...base,\n ...img,\n orientation: computeOrientation(img.width, img.height),\n };\n }\n\n if (kind === \"video\" || kind === \"audio\") {\n const av = await readAV(file);\n return {\n ...base,\n ...av,\n orientation: kind === \"video\" ? computeOrientation(av.width, av.height) : undefined,\n };\n }\n\n return base;\n}\n"]}
1
+ {"version":3,"file":"read-media-meta.js","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"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","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport type { MediaKind, VideoOrientation } from '@ttt-productions/media-schemas';\n\nexport interface ReadMediaMetaResult {\n kind: MediaKind;\n mime?: string;\n sizeBytes: number;\n width?: number;\n height?: number;\n durationSec?: number;\n orientation?: VideoOrientation;\n aspectRatio?: number;\n}\n\nfunction simplifiedToKind(s: ReturnType<typeof getSimplifiedMediaType>): MediaKind {\n if (s === 'image' || s === 'video' || s === 'audio') return s;\n return 'file';\n}\n\nfunction computeOrientation(width?: number, height?: number): VideoOrientation | undefined {\n if (!width || !height) return undefined;\n if (width === height) return 'any';\n return height > width ? 'vertical' : 'horizontal';\n}\n\nasync function readImage(\n file: File,\n): Promise<Pick<ReadMediaMetaResult, 'width' | 'height' | 'aspectRatio'>> {\n return new Promise((resolve) => {\n const url = URL.createObjectURL(file);\n const img = new Image();\n img.onload = () => {\n try {\n const width = img.naturalWidth;\n const height = img.naturalHeight;\n resolve({ width, height, aspectRatio: height ? width / height : undefined });\n } finally {\n URL.revokeObjectURL(url);\n }\n };\n img.onerror = () => {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n resolve({});\n };\n img.src = url;\n });\n}\n\nasync function readAV(\n file: File,\n): Promise<Pick<ReadMediaMetaResult, 'width' | 'height' | 'durationSec' | 'aspectRatio'>> {\n return new Promise((resolve) => {\n const isVideo = file.type.startsWith('video/');\n const el = document.createElement(isVideo ? 'video' : 'audio');\n el.preload = 'metadata';\n\n const url = URL.createObjectURL(file);\n\n el.onloadedmetadata = () => {\n try {\n const durationSec = Number.isFinite((el as any).duration)\n ? (el as any).duration\n : undefined;\n if (isVideo) {\n const v = el as HTMLVideoElement;\n const width = v.videoWidth || undefined;\n const height = v.videoHeight || undefined;\n resolve({\n width,\n height,\n durationSec,\n aspectRatio: height ? width! / height : undefined,\n });\n } else {\n resolve({ durationSec });\n }\n } finally {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n }\n };\n\n el.onerror = () => {\n try {\n URL.revokeObjectURL(url);\n } catch {}\n resolve({});\n };\n\n el.src = url;\n });\n}\n\nexport async function readMediaMeta(file: File): Promise<ReadMediaMetaResult> {\n const simplified = getSimplifiedMediaType(file);\n const kind = simplifiedToKind(simplified);\n\n const base: ReadMediaMetaResult = {\n kind,\n mime: file.type || undefined,\n sizeBytes: file.size,\n };\n\n if (kind === 'image') {\n const img = await readImage(file);\n return {\n ...base,\n ...img,\n orientation: computeOrientation(img.width, img.height),\n };\n }\n\n if (kind === 'video' || kind === 'audio') {\n const av = await readAV(file);\n return {\n ...base,\n ...av,\n orientation: kind === 'video' ? computeOrientation(av.width, av.height) : undefined,\n };\n }\n\n return base;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsChG"}
1
+ {"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiChG"}
@@ -1,8 +1,8 @@
1
- import { resolveInfiniteDuration } from "@ttt-productions/media-viewer";
1
+ import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';
2
2
  export async function validateMediaDuration(file, maxDurationSec) {
3
3
  return new Promise((resolve) => {
4
- const el = document.createElement(file.type.startsWith("video/") ? "video" : "audio");
5
- el.preload = "metadata";
4
+ const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');
5
+ el.preload = 'metadata';
6
6
  let settled = false;
7
7
  const finish = (ok) => {
8
8
  if (settled)
@@ -19,18 +19,13 @@ export async function validateMediaDuration(file, maxDurationSec) {
19
19
  finish(el.duration <= maxDurationSec);
20
20
  return;
21
21
  }
22
- // Chromium MediaRecorder blobs (recorded audio/video) report duration
23
- // Infinity at loadedmetadata, so `Infinity <= max` would reject every
24
- // recording. Resolve the real duration via the shared seek workaround;
25
- // if it still cannot be determined, let the file through — the server
26
- // measures every file's length and refuses one over the limit or with
27
- // no length at all.
22
+ // Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround
23
+ // learns the real one.
28
24
  void resolveInfiniteDuration(el).then((duration) => {
29
25
  finish(duration === null ? true : duration <= maxDurationSec);
30
26
  });
31
27
  };
32
28
  el.onerror = () => {
33
- // Fail open on client (backend can enforce again).
34
29
  finish(true);
35
30
  };
36
31
  el.src = URL.createObjectURL(file);
@@ -1 +1 @@
1
- {"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBACH,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QAEF,EAAE,CAAC,gBAAgB,GAAG,GAAG,EAAE;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;gBACtC,OAAO;YACT,CAAC;YACD,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,sEAAsE;YACtE,sEAAsE;YACtE,oBAAoB;YACpB,KAAK,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACjD,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,mDAAmD;YACnD,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC;QAEF,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { resolveInfiniteDuration } from \"@ttt-productions/media-viewer\";\n\nexport async function validateMediaDuration(file: File, maxDurationSec: number): Promise<boolean> {\n return new Promise((resolve) => {\n const el = document.createElement(file.type.startsWith(\"video/\") ? \"video\" : \"audio\");\n el.preload = \"metadata\";\n\n let settled = false;\n const finish = (ok: boolean) => {\n if (settled) return;\n settled = true;\n try {\n URL.revokeObjectURL(el.src);\n } catch {}\n resolve(ok);\n };\n\n el.onloadedmetadata = () => {\n if (Number.isFinite(el.duration)) {\n finish(el.duration <= maxDurationSec);\n return;\n }\n // Chromium MediaRecorder blobs (recorded audio/video) report duration\n // Infinity at loadedmetadata, so `Infinity <= max` would reject every\n // recording. Resolve the real duration via the shared seek workaround;\n // if it still cannot be determined, let the file through — the server\n // measures every file's length and refuses one over the limit or with\n // no length at all.\n void resolveInfiniteDuration(el).then((duration) => {\n finish(duration === null ? true : duration <= maxDurationSec);\n });\n };\n\n el.onerror = () => {\n // Fail open on client (backend can enforce again).\n finish(true);\n };\n\n el.src = URL.createObjectURL(file);\n });\n}\n"]}
1
+ {"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBACH,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QAEF,EAAE,CAAC,gBAAgB,GAAG,GAAG,EAAE;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;gBACtC,OAAO;YACT,CAAC;YACD,+FAA+F;YAC/F,uBAAuB;YACvB,KAAK,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACjD,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC;QAEF,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';\n\nexport async function validateMediaDuration(file: File, maxDurationSec: number): Promise<boolean> {\n return new Promise((resolve) => {\n const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');\n el.preload = 'metadata';\n\n let settled = false;\n const finish = (ok: boolean) => {\n if (settled) return;\n settled = true;\n try {\n URL.revokeObjectURL(el.src);\n } catch {}\n resolve(ok);\n };\n\n el.onloadedmetadata = () => {\n if (Number.isFinite(el.duration)) {\n finish(el.duration <= maxDurationSec);\n return;\n }\n // Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround\n // learns the real one.\n void resolveInfiniteDuration(el).then((duration) => {\n finish(duration === null ? true : duration <= maxDurationSec);\n });\n };\n\n el.onerror = () => {\n finish(true);\n };\n\n el.src = URL.createObjectURL(file);\n });\n}\n"]}
@@ -1,17 +1,7 @@
1
1
  type SingleKind = 'image' | 'video' | 'audio';
2
2
  export type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];
3
- /**
4
- * Diagnostic codes emitted by validateAndNormalizeUploadFile when validation
5
- * fails. Consumers can route these to monitoring/Sentry via the onError option.
6
- */
7
3
  export type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';
8
4
  export interface ValidateUploadFileOptions {
9
- /**
10
- * Optional diagnostic callback fired before the function throws.
11
- * Useful for routing to Sentry / monitoring-core. The thrown Error
12
- * carries the user-facing message; this callback gets the machine
13
- * code + structured extras.
14
- */
15
5
  onError?: (code: ValidateUploadFileErrorCode, extra: {
16
6
  fileOrigin: string;
17
7
  expectedKind: string;
@@ -20,32 +10,8 @@ export interface ValidateUploadFileOptions {
20
10
  fileSize: number;
21
11
  fileType: string;
22
12
  }) => void;
23
- /**
24
- * FAIL-OPEN for indeterminate picker metadata: when the file's kind cannot
25
- * be determined from browser MIME/extension, pass it through with the
26
- * NEUTRAL `application/octet-stream` type (never a fabricated media type)
27
- * instead of throwing, so the server's canonical byte inspector — the only
28
- * classification authority — decides. The kind-mismatch check still throws
29
- * when the client metadata IS determinate. Requires the upload path to opt
30
- * into `allowNeutralContentType`.
31
- */
32
13
  allowIndeterminate?: boolean;
33
14
  }
34
- /**
35
- * Validates and normalizes a file for upload.
36
- *
37
- * - Throws a user-friendly Error if the file's kind cannot be determined
38
- * (unless `allowIndeterminate` fails open to the neutral content type).
39
- * - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't
40
- * match the detected kind.
41
- * - Otherwise returns a File with a guaranteed valid media contentType.
42
- *
43
- * Optional `onError` callback fires with a machine-readable code + structured
44
- * extras before the throw, so consumers can route to monitoring without this
45
- * package depending on any monitoring library.
46
- *
47
- * Use this at the top of every upload mutation, before uploadFileResumable.
48
- */
49
15
  export declare function validateAndNormalizeUploadFile(file: File, fileOrigin: string, expectedKind: ExpectedKind, options?: ValidateUploadFileOptions): File;
50
16
  export {};
51
17
  //# sourceMappingURL=validate-upload-kind.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,2BAA2B,GACnC,2BAA2B,GAC3B,sBAAsB,CAAC;AAE3B,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,2BAA2B,EAAE,KAAK,EAAE;QACnD,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,IAAI,CAAC;IACX;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAcD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAqCN"}
1
+ {"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE,MAAM,MAAM,2BAA2B,GAAG,2BAA2B,GAAG,sBAAsB,CAAC;AAE/F,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,2BAA2B,EACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,KACE,IAAI,CAAC;IACV,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAcD,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAmCN"}