@de-otio/trellis 0.12.3 → 0.14.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.
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +3 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lambda/media-processing-worker.d.ts +11 -1
- package/dist/lambda/media-processing-worker.d.ts.map +1 -1
- package/dist/lambda/media-processing-worker.js +38 -11
- package/dist/lambda/media-processing-worker.js.map +1 -1
- package/dist/lib/media/cas-keys.d.ts +15 -0
- package/dist/lib/media/cas-keys.d.ts.map +1 -1
- package/dist/lib/media/cas-keys.js +27 -0
- package/dist/lib/media/cas-keys.js.map +1 -1
- package/dist/lib/media/media-ports.d.ts +14 -0
- package/dist/lib/media/media-ports.d.ts.map +1 -1
- package/dist/lib/media/media-ports.js +9 -0
- package/dist/lib/media/media-ports.js.map +1 -1
- package/dist/lib/media/media-upsert.d.ts +9 -0
- package/dist/lib/media/media-upsert.d.ts.map +1 -1
- package/dist/lib/media/media-upsert.js +5 -0
- package/dist/lib/media/media-upsert.js.map +1 -1
- package/dist/lib/media/moderation-status.d.ts +12 -0
- package/dist/lib/media/moderation-status.d.ts.map +1 -1
- package/dist/lib/media/moderation-status.js +15 -0
- package/dist/lib/media/moderation-status.js.map +1 -1
- package/dist/lib/media/request-moderation.d.ts +18 -0
- package/dist/lib/media/request-moderation.d.ts.map +1 -0
- package/dist/lib/media/request-moderation.js +45 -0
- package/dist/lib/media/request-moderation.js.map +1 -0
- package/dist/lib/media/route-upload.d.ts +4 -3
- package/dist/lib/media/route-upload.d.ts.map +1 -1
- package/dist/lib/media/route-upload.js.map +1 -1
- package/dist/lib/routes/media.d.ts.map +1 -1
- package/dist/lib/routes/media.js +134 -26
- package/dist/lib/routes/media.js.map +1 -1
- package/package.json +1 -1
- package/src/lambda/media-processing-worker.ts +48 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation-status.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,CAAC;AAEf;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACxD,iBAAiB,CAAC;AAwBtB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,eAAe,GACrB,gBAAgB,CAoClB;
|
|
1
|
+
{"version":3,"file":"moderation-status.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,CAAC;AAEf;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACxD,iBAAiB,CAAC;AAwBtB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,eAAe,GACrB,gBAAgB,CAoClB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,GAC3B,gBAAgB,CAGlB;AAqBD,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAExC,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,SAAS,kBAAkB,EAIxD,CAAC"}
|
|
@@ -93,6 +93,21 @@ export function nextStatus(current, event) {
|
|
|
93
93
|
function illegal(from, event) {
|
|
94
94
|
return { ok: false, reason: "illegal-transition", from, event };
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Map a classifier {@link ModerationDecision} to the {@link ModerationStatus} a
|
|
98
|
+
* freshly-uploaded (PENDING) object should land in — a thin shell over
|
|
99
|
+
* {@link nextStatus} for the synchronous image-upload path.
|
|
100
|
+
*
|
|
101
|
+
* Drives the transition `PENDING --decision <d>--> status` and returns the
|
|
102
|
+
* resulting status. The transition out of PENDING on a `decision` event is
|
|
103
|
+
* always legal, but should the machine ever report a not-ok transition we fail
|
|
104
|
+
* closed to `REVIEW` (never `APPROVED`): an unexpected refusal must degrade to
|
|
105
|
+
* human review, not to serving.
|
|
106
|
+
*/
|
|
107
|
+
export function decisionToStatus(decision) {
|
|
108
|
+
const result = nextStatus("PENDING", { kind: "decision", decision });
|
|
109
|
+
return result.ok ? result.status : "REVIEW";
|
|
110
|
+
}
|
|
96
111
|
/**
|
|
97
112
|
* Compile-time exhaustiveness guard for {@link ModerationStatus}.
|
|
98
113
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation-status.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4DH;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,QAA4B;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,aAAa,CAAC;QACvB;YACE,8DAA8D;YAC9D,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,UAAU,CACxB,OAAyB,EACzB,KAAsB;IAEtB,wEAAwE;IACxE,gCAAgC;IAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,yDAAyD;YACzD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjE,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,yCAAyC;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,OAAO;oBACL,EAAE,EAAE,IAAI;oBACR,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;iBAC7D,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU;YACb,sCAAsC;YACtC,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAEjC;YACE,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CACd,IAAsB,EACtB,KAAsB;IAEtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,yBAAyB,GAAG;IAChC,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;CACmC,CAAC;AAEpD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAgC,MAAM,CAAC,IAAI,CAC7E,yBAAyB,CACJ,CAAC;AAExB,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAkC;IACrE,UAAU;IACV,QAAQ;IACR,YAAY;CACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"moderation-status.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4DH;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,QAA4B;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,aAAa,CAAC;QACvB;YACE,8DAA8D;YAC9D,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,UAAU,CACxB,OAAyB,EACzB,KAAsB;IAEtB,wEAAwE;IACxE,gCAAgC;IAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,yDAAyD;YACzD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjE,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,yCAAyC;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,OAAO;oBACL,EAAE,EAAE,IAAI;oBACR,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;iBAC7D,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU;YACb,sCAAsC;YACtC,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAEjC;YACE,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CACd,IAAsB,EACtB,KAAsB;IAEtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA4B;IAE5B,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,yBAAyB,GAAG;IAChC,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;CACmC,CAAC;AAEpD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAgC,MAAM,CAAC,IAAI,CAC7E,yBAAyB,CACJ,CAAC;AAExB,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAkC;IACrE,UAAU;IACV,QAAQ;IACR,YAAY;CACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MediaModerationProvider } from "./moderation-provider.js";
|
|
2
|
+
/**
|
|
3
|
+
* Consuming app (Skybber) calls this at startup with its concrete moderation
|
|
4
|
+
* provider (e.g. an AWS Rekognition adapter). MUST run before the upload route
|
|
5
|
+
* serves. Re-exported from `@de-otio/trellis` (apps/api/src/index.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare function setMediaModerationProvider(provider: MediaModerationProvider): void;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the injected provider if one was registered, else a fail-closed
|
|
10
|
+
* {@link NullModerationProvider} (every verdict = `review`). The upload handler
|
|
11
|
+
* calls this on each sync-image request. Defaulting to Null — rather than
|
|
12
|
+
* throwing — means an un-wired deploy degrades to REVIEW (never serves, never
|
|
13
|
+
* 500), which is the safe behaviour for a moderation seam.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getMediaModerationProvider(): MediaModerationProvider;
|
|
16
|
+
/** Test-only: clear the injected provider so tests don't leak across cases. */
|
|
17
|
+
export declare function __resetMediaModerationProviderForTests(): void;
|
|
18
|
+
//# sourceMappingURL=request-moderation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-moderation.d.ts","sourceRoot":"","sources":["../../../src/lib/media/request-moderation.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,0BAA0B,CAAC;AAWlC;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAEN;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,uBAAuB,CAEpE;AAED,+EAA+E;AAC/E,wBAAgB,sCAAsC,IAAI,IAAI,CAE7D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes.
|
|
2
|
+
//
|
|
3
|
+
// Request-path provider seam for SYNCHRONOUS image moderation. Mirrors the
|
|
4
|
+
// realtime-transport seam (apps/api/src/lib/realtime/index.ts): a consuming app
|
|
5
|
+
// (Skybber) calls setMediaModerationProvider() ONCE at startup with its concrete
|
|
6
|
+
// cloud adapter, and the upload handler reads it via getMediaModerationProvider()
|
|
7
|
+
// on every sync-image request. Core imports no cloud SDK.
|
|
8
|
+
//
|
|
9
|
+
// FAIL-CLOSED DEFAULT: when no provider has been injected, getMediaModerationProvider()
|
|
10
|
+
// returns a NullModerationProvider (every verdict = "review") rather than throwing.
|
|
11
|
+
// An un-wired deploy must degrade to REVIEW (never serve, never 500) — a thrown
|
|
12
|
+
// error here would turn every image upload into a 500. The startup guard
|
|
13
|
+
// (assertModerationProviderAllowed) is the place that loudly refuses to run Null
|
|
14
|
+
// outside dev; this seam is the safe runtime fallback.
|
|
15
|
+
import { NullModerationProvider, } from "./moderation-provider.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Provider-injection hook (mirrors setRealtimeProvider). A consuming app calls
|
|
18
|
+
// setMediaModerationProvider() at startup, BEFORE the upload route serves, with
|
|
19
|
+
// its concrete provider. getMediaModerationProvider() returns the injected
|
|
20
|
+
// provider if present, else a fail-closed Null default.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
let injected;
|
|
23
|
+
/**
|
|
24
|
+
* Consuming app (Skybber) calls this at startup with its concrete moderation
|
|
25
|
+
* provider (e.g. an AWS Rekognition adapter). MUST run before the upload route
|
|
26
|
+
* serves. Re-exported from `@de-otio/trellis` (apps/api/src/index.ts).
|
|
27
|
+
*/
|
|
28
|
+
export function setMediaModerationProvider(provider) {
|
|
29
|
+
injected = provider;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the injected provider if one was registered, else a fail-closed
|
|
33
|
+
* {@link NullModerationProvider} (every verdict = `review`). The upload handler
|
|
34
|
+
* calls this on each sync-image request. Defaulting to Null — rather than
|
|
35
|
+
* throwing — means an un-wired deploy degrades to REVIEW (never serves, never
|
|
36
|
+
* 500), which is the safe behaviour for a moderation seam.
|
|
37
|
+
*/
|
|
38
|
+
export function getMediaModerationProvider() {
|
|
39
|
+
return injected ?? new NullModerationProvider();
|
|
40
|
+
}
|
|
41
|
+
/** Test-only: clear the injected provider so tests don't leak across cases. */
|
|
42
|
+
export function __resetMediaModerationProviderForTests() {
|
|
43
|
+
injected = undefined;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=request-moderation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-moderation.js","sourceRoot":"","sources":["../../../src/lib/media/request-moderation.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,iFAAiF;AACjF,kFAAkF;AAClF,0DAA0D;AAC1D,EAAE;AACF,wFAAwF;AACxF,oFAAoF;AACpF,gFAAgF;AAChF,yEAAyE;AACzE,iFAAiF;AACjF,uDAAuD;AAEvD,OAAO,EAEL,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAElC,8EAA8E;AAC9E,+EAA+E;AAC/E,gFAAgF;AAChF,2EAA2E;AAC3E,wDAAwD;AACxD,8EAA8E;AAE9E,IAAI,QAA6C,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAiC;IAEjC,QAAQ,GAAG,QAAQ,CAAC;AACtB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO,QAAQ,IAAI,IAAI,sBAAsB,EAAE,CAAC;AAClD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,sCAAsC;IACpD,QAAQ,GAAG,SAAS,CAAC;AACvB,CAAC"}
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
/**
|
|
25
25
|
* The three ingest routes for an uploaded object.
|
|
26
26
|
*
|
|
27
|
-
* - `sync-image` — re-encode synchronously
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* - `sync-image` — re-encode synchronously, then determine
|
|
28
|
+
* APPROVED/REVIEW/QUARANTINED via the injected
|
|
29
|
+
* moderateImage provider; the handler stages the cleaned
|
|
30
|
+
* bytes and promotes them to cas/ only on APPROVED.
|
|
30
31
|
* - `async-pending` — store as-is, record as PENDING, fan out to the P0b
|
|
31
32
|
* async processing worker.
|
|
32
33
|
* - `reject` — refuse the upload at the type-routing boundary (before
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-upload.d.ts","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH
|
|
1
|
+
{"version":3,"file":"route-upload.d.ts","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;;GAWG;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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-upload.js","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;
|
|
1
|
+
{"version":3,"file":"route-upload.js","sourceRoot":"","sources":["../../../src/lib/media/route-upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAmBH,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../../src/lib/routes/media.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../../src/lib/routes/media.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiCH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAoPxC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1B,OAAO,CAAC,QAAQ,CAAC,CA+HnB;AAoDD,eAAO,MAAM,WAAW,EAAE,KAAK,EAi5E9B,CAAC"}
|
package/dist/lib/routes/media.js
CHANGED
|
@@ -9,9 +9,11 @@ import { CorsHandler } from "../cors-handler.js";
|
|
|
9
9
|
import { sharedDatabaseConnectionManager } from "../database-connection-manager.js";
|
|
10
10
|
import { QueryTimeoutPresets, withQueryTimeoutAndRetry, } from "../db-query-helper.js";
|
|
11
11
|
import { getLogger } from "../logger.js";
|
|
12
|
-
import { casKey, isCasKeyError, pendingKey, validateContentHash } from "../media/cas-keys.js";
|
|
12
|
+
import { casKey, isCasKeyError, pendingKey, processingKey, validateContentHash } from "../media/cas-keys.js";
|
|
13
13
|
import { buildMediaUpsertArgs } from "../media/media-upsert.js";
|
|
14
14
|
import { checkUploadQuota } from "../media/quota-check.js";
|
|
15
|
+
import { decisionToStatus } from "../media/moderation-status.js";
|
|
16
|
+
import { getMediaModerationProvider } from "../media/request-moderation.js";
|
|
15
17
|
import { canonicalContentType, isServable, } from "../media/serve-gate.js";
|
|
16
18
|
import { resolveMediaTenantId } from "../media/tenant-resolution.js";
|
|
17
19
|
import { routeUpload } from "../media/route-upload.js";
|
|
@@ -903,19 +905,29 @@ export const mediaRoutes = [
|
|
|
903
905
|
height: extracted?.exifData?.height || extracted?.videoMetadata?.height,
|
|
904
906
|
duration: extracted?.videoMetadata?.duration,
|
|
905
907
|
};
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
908
|
+
// --- STAGE → MODERATE → PROMOTE-ON-APPROVE -----------------------------
|
|
909
|
+
// Synchronous image moderation. The cleaned (re-encoded) bytes are
|
|
910
|
+
// written to a STAGING key first, moderated, and only PROMOTED to the
|
|
911
|
+
// canonical `cas/` key when the verdict is APPROVED. `cas/` therefore
|
|
912
|
+
// only ever holds approved bytes; REVIEW/QUARANTINED bytes stay at
|
|
913
|
+
// staging out of the serve path (the gate is APPROVED-only). FAIL-CLOSED
|
|
914
|
+
// throughout — any uncertainty resolves to REVIEW, never APPROVED.
|
|
915
|
+
// 1. Hash the CLEANED output bytes so the CAS key addresses clean bytes
|
|
916
|
+
// (the same scheme MediaUploadService used: SHA-256 hex of the buffer).
|
|
917
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", uploadBuffer);
|
|
918
|
+
const contentHash = Array.from(new Uint8Array(hashBuffer))
|
|
919
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
920
|
+
.join("");
|
|
921
|
+
// 2. Build the canonical cas/ key (final serve location) and the
|
|
922
|
+
// processing/ staging key (pre-promotion location).
|
|
923
|
+
const uploadOriginalKey = casKey(tenantId, contentHash);
|
|
924
|
+
const stagingKey = processingKey(tenantId, contentHash);
|
|
925
|
+
if (isCasKeyError(uploadOriginalKey) || isCasKeyError(stagingKey)) {
|
|
926
|
+
logger.error("[Media Upload] Failed to build CAS/staging key", {
|
|
917
927
|
userId: session.userId,
|
|
918
|
-
kind: uploadOriginalKey
|
|
928
|
+
kind: isCasKeyError(uploadOriginalKey)
|
|
929
|
+
? uploadOriginalKey.kind
|
|
930
|
+
: stagingKey.kind,
|
|
919
931
|
});
|
|
920
932
|
const errorResponse = securityHeaders.createSecureResponse(JSON.stringify({
|
|
921
933
|
error: "Database error",
|
|
@@ -923,9 +935,95 @@ export const mediaRoutes = [
|
|
|
923
935
|
}), { status: 500, headers: { "content-type": "application/json" } });
|
|
924
936
|
return CorsHandler.addCorsHeaders(errorResponse, request, env);
|
|
925
937
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
938
|
+
const mediaBucket = env.MEDIA_BUCKET_R2 || env.R2_BUCKET;
|
|
939
|
+
if (!mediaBucket) {
|
|
940
|
+
// No object store to stage into — fail closed (cannot moderate, must
|
|
941
|
+
// not serve). Mirrors the async-pending path's 503.
|
|
942
|
+
logger.error("[Media Upload] No media bucket configured for staging", {
|
|
943
|
+
userId: session.userId,
|
|
944
|
+
});
|
|
945
|
+
const errorResponse = securityHeaders.createSecureResponse(JSON.stringify({ error: "Upload unavailable" }), { status: 503, headers: { "content-type": "application/json" } });
|
|
946
|
+
return CorsHandler.addCorsHeaders(errorResponse, request, env);
|
|
947
|
+
}
|
|
948
|
+
// 3. Write the cleaned bytes to STAGING (NOT cas/). cas/ is written only
|
|
949
|
+
// on APPROVED, below.
|
|
950
|
+
try {
|
|
951
|
+
await mediaBucket.put(stagingKey, uploadBuffer, {
|
|
952
|
+
httpMetadata: { contentType: mimeType },
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
catch (stageError) {
|
|
956
|
+
logger.error("[Media Upload] Staging write failed", {
|
|
957
|
+
userId: session.userId,
|
|
958
|
+
error: stageError?.message,
|
|
959
|
+
});
|
|
960
|
+
const errorResponse = securityHeaders.createSecureResponse(JSON.stringify({ error: "Upload failed" }), { status: 500, headers: { "content-type": "application/json" } });
|
|
961
|
+
return CorsHandler.addCorsHeaders(errorResponse, request, env);
|
|
962
|
+
}
|
|
963
|
+
// 4. Moderate the STAGED object via the injected provider. FAIL-CLOSED:
|
|
964
|
+
// any throw/timeout is treated as `review` (→ REVIEW, never promoted).
|
|
965
|
+
// The provider owns all thresholds (threshold-secrecy); core passes no
|
|
966
|
+
// numbers.
|
|
967
|
+
// The bucket handle the moderation ref carries is the RESOLVED media
|
|
968
|
+
// bucket name from the env — identical to what MEDIA_BUCKET_R2 (the
|
|
969
|
+
// binding the staging write went to) wraps, including the
|
|
970
|
+
// `${stage}-${appName}-media` fallback. Reading it back from the env
|
|
971
|
+
// (never re-deriving the name or fallback here) guarantees the staging
|
|
972
|
+
// WRITE and the moderation READ point at the same bucket, so it is never
|
|
973
|
+
// empty. The injected provider uses {bucket, key} to locate the STAGED
|
|
974
|
+
// object.
|
|
975
|
+
const moderationBucketName = env.MEDIA_BUCKET_NAME;
|
|
976
|
+
let decision;
|
|
977
|
+
try {
|
|
978
|
+
const verdict = await getMediaModerationProvider().moderateImage({
|
|
979
|
+
bucket: moderationBucketName,
|
|
980
|
+
key: stagingKey,
|
|
981
|
+
});
|
|
982
|
+
decision = decisionToStatus(verdict.decision);
|
|
983
|
+
}
|
|
984
|
+
catch (moderationError) {
|
|
985
|
+
logger.warn("[Media Upload] Image moderation failed — failing closed to REVIEW", {
|
|
986
|
+
userId: session.userId,
|
|
987
|
+
error: moderationError?.message,
|
|
988
|
+
});
|
|
989
|
+
decision = "REVIEW";
|
|
990
|
+
}
|
|
991
|
+
// 5. PROMOTE on APPROVED: copy staging → cas/ (the cleaned bytes we
|
|
992
|
+
// already hold in memory), then best-effort delete the staging copy.
|
|
993
|
+
// Anything else (REVIEW/QUARANTINED/REJECTED) leaves the bytes at
|
|
994
|
+
// staging and NEVER writes cas/.
|
|
995
|
+
if (decision === "APPROVED") {
|
|
996
|
+
try {
|
|
997
|
+
await mediaBucket.put(uploadOriginalKey, uploadBuffer, {
|
|
998
|
+
httpMetadata: { contentType: mimeType },
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
catch (promoteError) {
|
|
1002
|
+
// Promotion failed — the bytes are still safely at staging and the
|
|
1003
|
+
// row has not been written. Fail the upload so the client retries
|
|
1004
|
+
// rather than recording an APPROVED row with no servable cas/ object.
|
|
1005
|
+
logger.error("[Media Upload] CAS promotion failed", {
|
|
1006
|
+
userId: session.userId,
|
|
1007
|
+
error: promoteError?.message,
|
|
1008
|
+
});
|
|
1009
|
+
const errorResponse = securityHeaders.createSecureResponse(JSON.stringify({ error: "Upload failed" }), { status: 500, headers: { "content-type": "application/json" } });
|
|
1010
|
+
return CorsHandler.addCorsHeaders(errorResponse, request, env);
|
|
1011
|
+
}
|
|
1012
|
+
// Best-effort staging cleanup — cas/ is what serves, so a leftover
|
|
1013
|
+
// staging object is harmless (lifecycle-expired) and never fatal.
|
|
1014
|
+
try {
|
|
1015
|
+
await mediaBucket.delete(stagingKey);
|
|
1016
|
+
}
|
|
1017
|
+
catch (deleteError) {
|
|
1018
|
+
logger.warn("[Media Upload] Staging delete tolerated", {
|
|
1019
|
+
userId: session.userId,
|
|
1020
|
+
error: deleteError?.message,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// 6. Create the MediaFile DB record synchronously so post creation can
|
|
1025
|
+
// reference it immediately. Dedup is within-tenant via
|
|
1026
|
+
// @@unique([tenantId, contentHash]) (D18).
|
|
929
1027
|
try {
|
|
930
1028
|
await withQueryTimeoutAndRetry(sharedDatabaseConnectionManager, uploadRegion, env, async (db) => {
|
|
931
1029
|
// T9: a within-tenant dedup hit (identical bytes re-uploaded)
|
|
@@ -933,10 +1031,10 @@ export const mediaRoutes = [
|
|
|
933
1031
|
// buildMediaUpsertArgs guarantees the `update` payload touches
|
|
934
1032
|
// neither uploadedBy nor moderationStatus — subsequent uploaders
|
|
935
1033
|
// get a reference (via the post→media relation), not a mutation
|
|
936
|
-
// of the shared row.
|
|
1034
|
+
// of the shared row. The verdict applies to the `create` only.
|
|
937
1035
|
return await db.mediaFile.upsert(buildMediaUpsertArgs({
|
|
938
1036
|
tenantId,
|
|
939
|
-
contentHash
|
|
1037
|
+
contentHash,
|
|
940
1038
|
mimeType,
|
|
941
1039
|
size: file.size,
|
|
942
1040
|
originalKey: uploadOriginalKey,
|
|
@@ -944,6 +1042,7 @@ export const mediaRoutes = [
|
|
|
944
1042
|
width: metadata?.width,
|
|
945
1043
|
height: metadata?.height,
|
|
946
1044
|
duration: metadata?.duration,
|
|
1045
|
+
moderationStatus: decision,
|
|
947
1046
|
}));
|
|
948
1047
|
}, {
|
|
949
1048
|
...QueryTimeoutPresets.USER_FACING,
|
|
@@ -958,7 +1057,7 @@ export const mediaRoutes = [
|
|
|
958
1057
|
// DB record is required for post creation to validate media ownership.
|
|
959
1058
|
// If this fails, the upload must fail so the frontend can retry.
|
|
960
1059
|
logger.error("[Media Upload] Synchronous DB record creation failed", {
|
|
961
|
-
contentHash
|
|
1060
|
+
contentHash,
|
|
962
1061
|
error: dbError.message,
|
|
963
1062
|
});
|
|
964
1063
|
const dbErrorResponse = securityHeaders.createSecureResponse(JSON.stringify({
|
|
@@ -971,9 +1070,10 @@ export const mediaRoutes = [
|
|
|
971
1070
|
return CorsHandler.addCorsHeaders(dbErrorResponse, request, env);
|
|
972
1071
|
}
|
|
973
1072
|
logger.debug("[Media Upload] DB record created successfully", {
|
|
974
|
-
contentHash
|
|
1073
|
+
contentHash,
|
|
975
1074
|
uploadedBy: session.userId,
|
|
976
1075
|
originalKey: uploadOriginalKey,
|
|
1076
|
+
moderationStatus: decision,
|
|
977
1077
|
uploadRegion,
|
|
978
1078
|
});
|
|
979
1079
|
logger.info("Media upload successful", {
|
|
@@ -981,14 +1081,22 @@ export const mediaRoutes = [
|
|
|
981
1081
|
fileName: file.name,
|
|
982
1082
|
fileSize: file.size,
|
|
983
1083
|
mimeType,
|
|
984
|
-
contentHash
|
|
985
|
-
|
|
1084
|
+
contentHash,
|
|
1085
|
+
moderationStatus: decision,
|
|
986
1086
|
});
|
|
1087
|
+
// Reconstruct the client serve URL (same scheme MediaUploadService
|
|
1088
|
+
// used). This is the public API URL the client GETs by hash — NOT a
|
|
1089
|
+
// storage key (the serve gate resolves the storage key from the DB row,
|
|
1090
|
+
// never by interpolating the hash; see serve-maze-removed.test.ts).
|
|
1091
|
+
const apiDomain = env.ENVIRONMENT === "prod"
|
|
1092
|
+
? "https://api.example.com"
|
|
1093
|
+
: "https://api.rkm1.de";
|
|
1094
|
+
const serveUrl = `${apiDomain}/api/media/${encodeURIComponent(contentHash)}`;
|
|
987
1095
|
const response = securityHeaders.createSecureResponse(JSON.stringify({
|
|
988
|
-
url:
|
|
989
|
-
mediaKey:
|
|
990
|
-
contentHash
|
|
991
|
-
status:
|
|
1096
|
+
url: serveUrl,
|
|
1097
|
+
mediaKey: contentHash,
|
|
1098
|
+
contentHash,
|
|
1099
|
+
status: decision === "APPROVED" ? "approved" : "pending",
|
|
992
1100
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
993
1101
|
return CorsHandler.addCorsHeaders(response, request, env);
|
|
994
1102
|
}
|