@de-otio/trellis 0.11.0 → 0.12.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 (123) hide show
  1. package/dist/env.d.ts +168 -0
  2. package/dist/env.d.ts.map +1 -1
  3. package/dist/env.js +155 -0
  4. package/dist/env.js.map +1 -1
  5. package/dist/lambda/media-completion-worker.d.ts +175 -0
  6. package/dist/lambda/media-completion-worker.d.ts.map +1 -0
  7. package/dist/lambda/media-completion-worker.js +373 -0
  8. package/dist/lambda/media-completion-worker.js.map +1 -0
  9. package/dist/lambda/media-processing-worker.d.ts +172 -1
  10. package/dist/lambda/media-processing-worker.d.ts.map +1 -1
  11. package/dist/lambda/media-processing-worker.js +343 -49
  12. package/dist/lambda/media-processing-worker.js.map +1 -1
  13. package/dist/lib/exif-stripper.d.ts +37 -22
  14. package/dist/lib/exif-stripper.d.ts.map +1 -1
  15. package/dist/lib/exif-stripper.js +101 -41
  16. package/dist/lib/exif-stripper.js.map +1 -1
  17. package/dist/lib/media/cas-keys.d.ts +63 -0
  18. package/dist/lib/media/cas-keys.d.ts.map +1 -0
  19. package/dist/lib/media/cas-keys.js +102 -0
  20. package/dist/lib/media/cas-keys.js.map +1 -0
  21. package/dist/lib/media/classify-worker-error.d.ts +48 -0
  22. package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
  23. package/dist/lib/media/classify-worker-error.js +319 -0
  24. package/dist/lib/media/classify-worker-error.js.map +1 -0
  25. package/dist/lib/media/dedupe-key.d.ts +29 -0
  26. package/dist/lib/media/dedupe-key.d.ts.map +1 -0
  27. package/dist/lib/media/dedupe-key.js +49 -0
  28. package/dist/lib/media/dedupe-key.js.map +1 -0
  29. package/dist/lib/media/duration-cap.d.ts +30 -0
  30. package/dist/lib/media/duration-cap.d.ts.map +1 -0
  31. package/dist/lib/media/duration-cap.js +37 -0
  32. package/dist/lib/media/duration-cap.js.map +1 -0
  33. package/dist/lib/media/ffmpeg-args.d.ts +83 -0
  34. package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
  35. package/dist/lib/media/ffmpeg-args.js +119 -0
  36. package/dist/lib/media/ffmpeg-args.js.map +1 -0
  37. package/dist/lib/media/media-ports.d.ts +126 -0
  38. package/dist/lib/media/media-ports.d.ts.map +1 -0
  39. package/dist/lib/media/media-ports.js +129 -0
  40. package/dist/lib/media/media-ports.js.map +1 -0
  41. package/dist/lib/media/media-upsert.d.ts +55 -0
  42. package/dist/lib/media/media-upsert.d.ts.map +1 -0
  43. package/dist/lib/media/media-upsert.js +38 -0
  44. package/dist/lib/media/media-upsert.js.map +1 -0
  45. package/dist/lib/media/moderation-provider.d.ts +111 -0
  46. package/dist/lib/media/moderation-provider.d.ts.map +1 -0
  47. package/dist/lib/media/moderation-provider.js +130 -0
  48. package/dist/lib/media/moderation-provider.js.map +1 -0
  49. package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
  50. package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
  51. package/dist/lib/media/moderation-resolved-payload.js +37 -0
  52. package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
  53. package/dist/lib/media/moderation-status.d.ts +98 -0
  54. package/dist/lib/media/moderation-status.d.ts.map +1 -0
  55. package/dist/lib/media/moderation-status.js +122 -0
  56. package/dist/lib/media/moderation-status.js.map +1 -0
  57. package/dist/lib/media/processing-types.d.ts +45 -0
  58. package/dist/lib/media/processing-types.d.ts.map +1 -0
  59. package/dist/lib/media/processing-types.js +9 -0
  60. package/dist/lib/media/processing-types.js.map +1 -0
  61. package/dist/lib/media/promote-decision.d.ts +64 -0
  62. package/dist/lib/media/promote-decision.d.ts.map +1 -0
  63. package/dist/lib/media/promote-decision.js +76 -0
  64. package/dist/lib/media/promote-decision.js.map +1 -0
  65. package/dist/lib/media/quota-check.d.ts +22 -0
  66. package/dist/lib/media/quota-check.d.ts.map +1 -0
  67. package/dist/lib/media/quota-check.js +42 -0
  68. package/dist/lib/media/quota-check.js.map +1 -0
  69. package/dist/lib/media/quota-types.d.ts +15 -0
  70. package/dist/lib/media/quota-types.d.ts.map +1 -0
  71. package/dist/lib/media/quota-types.js +9 -0
  72. package/dist/lib/media/quota-types.js.map +1 -0
  73. package/dist/lib/media/route-upload.d.ts +58 -0
  74. package/dist/lib/media/route-upload.d.ts.map +1 -0
  75. package/dist/lib/media/route-upload.js +80 -0
  76. package/dist/lib/media/route-upload.js.map +1 -0
  77. package/dist/lib/media/serve-gate.d.ts +51 -0
  78. package/dist/lib/media/serve-gate.d.ts.map +1 -0
  79. package/dist/lib/media/serve-gate.js +68 -0
  80. package/dist/lib/media/serve-gate.js.map +1 -0
  81. package/dist/lib/media/tenant-resolution.d.ts +42 -0
  82. package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
  83. package/dist/lib/media/tenant-resolution.js +45 -0
  84. package/dist/lib/media/tenant-resolution.js.map +1 -0
  85. package/dist/lib/media/text-moderation.d.ts +28 -0
  86. package/dist/lib/media/text-moderation.d.ts.map +1 -0
  87. package/dist/lib/media/text-moderation.js +62 -0
  88. package/dist/lib/media/text-moderation.js.map +1 -0
  89. package/dist/lib/media/track-verdict.d.ts +45 -0
  90. package/dist/lib/media/track-verdict.d.ts.map +1 -0
  91. package/dist/lib/media/track-verdict.js +52 -0
  92. package/dist/lib/media/track-verdict.js.map +1 -0
  93. package/dist/lib/media/transcript-moderation.d.ts +47 -0
  94. package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
  95. package/dist/lib/media/transcript-moderation.js +70 -0
  96. package/dist/lib/media/transcript-moderation.js.map +1 -0
  97. package/dist/lib/media-handler.d.ts.map +1 -1
  98. package/dist/lib/media-handler.js +15 -9
  99. package/dist/lib/media-handler.js.map +1 -1
  100. package/dist/lib/post-handler.d.ts.map +1 -1
  101. package/dist/lib/post-handler.js +4 -1
  102. package/dist/lib/post-handler.js.map +1 -1
  103. package/dist/lib/routes/media.d.ts +21 -0
  104. package/dist/lib/routes/media.d.ts.map +1 -1
  105. package/dist/lib/routes/media.js +584 -483
  106. package/dist/lib/routes/media.js.map +1 -1
  107. package/dist/lib/services/image-normalizer.d.ts +64 -6
  108. package/dist/lib/services/image-normalizer.d.ts.map +1 -1
  109. package/dist/lib/services/image-normalizer.js +88 -6
  110. package/dist/lib/services/image-normalizer.js.map +1 -1
  111. package/dist/lib/services/media-upload-service.d.ts +2 -2
  112. package/dist/lib/services/media-upload-service.d.ts.map +1 -1
  113. package/dist/lib/services/media-upload-service.js +22 -21
  114. package/dist/lib/services/media-upload-service.js.map +1 -1
  115. package/dist/lib/tenant-scope.d.ts.map +1 -1
  116. package/dist/lib/tenant-scope.js +16 -1
  117. package/dist/lib/tenant-scope.js.map +1 -1
  118. package/package.json +2 -1
  119. package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
  120. package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
  121. package/prisma/schema.prisma +95 -17
  122. package/src/lambda/media-completion-worker.ts +567 -0
  123. package/src/lambda/media-processing-worker.ts +508 -59
@@ -1,59 +1,119 @@
1
1
  /**
2
- * EXIF Stripping Utility
2
+ * EXIF strip verification helper (T6).
3
3
  *
4
- * PREPARATORY CHANGE: This utility is created now but not yet used in production.
5
- * It will be enabled when implementing location safety features for at-risk users.
4
+ * The byte-level strip is a CONSEQUENCE of T7's re-encode: sharp re-encoding
5
+ * without `.withMetadata()` drops all embedded metadata (EXIF, GPS, ICC,
6
+ * maker-notes). This module provides the POST-encode assertion that the strip
7
+ * actually happened — used defensively at runtime and as the verification
8
+ * contract in tests.
6
9
  *
7
- * Removes EXIF data from images for privacy.
8
- * Can be enabled/disabled via configuration.
10
+ * The old placeholder `stripEXIF()` is preserved below with a narrowed
11
+ * signature so existing callers compile; it delegates to the re-encode and is
12
+ * deprecated. New code must call `assertNoExif` after `reencodeImage`.
9
13
  *
10
- * FUTURE USE:
11
- * - When EXIF_STRIPPING_ENABLED=true, this utility will strip EXIF data from
12
- * uploaded images before storing them
13
- * - Prevents location data, device info, and timestamps from being embedded in images
14
- * - Helps protect user privacy, especially for at-risk users
14
+ * GPS coordinates are NOT persisted: `gpsLatitude`/`gpsLongitude` columns
15
+ * were removed in T8's schema migration. Nothing in this file or the upload
16
+ * handler writes those fields.
15
17
  */
18
+ import exifr from "exifr";
16
19
  /**
17
- * Strip EXIF data from image buffer
20
+ * Parse the EXIF/IPTC/GPS/ICC metadata embedded in `imageBytes` and return
21
+ * it as a flat object. Returns `undefined` when exifr finds nothing.
18
22
  *
19
- * FUTURE USE: This function will be called during media upload processing
20
- * to remove EXIF data from images before storing them in R2.
23
+ * Exported for use in tests (assert the object is empty/undefined after
24
+ * re-encode) and as an optional runtime defensive check.
25
+ */
26
+ export async function parseMetadata(imageBytes) {
27
+ const buf = imageBytes instanceof Buffer
28
+ ? imageBytes
29
+ : Buffer.from(imageBytes instanceof ArrayBuffer
30
+ ? imageBytes
31
+ : imageBytes.buffer);
32
+ try {
33
+ // Parse all segments: EXIF (including GPS), IPTC, ICC, XMP.
34
+ const parsed = await exifr.parse(buf, {
35
+ gps: true,
36
+ icc: true,
37
+ iptc: true,
38
+ xmp: true,
39
+ makerNote: true,
40
+ userComment: true,
41
+ });
42
+ return parsed;
43
+ }
44
+ catch {
45
+ // If exifr cannot parse the buffer it throws (unknown format, truncated
46
+ // data, etc.). Treat as "no metadata found" — the bytes are already
47
+ // re-encoded clean raster pixels.
48
+ return undefined;
49
+ }
50
+ }
51
+ /**
52
+ * Keys that are considered benign structural PNG chunk fields — not EXIF or
53
+ * privacy-sensitive metadata. exifr parses these from the PNG IHDR/iCCP chunks,
54
+ * but they are format metadata, not user metadata.
55
+ */
56
+ const PNG_STRUCTURAL_KEYS = new Set([
57
+ "ImageWidth",
58
+ "ImageHeight",
59
+ "BitDepth",
60
+ "ColorType",
61
+ "Compression",
62
+ "Filter",
63
+ "Interlace",
64
+ ]);
65
+ /**
66
+ * Privacy-sensitive metadata keys that must NOT appear in re-encoded output.
67
+ * Any of these keys present after re-encode indicates `.withMetadata()` was
68
+ * mistakenly added or sharp is re-adding metadata.
69
+ */
70
+ const SENSITIVE_EXIF_PREFIXES = [
71
+ "GPS", // GPS coordinates
72
+ "Make", // camera maker (maker-notes gateway)
73
+ "Model", // camera model
74
+ "Software",
75
+ "DateTime",
76
+ "Orientation",
77
+ "ExifIFD",
78
+ "InteropIFD",
79
+ ];
80
+ /**
81
+ * Assert that `imageBytes` contains NO privacy-sensitive embedded metadata
82
+ * (EXIF GPS, ICC, maker-notes, camera info). Benign PNG structural fields
83
+ * (ImageWidth, ColorType, etc.) are excluded from this check since exifr
84
+ * parses them from the PNG format header, not from EXIF APP1 segments.
21
85
  *
22
- * NOTE: This is currently a placeholder implementation.
23
- * When implementing, use a library like 'piexifjs' or 'exifr' to actually
24
- * remove EXIF data from the image buffer.
86
+ * Call this on the OUTPUT of `reencodeImage` to confirm the re-encode dropped
87
+ * all user/device metadata. Useful both in tests and as an optional defensive
88
+ * runtime check.
25
89
  *
26
- * @param imageBuffer - Image buffer to process
27
- * @param config - Stripping configuration
28
- * @returns Processed image buffer (with EXIF removed if enabled)
90
+ * @throws `Error` when privacy-sensitive metadata is present.
91
+ */
92
+ export async function assertNoExif(imageBytes) {
93
+ const metadata = await parseMetadata(imageBytes);
94
+ if (metadata === undefined)
95
+ return;
96
+ const sensitiveKeys = Object.keys(metadata).filter((k) => !PNG_STRUCTURAL_KEYS.has(k) &&
97
+ SENSITIVE_EXIF_PREFIXES.some((prefix) => k.startsWith(prefix)));
98
+ if (sensitiveKeys.length > 0) {
99
+ throw new Error(`Re-encoded image still contains privacy-sensitive metadata (keys: ${sensitiveKeys.join(", ")}). ` +
100
+ "Ensure reencodeImage is called without .withMetadata().");
101
+ }
102
+ }
103
+ /**
104
+ * @deprecated The strip now happens as a side-effect of `reencodeImage`
105
+ * (T7). This function is a no-op passthrough kept solely so the existing
106
+ * test suite compiles without change. Do not call in new code.
29
107
  */
30
108
  export async function stripEXIF(imageBuffer, config = {
31
109
  enabled: true,
32
110
  removeLocation: true,
33
111
  removeDeviceInfo: true,
34
- removeTimestamp: false, // Keep timestamp for now (can be enabled later)
112
+ removeTimestamp: false,
35
113
  }) {
36
- if (!config.enabled) {
37
- return imageBuffer;
38
- }
39
- // TODO: Implement actual EXIF stripping using a library
40
- // FUTURE IMPLEMENTATION:
41
- // 1. Install library: npm install piexifjs or npm install exifr
42
- // 2. Parse EXIF data from image buffer
43
- // 3. Remove location data if removeLocation=true
44
- // 4. Remove device info if removeDeviceInfo=true
45
- // 5. Remove timestamp if removeTimestamp=true
46
- // 6. Reconstruct image buffer without EXIF data
47
- //
48
- // Example with piexifjs:
49
- // import piexif from 'piexifjs';
50
- // const exifObj = piexif.load(imageBuffer);
51
- // if (config.removeLocation) delete exifObj['GPS'];
52
- // if (config.removeDeviceInfo) delete exifObj['0th'][piexif.ImageIFD.Make];
53
- // const newBuffer = piexif.dump(exifObj);
54
- // return newBuffer;
55
- // For now, return buffer as-is (no breaking changes)
56
- // This allows the utility to exist and be called without affecting current behavior
114
+ // Pass-through: the strip is done by reencodeImage (T7). The config
115
+ // parameter is accepted to keep the call-site signature stable.
116
+ void config;
57
117
  return imageBuffer;
58
118
  }
59
119
  //# sourceMappingURL=exif-stripper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exif-stripper.js","sourceRoot":"","sources":["../../src/lib/exif-stripper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,WAAwB,EACxB,SAA6B;IAC3B,OAAO,EAAE,IAAI;IACb,cAAc,EAAE,IAAI;IACpB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,KAAK,EAAE,gDAAgD;CACzE;IAED,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,wDAAwD;IACxD,yBAAyB;IACzB,gEAAgE;IAChE,uCAAuC;IACvC,iDAAiD;IACjD,iDAAiD;IACjD,8CAA8C;IAC9C,gDAAgD;IAChD,EAAE;IACF,yBAAyB;IACzB,iCAAiC;IACjC,4CAA4C;IAC5C,oDAAoD;IACpD,4EAA4E;IAC5E,0CAA0C;IAC1C,oBAAoB;IAEpB,qDAAqD;IACrD,oFAAoF;IACpF,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"exif-stripper.js","sourceRoot":"","sources":["../../src/lib/exif-stripper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAA6C;IAE7C,MAAM,GAAG,GACP,UAAU,YAAY,MAAM;QAC1B,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,MAAM,CAAC,IAAI,CACT,UAAU,YAAY,WAAW;YAC/B,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,UAAU,CAAC,MAAM,CACtB,CAAC;IAER,IAAI,CAAC;QACH,4DAA4D;QAC5D,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;YACpC,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,IAAI;YACT,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,OAAO,MAA6C,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,oEAAoE;QACpE,kCAAkC;QAClC,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,YAAY;IACZ,aAAa;IACb,UAAU;IACV,WAAW;IACX,aAAa;IACb,QAAQ;IACR,WAAW;CACZ,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,uBAAuB,GAAG;IAC9B,KAAK,EAAK,kBAAkB;IAC5B,MAAM,EAAI,qCAAqC;IAC/C,OAAO,EAAG,eAAe;IACzB,UAAU;IACV,UAAU;IACV,aAAa;IACb,SAAS;IACT,YAAY;CACb,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,UAA6C;IAE7C,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,UAAU,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO;IAEnC,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAChD,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3B,uBAAuB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CACjE,CAAC;IAEF,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,qEAAqE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAChG,yDAAyD,CAC5D,CAAC;IACJ,CAAC;AACH,CAAC;AAgBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,WAAwB,EACxB,SAA6B;IAC3B,OAAO,EAAE,IAAI;IACb,cAAc,EAAE,IAAI;IACpB,gBAAgB,EAAE,IAAI;IACtB,eAAe,EAAE,KAAK;CACvB;IAED,oEAAoE;IACpE,gEAAgE;IAChE,KAAK,MAAM,CAAC;IACZ,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Canonical tenant-scoped CAS key builder.
3
+ *
4
+ * Provides the ONE canonical key scheme (D18) for content-addressed storage,
5
+ * replacing multiple conflicting schemes. All storage paths MUST go through
6
+ * these builders — never hand-roll a key string elsewhere.
7
+ *
8
+ * Keys are pure functions: no I/O, no clock, no external dependencies.
9
+ */
10
+ /**
11
+ * Allowed derivative presets for a CAS object.
12
+ * Only values in this union are accepted by casKey(); all other strings are rejected.
13
+ */
14
+ export type CasPreset = "thumbnail" | "optimized";
15
+ export type CasKeyError = {
16
+ kind: "invalid_hash";
17
+ raw: string;
18
+ } | {
19
+ kind: "invalid_tenant_id";
20
+ raw: string;
21
+ } | {
22
+ kind: "invalid_upload_id";
23
+ raw: string;
24
+ } | {
25
+ kind: "invalid_preset";
26
+ raw: string;
27
+ };
28
+ /**
29
+ * Validate and lowercase-normalize an inbound content hash.
30
+ *
31
+ * Used at BOTH the upload boundary AND the serve entry point before any key
32
+ * construction or DB lookup. Lowercase normalization prevents a split CAS
33
+ * keyspace (two keys for the same bytes) caused by mixed-case hex.
34
+ *
35
+ * @returns The lowercase-normalized 64-char hex string, or a typed error.
36
+ */
37
+ export declare function validateContentHash(raw: string): string | CasKeyError;
38
+ /**
39
+ * Build the canonical CAS storage key for a tenant-scoped object.
40
+ *
41
+ * Overload without preset: `cas/{tenantId}/{sha256}`
42
+ * Overload with preset: `cas/{tenantId}/{sha256}/{preset}`
43
+ *
44
+ * All inputs are validated against anchored allowlist regexes. The hash is
45
+ * always lowercase-normalized so that the CAS keyspace is consistent.
46
+ *
47
+ * @returns The key string, or a typed error describing which input was invalid.
48
+ */
49
+ export declare function casKey(tenantId: string, sha256: string): string | CasKeyError;
50
+ export declare function casKey(tenantId: string, sha256: string, preset: CasPreset): string | CasKeyError;
51
+ /**
52
+ * Build the pending-upload staging key for a tenant-scoped upload.
53
+ *
54
+ * `pending/{tenantId}/{uploadId}`
55
+ *
56
+ * @returns The key string, or a typed error describing which input was invalid.
57
+ */
58
+ export declare function pendingKey(tenantId: string, uploadId: string): string | CasKeyError;
59
+ /** Returns true when the result is a CasKeyError (not a key string). */
60
+ export declare function isCasKeyError(result: string | CasKeyError): result is CasKeyError;
61
+ /** Returns the set of valid preset values (for exhaustiveness checks). */
62
+ export declare function allPresets(): readonly CasPreset[];
63
+ //# sourceMappingURL=cas-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cas-keys.d.ts","sourceRoot":"","sources":["../../../src/lib/media/cas-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAsClD,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAM5C;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,WAAW,CAMtB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,WAAW,CAAC;AACxB,wBAAgB,MAAM,CACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,GAChB,MAAM,GAAG,WAAW,CAAC;AA6BxB;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,WAAW,CAUtB;AAMD,wEAAwE;AACxE,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,MAAM,IAAI,WAAW,CAEvB;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,SAAS,SAAS,EAAE,CAEjD"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Canonical tenant-scoped CAS key builder.
3
+ *
4
+ * Provides the ONE canonical key scheme (D18) for content-addressed storage,
5
+ * replacing multiple conflicting schemes. All storage paths MUST go through
6
+ * these builders — never hand-roll a key string elsewhere.
7
+ *
8
+ * Keys are pure functions: no I/O, no clock, no external dependencies.
9
+ */
10
+ const CAS_PRESETS = new Set([
11
+ "thumbnail",
12
+ "optimized",
13
+ ]);
14
+ // ---------------------------------------------------------------------------
15
+ // Validation regexes (anchored allowlists — NOT blocklists)
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * SHA-256 hex digest: exactly 64 lowercase hex characters.
19
+ * Anchored so that path-traversal payloads, encoded dots, or extra characters
20
+ * never pass validation.
21
+ */
22
+ const SHA256_RE = /^[0-9a-f]{64}$/;
23
+ /**
24
+ * CUID v1 format as generated by Prisma's @default(cuid()).
25
+ * Format: 'c' followed by 24 lowercase alphanumeric characters (base-36 alphabet).
26
+ * Total length: 25 characters.
27
+ * See prisma/schema.prisma (id @default(cuid()) fields).
28
+ *
29
+ * Anchored to prevent traversal/injection: only [a-z0-9] within a fixed length band.
30
+ */
31
+ const TENANT_ID_RE = /^c[a-z0-9]{24}$/;
32
+ /**
33
+ * Upload session / pending key ID: we accept the same CUID format.
34
+ * Prisma UploadSession.id is also @default(cuid()).
35
+ */
36
+ const UPLOAD_ID_RE = /^c[a-z0-9]{24}$/;
37
+ // ---------------------------------------------------------------------------
38
+ // validateContentHash
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Validate and lowercase-normalize an inbound content hash.
42
+ *
43
+ * Used at BOTH the upload boundary AND the serve entry point before any key
44
+ * construction or DB lookup. Lowercase normalization prevents a split CAS
45
+ * keyspace (two keys for the same bytes) caused by mixed-case hex.
46
+ *
47
+ * @returns The lowercase-normalized 64-char hex string, or a typed error.
48
+ */
49
+ export function validateContentHash(raw) {
50
+ const normalized = raw.toLowerCase();
51
+ if (!SHA256_RE.test(normalized)) {
52
+ return { kind: "invalid_hash", raw };
53
+ }
54
+ return normalized;
55
+ }
56
+ export function casKey(tenantId, sha256, preset) {
57
+ if (!TENANT_ID_RE.test(tenantId)) {
58
+ return { kind: "invalid_tenant_id", raw: tenantId };
59
+ }
60
+ const normalized = sha256.toLowerCase();
61
+ if (!SHA256_RE.test(normalized)) {
62
+ return { kind: "invalid_hash", raw: sha256 };
63
+ }
64
+ if (preset !== undefined) {
65
+ if (!CAS_PRESETS.has(preset)) {
66
+ return { kind: "invalid_preset", raw: preset };
67
+ }
68
+ return `cas/${tenantId}/${normalized}/${preset}`;
69
+ }
70
+ return `cas/${tenantId}/${normalized}`;
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // pendingKey
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Build the pending-upload staging key for a tenant-scoped upload.
77
+ *
78
+ * `pending/{tenantId}/{uploadId}`
79
+ *
80
+ * @returns The key string, or a typed error describing which input was invalid.
81
+ */
82
+ export function pendingKey(tenantId, uploadId) {
83
+ if (!TENANT_ID_RE.test(tenantId)) {
84
+ return { kind: "invalid_tenant_id", raw: tenantId };
85
+ }
86
+ if (!UPLOAD_ID_RE.test(uploadId)) {
87
+ return { kind: "invalid_upload_id", raw: uploadId };
88
+ }
89
+ return `pending/${tenantId}/${uploadId}`;
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Type guard helpers
93
+ // ---------------------------------------------------------------------------
94
+ /** Returns true when the result is a CasKeyError (not a key string). */
95
+ export function isCasKeyError(result) {
96
+ return typeof result === "object";
97
+ }
98
+ /** Returns the set of valid preset values (for exhaustiveness checks). */
99
+ export function allPresets() {
100
+ return ["thumbnail", "optimized"];
101
+ }
102
+ //# sourceMappingURL=cas-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cas-keys.js","sourceRoot":"","sources":["../../../src/lib/media/cas-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAY;IAC1D,WAAW;IACX,WAAW;CACZ,CAAC,CAAC;AAEH,8EAA8E;AAC9E,4DAA4D;AAC5D,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC;;;GAGG;AACH,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAYvC,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAW;IAEX,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC;IACvC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AA0BD,MAAM,UAAU,MAAM,CACpB,QAAgB,EAChB,MAAc,EACd,MAAkB;IAElB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC/C,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QACjD,CAAC;QACD,OAAO,OAAO,QAAQ,IAAI,UAAU,IAAI,MAAM,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,OAAO,QAAQ,IAAI,UAAU,EAAE,CAAC;AACzC,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,QAAgB,EAChB,QAAgB;IAEhB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IACtD,CAAC;IAED,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IACtD,CAAC;IAED,OAAO,WAAW,QAAQ,IAAI,QAAQ,EAAE,CAAC;AAC3C,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,UAAU,aAAa,CAC3B,MAA4B;IAE5B,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC;AACpC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU;IACxB,OAAO,CAAC,WAAW,EAAE,WAAW,CAAU,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * classify-worker-error.ts — pure functional core, no I/O, no AWS SDK.
3
+ *
4
+ * Classifies any error thrown by a media-processing worker into one of two
5
+ * treatment buckets:
6
+ *
7
+ * "poison" — the media itself (or the job payload) is bad. A retry cannot
8
+ * help: the same bytes will fail the same way. The caller must
9
+ * ACK the message and move the object to moderationStatus=REVIEW
10
+ * for human triage, avoiding an infinite DLQ loop.
11
+ *
12
+ * "retryable" — a transient infrastructure fault: throttling, timeout, 5xx
13
+ * upstream, network blip, eventual-consistency AccessDenied.
14
+ * The caller lets SQS retry naturally; the 3-strike DLQ + alert
15
+ * acts as the upper bound.
16
+ *
17
+ * DEFAULT RULE (documented here — the only operative "fallback threshold"):
18
+ * When the error is unknown or cannot be classified with confidence, the
19
+ * function returns "retryable". Rationale: an unrecognised error is more
20
+ * likely to be a transient infra fault than a permanent media defect, and the
21
+ * DLQ + 3-strike alert provides a bounded safety net. Silently sending
22
+ * ambiguous errors to REVIEW would suppress alerts and hide systemic infra
23
+ * problems. This default is deliberately fail-OPEN for retry but
24
+ * fail-CLOSED for approval — REVIEW is never returned from this module; the
25
+ * caller owns the status mapping.
26
+ *
27
+ * PURITY GUARANTEE: this module has no I/O, no AWS SDK dependency, no
28
+ * Date.now/Math.random. node:crypto is NOT used. All inputs produce
29
+ * deterministic outputs.
30
+ */
31
+ /**
32
+ * The two exclusive classifications.
33
+ *
34
+ * - `"poison"` — stop retrying; ACK + route to REVIEW.
35
+ * - `"retryable"` — let SQS retry; DLQ + alert is the backstop.
36
+ */
37
+ export type ErrorClass = "poison" | "retryable";
38
+ /**
39
+ * Classifies `err` as `"poison"` or `"retryable"`.
40
+ *
41
+ * - TOTAL: never throws for any input (including null, undefined, non-Error objects).
42
+ * - FAIL-CLOSED on unknown: returns `"retryable"` when the error cannot be
43
+ * classified with confidence (DLQ + alert is the bounded backstop).
44
+ * - Poison wins over retryable when signals conflict (conservative for serving
45
+ * safety: a bad payload that also triggers throttle is still bad).
46
+ */
47
+ export declare function classifyWorkerError(err: unknown): ErrorClass;
48
+ //# sourceMappingURL=classify-worker-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify-worker-error.d.ts","sourceRoot":"","sources":["../../../src/lib/media/classify-worker-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;AAgLhD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CA+B5D"}