@de-otio/trellis 0.10.11 → 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.
- package/dist/env.d.ts +232 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +221 -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 +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lambda/media-completion-worker.d.ts +175 -0
- package/dist/lambda/media-completion-worker.d.ts.map +1 -0
- package/dist/lambda/media-completion-worker.js +373 -0
- package/dist/lambda/media-completion-worker.js.map +1 -0
- package/dist/lambda/media-processing-worker.d.ts +172 -1
- package/dist/lambda/media-processing-worker.d.ts.map +1 -1
- package/dist/lambda/media-processing-worker.js +343 -49
- package/dist/lambda/media-processing-worker.js.map +1 -1
- package/dist/lib/app.d.ts.map +1 -1
- package/dist/lib/app.js +5 -0
- package/dist/lib/app.js.map +1 -1
- package/dist/lib/encrypted-settings/config.d.ts +13 -0
- package/dist/lib/encrypted-settings/config.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/config.js +19 -0
- package/dist/lib/encrypted-settings/config.js.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts +57 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.js +178 -0
- package/dist/lib/encrypted-settings/encrypted-settings-handler.js.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts +110 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.js +103 -0
- package/dist/lib/encrypted-settings/encrypted-settings-store.js.map +1 -0
- package/dist/lib/encrypted-settings/types.d.ts +26 -0
- package/dist/lib/encrypted-settings/types.d.ts.map +1 -0
- package/dist/lib/encrypted-settings/types.js +27 -0
- package/dist/lib/encrypted-settings/types.js.map +1 -0
- package/dist/lib/exif-stripper.d.ts +37 -22
- package/dist/lib/exif-stripper.d.ts.map +1 -1
- package/dist/lib/exif-stripper.js +101 -41
- package/dist/lib/exif-stripper.js.map +1 -1
- package/dist/lib/media/cas-keys.d.ts +63 -0
- package/dist/lib/media/cas-keys.d.ts.map +1 -0
- package/dist/lib/media/cas-keys.js +102 -0
- package/dist/lib/media/cas-keys.js.map +1 -0
- package/dist/lib/media/classify-worker-error.d.ts +48 -0
- package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
- package/dist/lib/media/classify-worker-error.js +319 -0
- package/dist/lib/media/classify-worker-error.js.map +1 -0
- package/dist/lib/media/dedupe-key.d.ts +29 -0
- package/dist/lib/media/dedupe-key.d.ts.map +1 -0
- package/dist/lib/media/dedupe-key.js +49 -0
- package/dist/lib/media/dedupe-key.js.map +1 -0
- package/dist/lib/media/duration-cap.d.ts +30 -0
- package/dist/lib/media/duration-cap.d.ts.map +1 -0
- package/dist/lib/media/duration-cap.js +37 -0
- package/dist/lib/media/duration-cap.js.map +1 -0
- package/dist/lib/media/ffmpeg-args.d.ts +83 -0
- package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
- package/dist/lib/media/ffmpeg-args.js +119 -0
- package/dist/lib/media/ffmpeg-args.js.map +1 -0
- package/dist/lib/media/media-ports.d.ts +126 -0
- package/dist/lib/media/media-ports.d.ts.map +1 -0
- package/dist/lib/media/media-ports.js +129 -0
- package/dist/lib/media/media-ports.js.map +1 -0
- package/dist/lib/media/media-upsert.d.ts +55 -0
- package/dist/lib/media/media-upsert.d.ts.map +1 -0
- package/dist/lib/media/media-upsert.js +38 -0
- package/dist/lib/media/media-upsert.js.map +1 -0
- package/dist/lib/media/moderation-provider.d.ts +111 -0
- package/dist/lib/media/moderation-provider.d.ts.map +1 -0
- package/dist/lib/media/moderation-provider.js +130 -0
- package/dist/lib/media/moderation-provider.js.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.js +37 -0
- package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
- package/dist/lib/media/moderation-status.d.ts +98 -0
- package/dist/lib/media/moderation-status.d.ts.map +1 -0
- package/dist/lib/media/moderation-status.js +122 -0
- package/dist/lib/media/moderation-status.js.map +1 -0
- package/dist/lib/media/processing-types.d.ts +45 -0
- package/dist/lib/media/processing-types.d.ts.map +1 -0
- package/dist/lib/media/processing-types.js +9 -0
- package/dist/lib/media/processing-types.js.map +1 -0
- package/dist/lib/media/promote-decision.d.ts +64 -0
- package/dist/lib/media/promote-decision.d.ts.map +1 -0
- package/dist/lib/media/promote-decision.js +76 -0
- package/dist/lib/media/promote-decision.js.map +1 -0
- package/dist/lib/media/quota-check.d.ts +22 -0
- package/dist/lib/media/quota-check.d.ts.map +1 -0
- package/dist/lib/media/quota-check.js +42 -0
- package/dist/lib/media/quota-check.js.map +1 -0
- package/dist/lib/media/quota-types.d.ts +15 -0
- package/dist/lib/media/quota-types.d.ts.map +1 -0
- package/dist/lib/media/quota-types.js +9 -0
- package/dist/lib/media/quota-types.js.map +1 -0
- package/dist/lib/media/route-upload.d.ts +58 -0
- package/dist/lib/media/route-upload.d.ts.map +1 -0
- package/dist/lib/media/route-upload.js +80 -0
- package/dist/lib/media/route-upload.js.map +1 -0
- package/dist/lib/media/serve-gate.d.ts +51 -0
- package/dist/lib/media/serve-gate.d.ts.map +1 -0
- package/dist/lib/media/serve-gate.js +68 -0
- package/dist/lib/media/serve-gate.js.map +1 -0
- package/dist/lib/media/tenant-resolution.d.ts +42 -0
- package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
- package/dist/lib/media/tenant-resolution.js +45 -0
- package/dist/lib/media/tenant-resolution.js.map +1 -0
- package/dist/lib/media/text-moderation.d.ts +28 -0
- package/dist/lib/media/text-moderation.d.ts.map +1 -0
- package/dist/lib/media/text-moderation.js +62 -0
- package/dist/lib/media/text-moderation.js.map +1 -0
- package/dist/lib/media/track-verdict.d.ts +45 -0
- package/dist/lib/media/track-verdict.d.ts.map +1 -0
- package/dist/lib/media/track-verdict.js +52 -0
- package/dist/lib/media/track-verdict.js.map +1 -0
- package/dist/lib/media/transcript-moderation.d.ts +47 -0
- package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
- package/dist/lib/media/transcript-moderation.js +70 -0
- package/dist/lib/media/transcript-moderation.js.map +1 -0
- package/dist/lib/media-handler.d.ts.map +1 -1
- package/dist/lib/media-handler.js +15 -9
- package/dist/lib/media-handler.js.map +1 -1
- package/dist/lib/notification-handler.d.ts +11 -4
- package/dist/lib/notification-handler.d.ts.map +1 -1
- package/dist/lib/notification-handler.js +161 -29
- package/dist/lib/notification-handler.js.map +1 -1
- package/dist/lib/post-handler.d.ts.map +1 -1
- package/dist/lib/post-handler.js +4 -1
- package/dist/lib/post-handler.js.map +1 -1
- package/dist/lib/realtime/block-store.d.ts +61 -0
- package/dist/lib/realtime/block-store.d.ts.map +1 -0
- package/dist/lib/realtime/block-store.js +0 -0
- package/dist/lib/realtime/block-store.js.map +1 -0
- package/dist/lib/realtime/channel.d.ts +34 -0
- package/dist/lib/realtime/channel.d.ts.map +1 -0
- package/dist/lib/realtime/channel.js +100 -0
- package/dist/lib/realtime/channel.js.map +1 -0
- package/dist/lib/realtime/delivery-policy.d.ts +51 -0
- package/dist/lib/realtime/delivery-policy.d.ts.map +1 -0
- package/dist/lib/realtime/delivery-policy.js +98 -0
- package/dist/lib/realtime/delivery-policy.js.map +1 -0
- package/dist/lib/realtime/index.d.ts +21 -0
- package/dist/lib/realtime/index.d.ts.map +1 -0
- package/dist/lib/realtime/index.js +39 -0
- package/dist/lib/realtime/index.js.map +1 -0
- package/dist/lib/realtime/no-op-transport.d.ts +10 -0
- package/dist/lib/realtime/no-op-transport.d.ts.map +1 -0
- package/dist/lib/realtime/no-op-transport.js +44 -0
- package/dist/lib/realtime/no-op-transport.js.map +1 -0
- package/dist/lib/realtime/poll-transport.d.ts +11 -0
- package/dist/lib/realtime/poll-transport.d.ts.map +1 -0
- package/dist/lib/realtime/poll-transport.js +68 -0
- package/dist/lib/realtime/poll-transport.js.map +1 -0
- package/dist/lib/realtime/push-notifier.d.ts +39 -0
- package/dist/lib/realtime/push-notifier.d.ts.map +1 -0
- package/dist/lib/realtime/push-notifier.js +76 -0
- package/dist/lib/realtime/push-notifier.js.map +1 -0
- package/dist/lib/realtime/realtime-transport.d.ts +2 -0
- package/dist/lib/realtime/realtime-transport.d.ts.map +1 -0
- package/dist/lib/realtime/realtime-transport.js +23 -0
- package/dist/lib/realtime/realtime-transport.js.map +1 -0
- package/dist/lib/realtime/setting-store.d.ts +30 -0
- package/dist/lib/realtime/setting-store.d.ts.map +1 -0
- package/dist/lib/realtime/setting-store.js +0 -0
- package/dist/lib/realtime/setting-store.js.map +1 -0
- package/dist/lib/realtime/types.d.ts +200 -0
- package/dist/lib/realtime/types.d.ts.map +1 -0
- package/dist/lib/realtime/types.js +61 -0
- package/dist/lib/realtime/types.js.map +1 -0
- package/dist/lib/routes/index.d.ts.map +1 -1
- package/dist/lib/routes/index.js +3 -0
- package/dist/lib/routes/index.js.map +1 -1
- package/dist/lib/routes/media.d.ts +21 -0
- package/dist/lib/routes/media.d.ts.map +1 -1
- package/dist/lib/routes/media.js +584 -483
- package/dist/lib/routes/media.js.map +1 -1
- package/dist/lib/routes/settings.d.ts +17 -0
- package/dist/lib/routes/settings.d.ts.map +1 -0
- package/dist/lib/routes/settings.js +187 -0
- package/dist/lib/routes/settings.js.map +1 -0
- package/dist/lib/services/image-normalizer.d.ts +64 -6
- package/dist/lib/services/image-normalizer.d.ts.map +1 -1
- package/dist/lib/services/image-normalizer.js +88 -6
- package/dist/lib/services/image-normalizer.js.map +1 -1
- package/dist/lib/services/media-upload-service.d.ts +2 -2
- package/dist/lib/services/media-upload-service.d.ts.map +1 -1
- package/dist/lib/services/media-upload-service.js +22 -21
- package/dist/lib/services/media-upload-service.js.map +1 -1
- package/dist/lib/tenant-scope.d.ts.map +1 -1
- package/dist/lib/tenant-scope.js +18 -1
- package/dist/lib/tenant-scope.js.map +1 -1
- package/package.json +23 -22
- package/prisma/migrations/20260620051144_add_encrypted_user_settings/migration.sql +24 -0
- package/prisma/migrations/20260620120000_add_blocked_users/migration.sql +29 -0
- package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
- package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
- package/prisma/schema.prisma +133 -15
- package/src/lambda/media-completion-worker.ts +567 -0
- 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"}
|