@de-otio/trellis 0.11.0 → 0.12.1

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 (126) 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/route-helpers.d.ts.map +1 -1
  104. package/dist/lib/route-helpers.js +9 -1
  105. package/dist/lib/route-helpers.js.map +1 -1
  106. package/dist/lib/routes/media.d.ts +21 -0
  107. package/dist/lib/routes/media.d.ts.map +1 -1
  108. package/dist/lib/routes/media.js +584 -483
  109. package/dist/lib/routes/media.js.map +1 -1
  110. package/dist/lib/services/image-normalizer.d.ts +64 -6
  111. package/dist/lib/services/image-normalizer.d.ts.map +1 -1
  112. package/dist/lib/services/image-normalizer.js +88 -6
  113. package/dist/lib/services/image-normalizer.js.map +1 -1
  114. package/dist/lib/services/media-upload-service.d.ts +2 -2
  115. package/dist/lib/services/media-upload-service.d.ts.map +1 -1
  116. package/dist/lib/services/media-upload-service.js +22 -21
  117. package/dist/lib/services/media-upload-service.js.map +1 -1
  118. package/dist/lib/tenant-scope.d.ts.map +1 -1
  119. package/dist/lib/tenant-scope.js +16 -1
  120. package/dist/lib/tenant-scope.js.map +1 -1
  121. package/package.json +2 -1
  122. package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
  123. package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
  124. package/prisma/schema.prisma +95 -17
  125. package/src/lambda/media-completion-worker.ts +567 -0
  126. package/src/lambda/media-processing-worker.ts +508 -59
@@ -0,0 +1,76 @@
1
+ // CONTRACT: stable — coordinate changes. C4 pure functional-core unit.
2
+ //
3
+ // Decide what the moderation worker SHOULD do once both per-track outcomes for
4
+ // a media object are known: combine the tracks into one object-level decision,
5
+ // run that decision through the lifecycle state machine, and report — as plain
6
+ // booleans — which side effects the imperative shell should perform.
7
+ //
8
+ // This function performs NO I/O. It only *reports* intent. The shell applies
9
+ // the reported actions in a fixed, safety-ordered sequence:
10
+ //
11
+ // 1. promote (publish/adopt the CAS object so bytes can be served)
12
+ // 2. persist (write the new moderationStatus)
13
+ // 3. emit (publish the "resolved" moderation event)
14
+ //
15
+ // FAIL-CLOSED is the whole point. An illegal transition (e.g. a replayed
16
+ // decision on an already-terminal APPROVED/REJECTED object) is an idempotent
17
+ // no-op: every action boolean is false, so the shell touches nothing. And
18
+ // `shouldPromote` is true ONLY when the combined decision drove the object to
19
+ // APPROVED *and* the CAS object is actually present — doubt never serves.
20
+ //
21
+ // Pure functional core: no I/O, no clock, no random. Total over its inputs.
22
+ // Lives in the PUBLIC npm tarball: NO thresholds, secrets, or real-category
23
+ // vocabulary here.
24
+ import { combineTrackVerdicts } from "./track-verdict.js";
25
+ import { nextStatus, } from "./moderation-status.js";
26
+ /**
27
+ * Decide the worker's action for a media object whose tracks have both resolved.
28
+ *
29
+ * Logic (total):
30
+ * 1. `combined = combineTrackVerdicts(visual, audio)` — fail-closed track join.
31
+ * 2. `transition = nextStatus(currentStatus, { kind: "decision", decision: combined })`.
32
+ * 3. If `transition.ok === false` (illegal — e.g. replay on a terminal status):
33
+ * idempotent NO-OP — `shouldPromote`/`shouldPersistStatus`/`shouldEmitResolved`
34
+ * are all `false`.
35
+ * 4. If `transition.ok === true`:
36
+ * - `shouldPersistStatus = true`
37
+ * - `shouldEmitResolved = true`
38
+ * - `shouldPromote = transition.status === "APPROVED" && casObjectPresent`
39
+ *
40
+ * Safety invariants (property-tested):
41
+ * - `shouldPromote === true` ⇒ BOTH tracks were decided-and-approved AND the
42
+ * resulting status is APPROVED. Promotion never happens from doubt.
43
+ * - APPROVED is never reached unless both tracks are decided-approved.
44
+ * - A replay/illegal transition on a terminal status yields an all-false no-op.
45
+ * - `casObjectPresent === false` ⇒ `shouldPromote === false`, even at APPROVED.
46
+ */
47
+ export function decidePromotion(input) {
48
+ const combined = combineTrackVerdicts(input.visual, input.audio);
49
+ const transition = nextStatus(input.currentStatus, {
50
+ kind: "decision",
51
+ decision: combined,
52
+ });
53
+ if (transition.ok === false) {
54
+ // Illegal transition (e.g. a replayed decision on an already-terminal
55
+ // APPROVED/REJECTED object). Idempotent no-op: touch nothing.
56
+ return {
57
+ combined,
58
+ transition,
59
+ shouldPromote: false,
60
+ shouldPersistStatus: false,
61
+ shouldEmitResolved: false,
62
+ };
63
+ }
64
+ // Legal transition: we will persist the new status and emit the resolved
65
+ // event. We only promote (publish bytes) when the object actually became
66
+ // APPROVED *and* the CAS object exists to serve.
67
+ const shouldPromote = transition.status === "APPROVED" && input.casObjectPresent;
68
+ return {
69
+ combined,
70
+ transition,
71
+ shouldPromote,
72
+ shouldPersistStatus: true,
73
+ shouldEmitResolved: true,
74
+ };
75
+ }
76
+ //# sourceMappingURL=promote-decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promote-decision.js","sourceRoot":"","sources":["../../../src/lib/media/promote-decision.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,qEAAqE;AACrE,EAAE;AACF,6EAA6E;AAC7E,4DAA4D;AAC5D,EAAE;AACF,sEAAsE;AACtE,iDAAiD;AACjD,0DAA0D;AAC1D,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,0EAA0E;AAC1E,8EAA8E;AAC9E,0EAA0E;AAC1E,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,mBAAmB;AAEnB,OAAO,EAAE,oBAAoB,EAAqB,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EACL,UAAU,GAIX,MAAM,wBAAwB,CAAC;AA2ChC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,KAAqB;IACnD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAEjE,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,aAAa,EAAE;QACjD,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,QAAQ;KACnB,CAAC,CAAC;IAEH,IAAI,UAAU,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC5B,sEAAsE;QACtE,8DAA8D;QAC9D,OAAO;YACL,QAAQ;YACR,UAAU;YACV,aAAa,EAAE,KAAK;YACpB,mBAAmB,EAAE,KAAK;YAC1B,kBAAkB,EAAE,KAAK;SAC1B,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,aAAa,GACjB,UAAU,CAAC,MAAM,KAAK,UAAU,IAAI,KAAK,CAAC,gBAAgB,CAAC;IAE7D,OAAO;QACL,QAAQ;QACR,UAAU;QACV,aAAa;QACb,mBAAmB,EAAE,IAAI;QACzB,kBAAkB,EAAE,IAAI;KACzB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { QuotaState, QuotaLimits } from "./quota-types.js";
2
+ export type QuotaDenialReason = "object-cap" | "byte-cap";
3
+ export interface QuotaCheckResult {
4
+ allowed: boolean;
5
+ reason?: QuotaDenialReason;
6
+ }
7
+ /**
8
+ * Determine whether an incoming upload is permitted under the tenant's quota.
9
+ *
10
+ * Decision logic (evaluated in order; FAIL-CLOSED throughout):
11
+ * 1. If any number argument is NaN or non-finite => denied (no reason tag, caller
12
+ * should treat this as a validation failure upstream).
13
+ * 2. If incomingBytes < 0 => denied (same: malformed input, not a quota case).
14
+ * 3. If currentObjects >= maxObjects => denied, reason "object-cap".
15
+ * 4. If currentBytes + incomingBytes > maxBytes => denied, reason "byte-cap".
16
+ * 5. Otherwise => allowed.
17
+ *
18
+ * Limits are ALWAYS supplied as arguments sourced from Env.media — this module
19
+ * never hard-codes operational parameters.
20
+ */
21
+ export declare function checkUploadQuota(state: QuotaState, incomingBytes: number, limits: QuotaLimits): QuotaCheckResult;
22
+ //# sourceMappingURL=quota-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-check.d.ts","sourceRoot":"","sources":["../../../src/lib/media/quota-check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,WAAW,GAClB,gBAAgB,CA+BlB"}
@@ -0,0 +1,42 @@
1
+ // Pure functional-core unit — no I/O, no AWS SDK, no network, no Date.now.
2
+ // FAIL-CLOSED: any input that cannot be safely reasoned about yields denied.
3
+ /**
4
+ * Determine whether an incoming upload is permitted under the tenant's quota.
5
+ *
6
+ * Decision logic (evaluated in order; FAIL-CLOSED throughout):
7
+ * 1. If any number argument is NaN or non-finite => denied (no reason tag, caller
8
+ * should treat this as a validation failure upstream).
9
+ * 2. If incomingBytes < 0 => denied (same: malformed input, not a quota case).
10
+ * 3. If currentObjects >= maxObjects => denied, reason "object-cap".
11
+ * 4. If currentBytes + incomingBytes > maxBytes => denied, reason "byte-cap".
12
+ * 5. Otherwise => allowed.
13
+ *
14
+ * Limits are ALWAYS supplied as arguments sourced from Env.media — this module
15
+ * never hard-codes operational parameters.
16
+ */
17
+ export function checkUploadQuota(state, incomingBytes, limits) {
18
+ const { currentObjects, currentBytes } = state;
19
+ const { maxObjects, maxBytes } = limits;
20
+ // FAIL-CLOSED: bad numbers => denied. Cover all inputs for completeness.
21
+ if (!Number.isFinite(currentObjects) ||
22
+ !Number.isFinite(currentBytes) ||
23
+ !Number.isFinite(incomingBytes) ||
24
+ !Number.isFinite(maxObjects) ||
25
+ !Number.isFinite(maxBytes)) {
26
+ return { allowed: false };
27
+ }
28
+ // FAIL-CLOSED: negative incomingBytes is nonsensical => denied.
29
+ if (incomingBytes < 0) {
30
+ return { allowed: false };
31
+ }
32
+ // Object cap is checked before byte cap (object cap takes priority in ordering).
33
+ if (currentObjects >= maxObjects) {
34
+ return { allowed: false, reason: "object-cap" };
35
+ }
36
+ // Byte cap: currentBytes + incomingBytes must be <= maxBytes.
37
+ if (currentBytes + incomingBytes > maxBytes) {
38
+ return { allowed: false, reason: "byte-cap" };
39
+ }
40
+ return { allowed: true };
41
+ }
42
+ //# sourceMappingURL=quota-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-check.js","sourceRoot":"","sources":["../../../src/lib/media/quota-check.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,6EAA6E;AAW7E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAiB,EACjB,aAAqB,EACrB,MAAmB;IAEnB,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IAC/C,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAExC,yEAAyE;IACzE,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;QAChC,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9B,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC/B,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;QAC5B,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAC1B,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,gEAAgE;IAChE,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,iFAAiF;IACjF,IAAI,cAAc,IAAI,UAAU,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAClD,CAAC;IAED,8DAA8D;IAC9D,IAAI,YAAY,GAAG,aAAa,GAAG,QAAQ,EAAE,CAAC;QAC5C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAChD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,15 @@
1
+ /** A tenant's current measured usage of the media store. */
2
+ export interface QuotaState {
3
+ /** Number of objects currently counted against the tenant's quota. */
4
+ readonly currentObjects: number;
5
+ /** Total bytes currently counted against the tenant's quota. */
6
+ readonly currentBytes: number;
7
+ }
8
+ /** The tenant's quota ceilings. Injected from Env.media — never literals. */
9
+ export interface QuotaLimits {
10
+ /** Maximum number of objects the tenant may store. */
11
+ readonly maxObjects: number;
12
+ /** Maximum total bytes the tenant may store. */
13
+ readonly maxBytes: number;
14
+ }
15
+ //# sourceMappingURL=quota-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-types.d.ts","sourceRoot":"","sources":["../../../src/lib/media/quota-types.ts"],"names":[],"mappings":"AAQA,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,sEAAsE;IACtE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,9 @@
1
+ // CONTRACT: stable — coordinate changes. Shared P0b quota value types.
2
+ //
3
+ // Pure data contracts shared by the quota-enforcement functional-core unit and
4
+ // the shell that loads/persists tenant usage. NO behavior, NO limits literals
5
+ // here — the LIMITS are operational parameters sourced from Env.media at the
6
+ // call site (a hard-coded cap in this PUBLIC tarball would be a published
7
+ // quota). This file only names the shapes.
8
+ export {};
9
+ //# sourceMappingURL=quota-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-types.js","sourceRoot":"","sources":["../../../src/lib/media/quota-types.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,6EAA6E;AAC7E,0EAA0E;AAC1E,2CAA2C"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * routeUpload — pure functional-core routing decision for an inbound upload.
3
+ *
4
+ * Maps a `Content-Type` header value to one of three ingest routes:
5
+ *
6
+ * - `sync-image` — the file is a re-encodable raster image (image/*,
7
+ * matching the P0a `REENCODABLE_IMAGE_TYPES` set); handled
8
+ * synchronously in the upload handler.
9
+ * - `async-pending` — the file is video/* or audio/*; stored PENDING and
10
+ * handed off to the P0b async worker.
11
+ * - `reject` — anything else, including empty, malformed, or unknown
12
+ * types (fail-closed).
13
+ *
14
+ * Design invariants:
15
+ * - Pure and total: no I/O, no exceptions. Returns one of the three union
16
+ * members for every input including null/undefined.
17
+ * - Case-insensitive: "Image/JPEG" and "image/jpeg" both route to sync-image.
18
+ * - Parameters stripped: "image/jpeg; charset=utf-8" is treated as
19
+ * "image/jpeg".
20
+ * - Fail-closed: uncertainty → reject, never sync-image/async-pending.
21
+ * - No operational thresholds (no size caps, no rate limits) — those are
22
+ * imperative-shell concerns.
23
+ */
24
+ /**
25
+ * The three ingest routes for an uploaded object.
26
+ *
27
+ * - `sync-image` — re-encode synchronously; the upload handler completes
28
+ * the full pipeline inline and records the object as APPROVED
29
+ * after the re-encode pass.
30
+ * - `async-pending` — store as-is, record as PENDING, fan out to the P0b
31
+ * async processing worker.
32
+ * - `reject` — refuse the upload at the type-routing boundary (before
33
+ * bytes are read / stored). Caller must return an error.
34
+ */
35
+ export type IngestRoute = {
36
+ readonly kind: "sync-image";
37
+ } | {
38
+ readonly kind: "async-pending";
39
+ } | {
40
+ readonly kind: "reject";
41
+ };
42
+ /**
43
+ * Route an inbound upload by its declared Content-Type (MIME type).
44
+ *
45
+ * Accepts the raw Content-Type string as sent by the browser/client.
46
+ * Parameters (`;` and everything after) are stripped before matching so
47
+ * "image/png; q=0.9" routes identically to "image/png".
48
+ *
49
+ * The image set mirrors `REENCODABLE_IMAGE_TYPES` from the P0a
50
+ * image-normalizer exactly: jpeg/jpg/png/webp/gif. SVG, HEIC/HEIF, TIFF, and
51
+ * all other image/* sub-types route to reject (fail-closed).
52
+ *
53
+ * @param contentType - The raw Content-Type header value. Accepts null/undefined
54
+ * (both route to reject).
55
+ * @returns The routing decision — never throws.
56
+ */
57
+ export declare function routeUpload(contentType: string): IngestRoute;
58
+ //# sourceMappingURL=route-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-upload.d.ts","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAOhC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAyC5D"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * routeUpload — pure functional-core routing decision for an inbound upload.
3
+ *
4
+ * Maps a `Content-Type` header value to one of three ingest routes:
5
+ *
6
+ * - `sync-image` — the file is a re-encodable raster image (image/*,
7
+ * matching the P0a `REENCODABLE_IMAGE_TYPES` set); handled
8
+ * synchronously in the upload handler.
9
+ * - `async-pending` — the file is video/* or audio/*; stored PENDING and
10
+ * handed off to the P0b async worker.
11
+ * - `reject` — anything else, including empty, malformed, or unknown
12
+ * types (fail-closed).
13
+ *
14
+ * Design invariants:
15
+ * - Pure and total: no I/O, no exceptions. Returns one of the three union
16
+ * members for every input including null/undefined.
17
+ * - Case-insensitive: "Image/JPEG" and "image/jpeg" both route to sync-image.
18
+ * - Parameters stripped: "image/jpeg; charset=utf-8" is treated as
19
+ * "image/jpeg".
20
+ * - Fail-closed: uncertainty → reject, never sync-image/async-pending.
21
+ * - No operational thresholds (no size caps, no rate limits) — those are
22
+ * imperative-shell concerns.
23
+ */
24
+ // Singleton values avoid allocating a new object on every call.
25
+ const SYNC_IMAGE = { kind: "sync-image" };
26
+ const ASYNC_PENDING = { kind: "async-pending" };
27
+ const REJECT = { kind: "reject" };
28
+ /**
29
+ * Route an inbound upload by its declared Content-Type (MIME type).
30
+ *
31
+ * Accepts the raw Content-Type string as sent by the browser/client.
32
+ * Parameters (`;` and everything after) are stripped before matching so
33
+ * "image/png; q=0.9" routes identically to "image/png".
34
+ *
35
+ * The image set mirrors `REENCODABLE_IMAGE_TYPES` from the P0a
36
+ * image-normalizer exactly: jpeg/jpg/png/webp/gif. SVG, HEIC/HEIF, TIFF, and
37
+ * all other image/* sub-types route to reject (fail-closed).
38
+ *
39
+ * @param contentType - The raw Content-Type header value. Accepts null/undefined
40
+ * (both route to reject).
41
+ * @returns The routing decision — never throws.
42
+ */
43
+ export function routeUpload(contentType) {
44
+ // Guard: null/undefined/empty → reject.
45
+ if (!contentType || typeof contentType !== "string") {
46
+ return REJECT;
47
+ }
48
+ // Strip parameters ("; charset=utf-8", "; boundary=…", etc.) and normalise
49
+ // to lowercase for case-insensitive matching.
50
+ const base = contentType.split(";")[0].trim().toLowerCase();
51
+ if (!base) {
52
+ return REJECT;
53
+ }
54
+ // --- image/* ---------------------------------------------------------------
55
+ // Only the re-encodable set (mirrors REENCODABLE_IMAGE_TYPES from
56
+ // apps/api/src/lib/services/image-normalizer.ts). Other image/* sub-types
57
+ // (svg+xml, heic, heif, tiff, bmp, …) are rejected.
58
+ switch (base) {
59
+ case "image/jpeg":
60
+ case "image/jpg": // alias; normalised to image/jpeg downstream
61
+ case "image/png":
62
+ case "image/webp":
63
+ case "image/gif": // static raster in P0a (animated → first frame only)
64
+ return SYNC_IMAGE;
65
+ }
66
+ // --- video/* and audio/* ---------------------------------------------------
67
+ // Stored PENDING; transcoded and moderated by the P0b async worker.
68
+ if (base.startsWith("video/") || base.startsWith("audio/")) {
69
+ // Require a non-empty sub-type: "video/" (bare slash, no sub-type) is
70
+ // malformed and goes to reject.
71
+ const subType = base.slice(base.indexOf("/") + 1);
72
+ if (subType) {
73
+ return ASYNC_PENDING;
74
+ }
75
+ return REJECT;
76
+ }
77
+ // Everything else (application/*, text/*, unknown, etc.) → fail-closed.
78
+ return REJECT;
79
+ }
80
+ //# sourceMappingURL=route-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-upload.js","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAkBH,gEAAgE;AAChE,MAAM,UAAU,GAAgB,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AACvD,MAAM,aAAa,GAAgB,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;AAC7D,MAAM,MAAM,GAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAE/C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,wCAAwC;IACxC,IAAI,CAAC,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,2EAA2E;IAC3E,8CAA8C;IAC9C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE5D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,8EAA8E;IAC9E,kEAAkE;IAClE,2EAA2E;IAC3E,oDAAoD;IACpD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,CAAC,CAAC,6CAA6C;QAC/D,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,EAAE,qDAAqD;YACrE,OAAO,UAAU,CAAC;IACtB,CAAC;IAED,8EAA8E;IAC9E,oEAAoE;IACpE,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,sEAAsE;QACtE,gCAAgC;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,aAAa,CAAC;QACvB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,wEAAwE;IACxE,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Fail-closed serve gate (T5).
3
+ *
4
+ * The pure functional core behind `serveMediaByHash`: the predicate that decides
5
+ * whether a media object's bytes may be served, and the content-type mapper for
6
+ * the (only) servable case. No I/O, no clock, no Prisma import — exhaustively
7
+ * unit/property-tested. The imperative shell (the route handler) maps the Prisma
8
+ * record to these inputs at the I/O boundary and never re-implements the
9
+ * decision inline.
10
+ */
11
+ import type { ModerationStatus } from "./moderation-status.js";
12
+ /**
13
+ * The ONLY state that may serve bytes.
14
+ *
15
+ * Flat and viewer-independent — there is **no owner exception**. The owner's
16
+ * optimistic "I can see my own upload" view is a client-local copy (Flutter),
17
+ * never a server URL. Operates on {@link ModerationStatus} (T1's hand-written
18
+ * source of truth), mapped from `MediaFile.moderationStatus` at the shell
19
+ * boundary.
20
+ *
21
+ * P0a invariant: video/audio are born `PENDING` and have no P0b worker to move
22
+ * them forward, so this predicate denies them for every viewer.
23
+ */
24
+ export declare function canServe(status: ModerationStatus): boolean;
25
+ /**
26
+ * The full serve decision for a looked-up media record. Returns `true` only when
27
+ * the object is `APPROVED` **and** not hidden **and** not soft-deleted. Every
28
+ * other combination (incl. a missing field) denies.
29
+ *
30
+ * The shell calls this with the fields read from the DB record; a `null` record
31
+ * (not-found) or a thrown query (DB-error) never reaches here — those deny via
32
+ * the uniform placeholder without consulting the predicate, so absence and
33
+ * not-yet-approved are byte-identical to a prober.
34
+ */
35
+ export declare function isServable(record: {
36
+ moderationStatus: ModerationStatus;
37
+ hidden: boolean;
38
+ deletedAt: Date | null;
39
+ }): boolean;
40
+ /**
41
+ * Map the canonical re-encode format (T7's `env.media.canonicalFormat`) to the
42
+ * Content-Type emitted on an APPROVED response.
43
+ *
44
+ * Content-type is derived **only** from the canonical format the bytes were
45
+ * re-encoded into — never from `object.httpMetadata.contentType` (attacker-
46
+ * influenced) and never from the stored `mimeType`. In P0a only images reach
47
+ * APPROVED, so the canonical format is always one of the sharp-writable raster
48
+ * formats.
49
+ */
50
+ export declare function canonicalContentType(canonicalFormat: "jpeg" | "png" | "webp"): string;
51
+ //# sourceMappingURL=serve-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-gate.d.ts","sourceRoot":"","sources":["../../../src/lib/media/serve-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE;IACjC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB,GAAG,OAAO,CAQV;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GACvC,MAAM,CAYR"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Fail-closed serve gate (T5).
3
+ *
4
+ * The pure functional core behind `serveMediaByHash`: the predicate that decides
5
+ * whether a media object's bytes may be served, and the content-type mapper for
6
+ * the (only) servable case. No I/O, no clock, no Prisma import — exhaustively
7
+ * unit/property-tested. The imperative shell (the route handler) maps the Prisma
8
+ * record to these inputs at the I/O boundary and never re-implements the
9
+ * decision inline.
10
+ */
11
+ /**
12
+ * The ONLY state that may serve bytes.
13
+ *
14
+ * Flat and viewer-independent — there is **no owner exception**. The owner's
15
+ * optimistic "I can see my own upload" view is a client-local copy (Flutter),
16
+ * never a server URL. Operates on {@link ModerationStatus} (T1's hand-written
17
+ * source of truth), mapped from `MediaFile.moderationStatus` at the shell
18
+ * boundary.
19
+ *
20
+ * P0a invariant: video/audio are born `PENDING` and have no P0b worker to move
21
+ * them forward, so this predicate denies them for every viewer.
22
+ */
23
+ export function canServe(status) {
24
+ return status === "APPROVED";
25
+ }
26
+ /**
27
+ * The full serve decision for a looked-up media record. Returns `true` only when
28
+ * the object is `APPROVED` **and** not hidden **and** not soft-deleted. Every
29
+ * other combination (incl. a missing field) denies.
30
+ *
31
+ * The shell calls this with the fields read from the DB record; a `null` record
32
+ * (not-found) or a thrown query (DB-error) never reaches here — those deny via
33
+ * the uniform placeholder without consulting the predicate, so absence and
34
+ * not-yet-approved are byte-identical to a prober.
35
+ */
36
+ export function isServable(record) {
37
+ if (record.hidden) {
38
+ return false;
39
+ }
40
+ if (record.deletedAt !== null && record.deletedAt !== undefined) {
41
+ return false;
42
+ }
43
+ return canServe(record.moderationStatus);
44
+ }
45
+ /**
46
+ * Map the canonical re-encode format (T7's `env.media.canonicalFormat`) to the
47
+ * Content-Type emitted on an APPROVED response.
48
+ *
49
+ * Content-type is derived **only** from the canonical format the bytes were
50
+ * re-encoded into — never from `object.httpMetadata.contentType` (attacker-
51
+ * influenced) and never from the stored `mimeType`. In P0a only images reach
52
+ * APPROVED, so the canonical format is always one of the sharp-writable raster
53
+ * formats.
54
+ */
55
+ export function canonicalContentType(canonicalFormat) {
56
+ switch (canonicalFormat) {
57
+ case "jpeg":
58
+ return "image/jpeg";
59
+ case "png":
60
+ return "image/png";
61
+ case "webp":
62
+ return "image/webp";
63
+ default:
64
+ // Exhaustive in the type; defensive fallback keeps this total.
65
+ return "application/octet-stream";
66
+ }
67
+ }
68
+ //# sourceMappingURL=serve-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-gate.js","sourceRoot":"","sources":["../../../src/lib/media/serve-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAwB;IAC/C,OAAO,MAAM,KAAK,UAAU,CAAC;AAC/B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,MAI1B;IACC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAAwC;IAExC,QAAQ,eAAe,EAAE,CAAC;QACxB,KAAK,MAAM;YACT,OAAO,YAAY,CAAC;QACtB,KAAK,KAAK;YACR,OAAO,WAAW,CAAC;QACrB,KAAK,MAAM;YACT,OAAO,YAAY,CAAC;QACtB;YACE,+DAA+D;YAC/D,OAAO,0BAA0B,CAAC;IACtC,CAAC;AACH,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure tenant-id resolution for the media path (T9).
3
+ *
4
+ * The media upload/serve shell must scope every CAS key by a tenant
5
+ * (`cas/{tenantId}/{hash}`, D18). The active tenant is normally ambient
6
+ * (`getCurrentTenantId()` from the auth seam, app.ts), but `TENANT_SCOPE_MODE`
7
+ * defaults to `"off"` and in that mode no ambient tenant is set on the request.
8
+ *
9
+ * ASSUMPTION (recorded per T9): when scope mode is `"off"` we fall back to the
10
+ * uploader's `User.personalTenantId`. This mirrors the graph layer's wiring
11
+ * (`input.tenantId ?? getCurrentTenantId()` in graph/postgres/sync.ts) — every
12
+ * user has a personal tenant created at sign-up (post-confirmation.ts), so the
13
+ * fallback is always available for an authenticated request. When scope mode is
14
+ * shadow/enforce the ambient tenant is authoritative and the fallback is not
15
+ * consulted (a missing ambient tenant in those modes is a real error, surfaced
16
+ * by the caller, not papered over by the personal tenant).
17
+ *
18
+ * This module is the PURE decision; the I/O (reading the ambient ALS value and
19
+ * loading `personalTenantId` from the DB) lives in the shell (media.ts).
20
+ */
21
+ import type { TenantScopeMode } from "../tenant-scope.js";
22
+ export type TenantResolution = {
23
+ ok: true;
24
+ tenantId: string;
25
+ source: "ambient" | "personal-fallback";
26
+ } | {
27
+ ok: false;
28
+ reason: "no-tenant";
29
+ };
30
+ /**
31
+ * Decide which tenant id scopes a media object, given the ambient tenant (may
32
+ * be undefined), the uploader's personal tenant (may be undefined), and the
33
+ * deploy scope mode.
34
+ *
35
+ * - Ambient tenant always wins when present (any mode).
36
+ * - When scope mode is `"off"` and there is no ambient tenant, fall back to the
37
+ * uploader's personal tenant.
38
+ * - In shadow/enforce with no ambient tenant, do NOT fall back — return
39
+ * `no-tenant` so the caller fails closed.
40
+ */
41
+ export declare function resolveMediaTenantId(ambientTenantId: string | undefined | null, personalTenantId: string | undefined | null, scopeMode: TenantScopeMode): TenantResolution;
42
+ //# sourceMappingURL=tenant-resolution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-resolution.d.ts","sourceRoot":"","sources":["../../../src/lib/media/tenant-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,mBAAmB,CAAA;CAAE,GACvE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC1C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC3C,SAAS,EAAE,eAAe,GACzB,gBAAgB,CAYlB"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pure tenant-id resolution for the media path (T9).
3
+ *
4
+ * The media upload/serve shell must scope every CAS key by a tenant
5
+ * (`cas/{tenantId}/{hash}`, D18). The active tenant is normally ambient
6
+ * (`getCurrentTenantId()` from the auth seam, app.ts), but `TENANT_SCOPE_MODE`
7
+ * defaults to `"off"` and in that mode no ambient tenant is set on the request.
8
+ *
9
+ * ASSUMPTION (recorded per T9): when scope mode is `"off"` we fall back to the
10
+ * uploader's `User.personalTenantId`. This mirrors the graph layer's wiring
11
+ * (`input.tenantId ?? getCurrentTenantId()` in graph/postgres/sync.ts) — every
12
+ * user has a personal tenant created at sign-up (post-confirmation.ts), so the
13
+ * fallback is always available for an authenticated request. When scope mode is
14
+ * shadow/enforce the ambient tenant is authoritative and the fallback is not
15
+ * consulted (a missing ambient tenant in those modes is a real error, surfaced
16
+ * by the caller, not papered over by the personal tenant).
17
+ *
18
+ * This module is the PURE decision; the I/O (reading the ambient ALS value and
19
+ * loading `personalTenantId` from the DB) lives in the shell (media.ts).
20
+ */
21
+ /**
22
+ * Decide which tenant id scopes a media object, given the ambient tenant (may
23
+ * be undefined), the uploader's personal tenant (may be undefined), and the
24
+ * deploy scope mode.
25
+ *
26
+ * - Ambient tenant always wins when present (any mode).
27
+ * - When scope mode is `"off"` and there is no ambient tenant, fall back to the
28
+ * uploader's personal tenant.
29
+ * - In shadow/enforce with no ambient tenant, do NOT fall back — return
30
+ * `no-tenant` so the caller fails closed.
31
+ */
32
+ export function resolveMediaTenantId(ambientTenantId, personalTenantId, scopeMode) {
33
+ if (ambientTenantId) {
34
+ return { ok: true, tenantId: ambientTenantId, source: "ambient" };
35
+ }
36
+ if (scopeMode === "off" && personalTenantId) {
37
+ return {
38
+ ok: true,
39
+ tenantId: personalTenantId,
40
+ source: "personal-fallback",
41
+ };
42
+ }
43
+ return { ok: false, reason: "no-tenant" };
44
+ }
45
+ //# sourceMappingURL=tenant-resolution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-resolution.js","sourceRoot":"","sources":["../../../src/lib/media/tenant-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,eAA0C,EAC1C,gBAA2C,EAC3C,SAA0B;IAE1B,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpE,CAAC;IACD,IAAI,SAAS,KAAK,KAAK,IAAI,gBAAgB,EAAE,CAAC;QAC5C,OAAO;YACL,EAAE,EAAE,IAAI;YACR,QAAQ,EAAE,gBAAgB;YAC1B,MAAM,EAAE,mBAAmB;SAC5B,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { ModerationVerdict } from "./moderation-provider.js";
2
+ /**
3
+ * The text-moderation capability seam used by the AUDIO track (over a
4
+ * transcript) and by any caller needing to classify free text into the
5
+ * canonical 3-value verdict.
6
+ *
7
+ * Binding rule (same as MediaModerationProvider): absence of signal, an
8
+ * internal fault, a spent budget, or ANY uncertainty MUST fail closed to
9
+ * `decision: "review"`. An implementation must NEVER manufacture `approved`
10
+ * from doubt.
11
+ */
12
+ export interface TextModerationProvider {
13
+ moderateText(text: string): Promise<ModerationVerdict>;
14
+ }
15
+ /**
16
+ * Test seam: returns a canned verdict (default fail-closed `review`). Labels, if
17
+ * programmed, must use ONLY opaque category tokens — never real-category strings.
18
+ */
19
+ export declare class MockTextModerationProvider implements TextModerationProvider {
20
+ private verdict;
21
+ /** Records of each input, for assertions. */
22
+ readonly calls: string[];
23
+ constructor(canned?: ModerationVerdict);
24
+ /** Program the verdict returned by subsequent `moderateText` calls. */
25
+ setVerdict(verdict: ModerationVerdict): void;
26
+ moderateText(text: string): Promise<ModerationVerdict>;
27
+ }
28
+ //# sourceMappingURL=text-moderation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-moderation.d.ts","sourceRoot":"","sources":["../../../src/lib/media/text-moderation.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACxD;AAUD;;;GAGG;AACH,qBAAa,0BAA2B,YAAW,sBAAsB;IACvE,OAAO,CAAC,OAAO,CAAoB;IAEnC,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAM;gBAElB,MAAM,GAAE,iBAAwC;IAI5D,uEAAuE;IACvE,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAItC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAI7D"}