@aotter/mantle-runtime 0.0.11-alpha.13 → 0.0.11-alpha.14
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/domain/port/MediaAssetRepository.d.ts +34 -0
- package/dist/domain/port/MediaAssetRepository.d.ts.map +1 -0
- package/dist/domain/port/MediaAssetRepository.js +2 -0
- package/dist/domain/port/MediaAssetRepository.js.map +1 -0
- package/dist/domain/port/MediaStorage.d.ts +118 -44
- package/dist/domain/port/MediaStorage.d.ts.map +1 -1
- package/dist/domain/port/MediaStorage.js +12 -1
- package/dist/domain/port/MediaStorage.js.map +1 -1
- package/dist/domain/port/SiteConfigRepository.d.ts +2 -2
- package/dist/domain/port/SiteConfigRepository.d.ts.map +1 -1
- package/dist/domain/port/index.d.ts +3 -1
- package/dist/domain/port/index.d.ts.map +1 -1
- package/dist/domain/port/index.js +1 -0
- package/dist/domain/port/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/boot/canonicalMigrations.d.ts.map +1 -1
- package/dist/infrastructure/boot/canonicalMigrations.js +17 -0
- package/dist/infrastructure/boot/canonicalMigrations.js.map +1 -1
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts +6 -3
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.d.ts.map +1 -1
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js +26 -11
- package/dist/infrastructure/mcp/McpJsonRpcDispatcher.js.map +1 -1
- package/dist/infrastructure/mcp/McpToolCatalog.d.ts +7 -4
- package/dist/infrastructure/mcp/McpToolCatalog.d.ts.map +1 -1
- package/dist/infrastructure/mcp/McpToolCatalog.js +62 -26
- package/dist/infrastructure/mcp/McpToolCatalog.js.map +1 -1
- package/dist/infrastructure/persistence/DatabaseMediaAssetRepository.d.ts +27 -0
- package/dist/infrastructure/persistence/DatabaseMediaAssetRepository.d.ts.map +1 -0
- package/dist/infrastructure/persistence/DatabaseMediaAssetRepository.js +80 -0
- package/dist/infrastructure/persistence/DatabaseMediaAssetRepository.js.map +1 -0
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts +2 -2
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.d.ts.map +1 -1
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.js +24 -3
- package/dist/infrastructure/persistence/DatabaseSiteConfigRepository.js.map +1 -1
- package/dist/infrastructure/persistence/index.d.ts +1 -0
- package/dist/infrastructure/persistence/index.d.ts.map +1 -1
- package/dist/infrastructure/persistence/index.js +1 -0
- package/dist/infrastructure/persistence/index.js.map +1 -1
- package/dist/runtime.d.ts +10 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +6 -2
- package/dist/runtime.js.map +1 -1
- package/dist/usecase/dto/media/index.d.ts +31 -15
- package/dist/usecase/dto/media/index.d.ts.map +1 -1
- package/dist/usecase/dto/media/index.js +10 -3
- package/dist/usecase/dto/media/index.js.map +1 -1
- package/dist/usecase/media/CommitMediaUploadUseCase.d.ts +14 -4
- package/dist/usecase/media/CommitMediaUploadUseCase.d.ts.map +1 -1
- package/dist/usecase/media/CommitMediaUploadUseCase.js +30 -19
- package/dist/usecase/media/CommitMediaUploadUseCase.js.map +1 -1
- package/dist/usecase/media/CreateMediaUploadUseCase.d.ts +34 -2
- package/dist/usecase/media/CreateMediaUploadUseCase.d.ts.map +1 -1
- package/dist/usecase/media/CreateMediaUploadUseCase.js +165 -40
- package/dist/usecase/media/CreateMediaUploadUseCase.js.map +1 -1
- package/dist/usecase/media/PendingUploadRecord.d.ts +29 -10
- package/dist/usecase/media/PendingUploadRecord.d.ts.map +1 -1
- package/dist/usecase/media/diagnostics.d.ts +19 -2
- package/dist/usecase/media/diagnostics.d.ts.map +1 -1
- package/dist/usecase/media/diagnostics.js +66 -7
- package/dist/usecase/media/diagnostics.js.map +1 -1
- package/dist/usecase/media/mediaAllowlist.d.ts +1 -1
- package/dist/usecase/media/mediaAllowlist.d.ts.map +1 -1
- package/dist/usecase/media/mediaAllowlist.js +3 -0
- package/dist/usecase/media/mediaAllowlist.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Media use-case DTOs. Distinct from `MediaStorage` port request types:
|
|
3
|
-
* the use case receives caller-supplied
|
|
4
|
-
* (
|
|
5
|
-
* adapter
|
|
3
|
+
* the use case receives caller-supplied variant declarations, derives
|
|
4
|
+
* the rest (uploadGroupId via IdGenerator, storage keys via the
|
|
5
|
+
* adapter, expiresAt via the clock, per-mime maxBytes via site
|
|
6
|
+
* config), and forwards port-shaped requests internally.
|
|
7
|
+
*
|
|
8
|
+
* The shape is multi-variant from the start (#272): one
|
|
9
|
+
* `create_media_upload` call yields N presigned URLs (one per
|
|
10
|
+
* format), and one `commit_media_upload` finalises the whole
|
|
11
|
+
* bundle. Optimization runs agent-side via
|
|
12
|
+
* `@aotter/mantle-media-tools` — the runtime never transforms bytes.
|
|
6
13
|
*/
|
|
7
14
|
export {};
|
|
8
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/usecase/dto/media/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/usecase/dto/media/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import type { Clock } from "../../domain/port/Clock.js";
|
|
2
2
|
import type { KvCache } from "../../domain/port/KvCache.js";
|
|
3
|
+
import type { MediaAssetRepository } from "../../domain/port/MediaAssetRepository.js";
|
|
3
4
|
import type { MediaAsset, MediaStorage } from "../../domain/port/MediaStorage.js";
|
|
4
5
|
import type { CommitMediaUploadRequest } from "../dto/media/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Finalise the variant bundle issued by `create_media_upload`. Reads
|
|
8
|
+
* the `PendingUploadRecord` from KV (each variant's expected
|
|
9
|
+
* mime + size + storageKey), asks the adapter to verify every
|
|
10
|
+
* uploaded object (HEAD + bytes), and on success persists the
|
|
11
|
+
* resulting `MediaAsset` to the `media_assets` table.
|
|
12
|
+
*
|
|
13
|
+
* All-or-nothing: any variant failing HEAD-verify rejects the whole
|
|
14
|
+
* commit. The orphan sweeper (#254) cleans up partially-uploaded
|
|
15
|
+
* bundles whose pending record expired.
|
|
16
|
+
*/
|
|
5
17
|
export declare class CommitMediaUploadUseCase {
|
|
6
18
|
private readonly storage;
|
|
7
19
|
private readonly kv;
|
|
8
20
|
private readonly clock;
|
|
9
|
-
private readonly
|
|
10
|
-
constructor(storage: MediaStorage, kv: KvCache, clock: Clock,
|
|
11
|
-
readonly maxBytes?: number;
|
|
12
|
-
});
|
|
21
|
+
private readonly assets;
|
|
22
|
+
constructor(storage: MediaStorage, kv: KvCache, clock: Clock, assets: MediaAssetRepository);
|
|
13
23
|
execute(request: CommitMediaUploadRequest): Promise<MediaAsset>;
|
|
14
24
|
}
|
|
15
25
|
//# sourceMappingURL=CommitMediaUploadUseCase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommitMediaUploadUseCase.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/CommitMediaUploadUseCase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"CommitMediaUploadUseCase.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/CommitMediaUploadUseCase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,KAAK,EAEV,UAAU,EACV,YAAY,EACb,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAKtE;;;;;;;;;;GAUG;AACH,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAHN,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,oBAAoB;IAGzC,OAAO,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,UAAU,CAAC;CAkCtE"}
|
|
@@ -1,40 +1,51 @@
|
|
|
1
1
|
import { DiagnosticError } from "@aotter/mantle-spec";
|
|
2
|
-
import { mediaUploadExpiredDiagnostic
|
|
3
|
-
import {
|
|
2
|
+
import { mediaUploadExpiredDiagnostic } from "./diagnostics.js";
|
|
3
|
+
import { PENDING_UPLOAD_KV_PREFIX } from "./mediaAllowlist.js";
|
|
4
|
+
/**
|
|
5
|
+
* Finalise the variant bundle issued by `create_media_upload`. Reads
|
|
6
|
+
* the `PendingUploadRecord` from KV (each variant's expected
|
|
7
|
+
* mime + size + storageKey), asks the adapter to verify every
|
|
8
|
+
* uploaded object (HEAD + bytes), and on success persists the
|
|
9
|
+
* resulting `MediaAsset` to the `media_assets` table.
|
|
10
|
+
*
|
|
11
|
+
* All-or-nothing: any variant failing HEAD-verify rejects the whole
|
|
12
|
+
* commit. The orphan sweeper (#254) cleans up partially-uploaded
|
|
13
|
+
* bundles whose pending record expired.
|
|
14
|
+
*/
|
|
4
15
|
export class CommitMediaUploadUseCase {
|
|
5
16
|
storage;
|
|
6
17
|
kv;
|
|
7
18
|
clock;
|
|
8
|
-
|
|
9
|
-
constructor(storage, kv, clock,
|
|
19
|
+
assets;
|
|
20
|
+
constructor(storage, kv, clock, assets) {
|
|
10
21
|
this.storage = storage;
|
|
11
22
|
this.kv = kv;
|
|
12
23
|
this.clock = clock;
|
|
13
|
-
this.
|
|
24
|
+
this.assets = assets;
|
|
14
25
|
}
|
|
15
26
|
async execute(request) {
|
|
16
27
|
const opPath = "usecase/CommitMediaUpload";
|
|
17
|
-
const kvKey = `${PENDING_UPLOAD_KV_PREFIX}${request.
|
|
28
|
+
const kvKey = `${PENDING_UPLOAD_KV_PREFIX}${request.uploadGroupId}`;
|
|
18
29
|
const raw = await this.kv.get(kvKey);
|
|
19
30
|
if (!raw) {
|
|
20
|
-
throw new DiagnosticError(mediaUploadExpiredDiagnostic(opPath, request.
|
|
31
|
+
throw new DiagnosticError(mediaUploadExpiredDiagnostic(opPath, request.uploadGroupId));
|
|
21
32
|
}
|
|
22
33
|
const record = JSON.parse(raw);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
const variantSpecs = record.variants.map((v) => ({
|
|
35
|
+
mimeType: v.mimeType,
|
|
36
|
+
role: v.role,
|
|
37
|
+
storageKey: v.storageKey,
|
|
38
|
+
maxBytes: Math.min(v.maxBytes, v.expectedSize),
|
|
39
|
+
}));
|
|
28
40
|
const asset = await this.storage.commitUpload({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
caption: request.caption,
|
|
35
|
-
checksum: request.checksum,
|
|
41
|
+
uploadGroupId: request.uploadGroupId,
|
|
42
|
+
filename: record.filename,
|
|
43
|
+
variants: variantSpecs,
|
|
44
|
+
alt: request.alt ?? record.alt,
|
|
45
|
+
caption: request.caption ?? record.caption,
|
|
36
46
|
now: this.clock.now(),
|
|
37
47
|
});
|
|
48
|
+
await this.assets.save(asset);
|
|
38
49
|
// Best-effort cleanup. KV TTL covers us if delete fails.
|
|
39
50
|
await this.kv.delete(kvKey).catch(() => undefined);
|
|
40
51
|
return asset;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommitMediaUploadUseCase.js","sourceRoot":"","sources":["../../../src/usecase/media/CommitMediaUploadUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"CommitMediaUploadUseCase.js","sourceRoot":"","sources":["../../../src/usecase/media/CommitMediaUploadUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAUtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAG/D;;;;;;;;;;GAUG;AACH,MAAM,OAAO,wBAAwB;IAEhB;IACA;IACA;IACA;IAJnB,YACmB,OAAqB,EACrB,EAAW,EACX,KAAY,EACZ,MAA4B;QAH5B,YAAO,GAAP,OAAO,CAAc;QACrB,OAAE,GAAF,EAAE,CAAS;QACX,UAAK,GAAL,KAAK,CAAO;QACZ,WAAM,GAAN,MAAM,CAAsB;IAC5C,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,OAAiC;QAC7C,MAAM,MAAM,GAAG,2BAA2B,CAAC;QAC3C,MAAM,KAAK,GAAG,GAAG,wBAAwB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;QACpE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,eAAe,CACvB,4BAA4B,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,CAC5D,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAwB,CAAC;QAEtD,MAAM,YAAY,GAA2C,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvF,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,YAAY,CAAC;SAC/C,CAAC,CAAC,CAAC;QAEJ,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YAC5C,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,YAAY;YACtB,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO;YAC1C,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE9B,yDAAyD;QACzD,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAEnD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -1,18 +1,50 @@
|
|
|
1
1
|
import type { Clock } from "../../domain/port/Clock.js";
|
|
2
|
+
import type { IdGenerator } from "../../domain/port/IdGenerator.js";
|
|
2
3
|
import type { KvCache } from "../../domain/port/KvCache.js";
|
|
3
4
|
import type { MediaStorage } from "../../domain/port/MediaStorage.js";
|
|
4
5
|
import type { SiteConfigRepository } from "../../domain/port/SiteConfigRepository.js";
|
|
5
6
|
import type { CreateMediaUploadRequest, CreateMediaUploadResponse } from "../dto/media/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Issue presigned upload capabilities for every variant the agent
|
|
9
|
+
* declares for one logical media asset (#272). The asset's `id` is
|
|
10
|
+
* minted here and surfaces as `uploadGroupId` in the response — the
|
|
11
|
+
* caller passes it back to `commit_media_upload`.
|
|
12
|
+
*
|
|
13
|
+
* Optimization runs agent-side via `@aotter/mantle-media-tools`. This
|
|
14
|
+
* use case enforces policy only — never transforms bytes:
|
|
15
|
+
* - purpose must be declared in `siteDefaults.media.purposes`
|
|
16
|
+
* - variants must cover every required mime for that purpose
|
|
17
|
+
* - each variant's byteSize ≤ `policy.maxBytes[mime]`
|
|
18
|
+
* - mime allowlist (still gates per variant; SVG opt-in via ctor)
|
|
19
|
+
* - "suspicious shape" heuristic: modern formats (avif / webp) MUST
|
|
20
|
+
* NOT be larger than the fallback (jpeg) — uploader skipped
|
|
21
|
+
* optimization → hard fail rather than emit a `<picture>` slower
|
|
22
|
+
* than the bare `<img>`.
|
|
23
|
+
*/
|
|
6
24
|
export declare class CreateMediaUploadUseCase {
|
|
7
25
|
private readonly storage;
|
|
8
26
|
private readonly kv;
|
|
9
27
|
private readonly clock;
|
|
28
|
+
private readonly idgen;
|
|
10
29
|
private readonly siteConfig;
|
|
11
30
|
private readonly opts;
|
|
12
|
-
constructor(storage: MediaStorage, kv: KvCache, clock: Clock, siteConfig: SiteConfigRepository, opts?: {
|
|
31
|
+
constructor(storage: MediaStorage, kv: KvCache, clock: Clock, idgen: IdGenerator, siteConfig: SiteConfigRepository, opts?: {
|
|
13
32
|
readonly allowSvg: boolean;
|
|
14
|
-
readonly maxBytes?: number;
|
|
15
33
|
});
|
|
16
34
|
execute(request: CreateMediaUploadRequest): Promise<CreateMediaUploadResponse>;
|
|
35
|
+
private assertVariantsCoverPolicy;
|
|
36
|
+
/** Enforce per-mime byte caps from the purpose policy BEFORE the
|
|
37
|
+
* adapter signs presigned PUT URLs. Mantle docs / ADR-0017
|
|
38
|
+
* promise this gate at create time — without it, oversized
|
|
39
|
+
* variants only fail after R2 has accepted the bytes (wasted
|
|
40
|
+
* upload + potential bill).
|
|
41
|
+
*
|
|
42
|
+
* Also gates on positive-integer byteSize. JSON-RPC and HTTP
|
|
43
|
+
* validation only narrow to `typeof === "number"`, so 0, -1, or
|
|
44
|
+
* 0.5 can reach here and produce signed URLs against a maxBytes
|
|
45
|
+
* comparison that accepts them. */
|
|
46
|
+
private assertVariantSizesUnderCap;
|
|
47
|
+
private assertEachVariantAccepted;
|
|
48
|
+
private assertNoSuspiciousSizing;
|
|
17
49
|
}
|
|
18
50
|
//# sourceMappingURL=CreateMediaUploadUseCase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateMediaUploadUseCase.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/CreateMediaUploadUseCase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"CreateMediaUploadUseCase.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/CreateMediaUploadUseCase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAEV,YAAY,EACb,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,KAAK,EACV,wBAAwB,EACxB,yBAAyB,EAE1B,MAAM,uBAAuB,CAAC;AAqB/B;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;gBALJ,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,OAAO,EACX,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,oBAAoB,EAChC,IAAI,GAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;KAAwB;IAGvE,OAAO,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC;IA0EpF,OAAO,CAAC,yBAAyB;IA8CjC;;;;;;;;;wCASoC;IACpC,OAAO,CAAC,0BAA0B;IA4BlC,OAAO,CAAC,yBAAyB;IAcjC,OAAO,CAAC,wBAAwB;CAmBjC"}
|
|
@@ -1,71 +1,196 @@
|
|
|
1
1
|
import { DiagnosticError } from "@aotter/mantle-spec";
|
|
2
|
-
import { mediaMimeRejectedDiagnostic, mediaPurposeRejectedDiagnostic,
|
|
3
|
-
import {
|
|
2
|
+
import { mediaMimeRejectedDiagnostic, mediaPurposeRejectedDiagnostic, mediaSvgRejectedDiagnostic, mediaVariantSizeExceededDiagnostic, mediaVariantsIncompleteDiagnostic, mediaVariantsSuspiciousSizeDiagnostic, } from "./diagnostics.js";
|
|
3
|
+
import { MEDIA_SVG_MIME, PENDING_UPLOAD_KV_PREFIX, PENDING_UPLOAD_KV_TTL_SECONDS, UPLOAD_URL_TTL_SECONDS, isAllowedMime, } from "./mediaAllowlist.js";
|
|
4
|
+
/**
|
|
5
|
+
* Issue presigned upload capabilities for every variant the agent
|
|
6
|
+
* declares for one logical media asset (#272). The asset's `id` is
|
|
7
|
+
* minted here and surfaces as `uploadGroupId` in the response — the
|
|
8
|
+
* caller passes it back to `commit_media_upload`.
|
|
9
|
+
*
|
|
10
|
+
* Optimization runs agent-side via `@aotter/mantle-media-tools`. This
|
|
11
|
+
* use case enforces policy only — never transforms bytes:
|
|
12
|
+
* - purpose must be declared in `siteDefaults.media.purposes`
|
|
13
|
+
* - variants must cover every required mime for that purpose
|
|
14
|
+
* - each variant's byteSize ≤ `policy.maxBytes[mime]`
|
|
15
|
+
* - mime allowlist (still gates per variant; SVG opt-in via ctor)
|
|
16
|
+
* - "suspicious shape" heuristic: modern formats (avif / webp) MUST
|
|
17
|
+
* NOT be larger than the fallback (jpeg) — uploader skipped
|
|
18
|
+
* optimization → hard fail rather than emit a `<picture>` slower
|
|
19
|
+
* than the bare `<img>`.
|
|
20
|
+
*/
|
|
4
21
|
export class CreateMediaUploadUseCase {
|
|
5
22
|
storage;
|
|
6
23
|
kv;
|
|
7
24
|
clock;
|
|
25
|
+
idgen;
|
|
8
26
|
siteConfig;
|
|
9
27
|
opts;
|
|
10
|
-
constructor(storage, kv, clock, siteConfig, opts = {
|
|
11
|
-
allowSvg: false,
|
|
12
|
-
}) {
|
|
28
|
+
constructor(storage, kv, clock, idgen, siteConfig, opts = { allowSvg: false }) {
|
|
13
29
|
this.storage = storage;
|
|
14
30
|
this.kv = kv;
|
|
15
31
|
this.clock = clock;
|
|
32
|
+
this.idgen = idgen;
|
|
16
33
|
this.siteConfig = siteConfig;
|
|
17
34
|
this.opts = opts;
|
|
18
35
|
}
|
|
19
36
|
async execute(request) {
|
|
20
37
|
const opPath = "usecase/CreateMediaUpload";
|
|
21
|
-
const maxBytes = this.opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
22
|
-
// Fail-closed purpose enforcement (#262). Empty `media.purposes` —
|
|
23
|
-
// either undeclared or operator-cleared — disables uploads entirely
|
|
24
|
-
// at this layer; the MCP tool catalog + admin endpoint also gate on
|
|
25
|
-
// the same condition for cleaner UX. Reading per request rather
|
|
26
|
-
// than snapshotting at boot lets operator edits via the admin
|
|
27
|
-
// Settings page take effect without a redeploy.
|
|
28
38
|
const declared = await this.siteConfig.readMediaPurposes();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new DiagnosticError(mediaSvgRejectedDiagnostic(opPath));
|
|
34
|
-
}
|
|
35
|
-
if (!isAllowedMime(request.mimeType, this.opts.allowSvg)) {
|
|
36
|
-
throw new DiagnosticError(mediaMimeRejectedDiagnostic(opPath, request.mimeType));
|
|
37
|
-
}
|
|
38
|
-
// Enforce the ceiling before minting a presigned URL — see
|
|
39
|
-
// CreateMediaUploadRequest.byteSize for the WHY.
|
|
40
|
-
if (request.byteSize > maxBytes) {
|
|
41
|
-
throw new DiagnosticError(mediaSizeExceededDiagnostic(opPath, request.byteSize, maxBytes));
|
|
39
|
+
const declaredNames = declared.map((p) => p.name);
|
|
40
|
+
const policy = declared.find((p) => p.name === request.purpose);
|
|
41
|
+
if (!policy) {
|
|
42
|
+
throw new DiagnosticError(mediaPurposeRejectedDiagnostic(opPath, request.purpose, declaredNames));
|
|
42
43
|
}
|
|
44
|
+
this.assertVariantsCoverPolicy(opPath, request, policy);
|
|
45
|
+
this.assertEachVariantAccepted(opPath, request);
|
|
46
|
+
this.assertVariantSizesUnderCap(opPath, request, policy);
|
|
47
|
+
this.assertNoSuspiciousSizing(opPath, request);
|
|
43
48
|
const now = this.clock.now();
|
|
44
49
|
const expiresAt = now + UPLOAD_URL_TTL_SECONDS * 1000;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
mimeType:
|
|
48
|
-
byteSize:
|
|
49
|
-
maxBytes,
|
|
50
|
+
const uploadGroupId = this.idgen.next();
|
|
51
|
+
const variantSpecs = request.variants.map((v) => ({
|
|
52
|
+
mimeType: v.mimeType,
|
|
53
|
+
byteSize: v.byteSize,
|
|
54
|
+
maxBytes: policy.maxBytes[v.mimeType] ?? Number.MAX_SAFE_INTEGER,
|
|
55
|
+
role: v.role,
|
|
56
|
+
}));
|
|
57
|
+
const result = await this.storage.createUpload({
|
|
58
|
+
uploadGroupId,
|
|
50
59
|
purpose: request.purpose,
|
|
60
|
+
filename: request.filename,
|
|
61
|
+
variants: variantSpecs,
|
|
51
62
|
now,
|
|
52
63
|
expiresAt,
|
|
53
64
|
});
|
|
65
|
+
const pendingVariants = result.capabilities.map((cap, i) => ({
|
|
66
|
+
mimeType: cap.mimeType,
|
|
67
|
+
role: cap.role,
|
|
68
|
+
storageKey: cap.storageKey,
|
|
69
|
+
expectedSize: variantSpecs[i].byteSize,
|
|
70
|
+
maxBytes: variantSpecs[i].maxBytes,
|
|
71
|
+
}));
|
|
54
72
|
const record = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
purpose: request.purpose,
|
|
74
|
+
filename: request.filename,
|
|
75
|
+
variants: pendingVariants,
|
|
76
|
+
alt: request.alt,
|
|
77
|
+
caption: request.caption,
|
|
78
|
+
expiresAt: result.expiresAt,
|
|
59
79
|
createdAt: now,
|
|
60
80
|
};
|
|
61
|
-
await this.kv.put(`${PENDING_UPLOAD_KV_PREFIX}${
|
|
81
|
+
await this.kv.put(`${PENDING_UPLOAD_KV_PREFIX}${result.uploadGroupId}`, JSON.stringify(record), { expirationTtl: PENDING_UPLOAD_KV_TTL_SECONDS });
|
|
62
82
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
uploadGroupId: result.uploadGroupId,
|
|
84
|
+
capabilities: result.capabilities.map((cap) => ({
|
|
85
|
+
mimeType: cap.mimeType,
|
|
86
|
+
role: cap.role,
|
|
87
|
+
method: cap.method,
|
|
88
|
+
uploadUrl: cap.uploadUrl,
|
|
89
|
+
requiredHeaders: cap.requiredHeaders,
|
|
90
|
+
})),
|
|
91
|
+
expiresAt: result.expiresAt,
|
|
68
92
|
};
|
|
69
93
|
}
|
|
94
|
+
assertVariantsCoverPolicy(opPath, request, policy) {
|
|
95
|
+
const supplied = request.variants.map((v) => v.mimeType);
|
|
96
|
+
const missing = policy.required.filter((m) => !supplied.includes(m));
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
throw new DiagnosticError(mediaVariantsIncompleteDiagnostic(opPath, policy.name, policy.required, supplied));
|
|
99
|
+
}
|
|
100
|
+
// Closed set: the policy declares which mimes belong to this
|
|
101
|
+
// purpose. Extras (e.g. an `image/png` alongside the declared
|
|
102
|
+
// avif/webp/jpeg trio) would land without a per-mime byte cap
|
|
103
|
+
// and bypass the size gate entirely. Reject any supplied mime
|
|
104
|
+
// outside `policy.required`.
|
|
105
|
+
const allowed = new Set(policy.required);
|
|
106
|
+
const extras = request.variants.filter((v) => !allowed.has(v.mimeType));
|
|
107
|
+
if (extras.length > 0) {
|
|
108
|
+
throw new DiagnosticError(mediaVariantsIncompleteDiagnostic(opPath, policy.name, policy.required, supplied));
|
|
109
|
+
}
|
|
110
|
+
// Exactly one primary. Storage key layout is `<group>/<role>.<ext>`,
|
|
111
|
+
// so two primaries collide on the same R2 key + ambiguate which
|
|
112
|
+
// variant `<img>` falls back to. Same for any duplicated (mime, role)
|
|
113
|
+
// pair — two `image/jpeg` alternates would also collide on key.
|
|
114
|
+
const primaries = request.variants.filter((v) => v.role === "primary");
|
|
115
|
+
if (primaries.length !== 1) {
|
|
116
|
+
throw new DiagnosticError(mediaVariantsIncompleteDiagnostic(opPath, policy.name, policy.required, supplied));
|
|
117
|
+
}
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const v of request.variants) {
|
|
120
|
+
const key = `${v.mimeType}/${v.role}`;
|
|
121
|
+
if (seen.has(key)) {
|
|
122
|
+
throw new DiagnosticError(mediaVariantsIncompleteDiagnostic(opPath, policy.name, policy.required, supplied));
|
|
123
|
+
}
|
|
124
|
+
seen.add(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Enforce per-mime byte caps from the purpose policy BEFORE the
|
|
128
|
+
* adapter signs presigned PUT URLs. Mantle docs / ADR-0017
|
|
129
|
+
* promise this gate at create time — without it, oversized
|
|
130
|
+
* variants only fail after R2 has accepted the bytes (wasted
|
|
131
|
+
* upload + potential bill).
|
|
132
|
+
*
|
|
133
|
+
* Also gates on positive-integer byteSize. JSON-RPC and HTTP
|
|
134
|
+
* validation only narrow to `typeof === "number"`, so 0, -1, or
|
|
135
|
+
* 0.5 can reach here and produce signed URLs against a maxBytes
|
|
136
|
+
* comparison that accepts them. */
|
|
137
|
+
assertVariantSizesUnderCap(opPath, request, policy) {
|
|
138
|
+
for (const v of request.variants) {
|
|
139
|
+
if (!Number.isSafeInteger(v.byteSize) || v.byteSize <= 0) {
|
|
140
|
+
throw new DiagnosticError(mediaVariantSizeExceededDiagnostic(opPath, v.mimeType, v.byteSize, 0));
|
|
141
|
+
}
|
|
142
|
+
const cap = policy.maxBytes[v.mimeType];
|
|
143
|
+
// mime outside policy.required was already rejected in
|
|
144
|
+
// assertVariantsCoverPolicy; defensive check so a future
|
|
145
|
+
// refactor doesn't silently uncap.
|
|
146
|
+
if (cap === undefined) {
|
|
147
|
+
throw new DiagnosticError(mediaVariantSizeExceededDiagnostic(opPath, v.mimeType, v.byteSize, 0));
|
|
148
|
+
}
|
|
149
|
+
if (v.byteSize > cap) {
|
|
150
|
+
throw new DiagnosticError(mediaVariantSizeExceededDiagnostic(opPath, v.mimeType, v.byteSize, cap));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
assertEachVariantAccepted(opPath, request) {
|
|
155
|
+
for (const v of request.variants) {
|
|
156
|
+
if (v.mimeType === MEDIA_SVG_MIME && !this.opts.allowSvg) {
|
|
157
|
+
throw new DiagnosticError(mediaSvgRejectedDiagnostic(opPath));
|
|
158
|
+
}
|
|
159
|
+
if (!isAllowedMime(v.mimeType, this.opts.allowSvg)) {
|
|
160
|
+
throw new DiagnosticError(mediaMimeRejectedDiagnostic(opPath, v.mimeType));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
assertNoSuspiciousSizing(opPath, request) {
|
|
165
|
+
const fallback = pickFallbackForSizing(request.variants);
|
|
166
|
+
if (!fallback)
|
|
167
|
+
return;
|
|
168
|
+
for (const v of request.variants) {
|
|
169
|
+
if (v === fallback)
|
|
170
|
+
continue;
|
|
171
|
+
if (!MODERN_FORMATS.has(v.mimeType))
|
|
172
|
+
continue;
|
|
173
|
+
if (v.byteSize > fallback.byteSize) {
|
|
174
|
+
throw new DiagnosticError(mediaVariantsSuspiciousSizeDiagnostic(opPath, `${v.mimeType} (${v.byteSize}B) > ${fallback.mimeType} (${fallback.byteSize}B)`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Modern formats that, by convention, should be smaller than the
|
|
180
|
+
* universal fallback (jpeg / png). Used by the "suspicious sizing"
|
|
181
|
+
* heuristic — if any of these weigh more than the fallback, the
|
|
182
|
+
* uploader almost certainly skipped optimization. */
|
|
183
|
+
const MODERN_FORMATS = new Set(["image/avif", "image/webp"]);
|
|
184
|
+
/** Pick the variant to compare modern formats against — preferred
|
|
185
|
+
* order: jpeg → png → gif. Returns undefined when none of the
|
|
186
|
+
* classic-format variants are present (e.g. an avif-only upload),
|
|
187
|
+
* in which case the heuristic skips. */
|
|
188
|
+
function pickFallbackForSizing(variants) {
|
|
189
|
+
for (const mime of ["image/jpeg", "image/png", "image/gif"]) {
|
|
190
|
+
const found = variants.find((v) => v.mimeType === mime);
|
|
191
|
+
if (found)
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
70
195
|
}
|
|
71
196
|
//# sourceMappingURL=CreateMediaUploadUseCase.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateMediaUploadUseCase.js","sourceRoot":"","sources":["../../../src/usecase/media/CreateMediaUploadUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"CreateMediaUploadUseCase.js","sourceRoot":"","sources":["../../../src/usecase/media/CreateMediaUploadUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA2B,MAAM,qBAAqB,CAAC;AAc/E,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAC9B,0BAA0B,EAC1B,kCAAkC,EAClC,iCAAiC,EACjC,qCAAqC,GACtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,6BAA6B,EAC7B,sBAAsB,EACtB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAM7B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,wBAAwB;IAEhB;IACA;IACA;IACA;IACA;IACA;IANnB,YACmB,OAAqB,EACrB,EAAW,EACX,KAAY,EACZ,KAAkB,EAClB,UAAgC,EAChC,OAAuC,EAAE,QAAQ,EAAE,KAAK,EAAE;QAL1D,YAAO,GAAP,OAAO,CAAc;QACrB,OAAE,GAAF,EAAE,CAAS;QACX,UAAK,GAAL,KAAK,CAAO;QACZ,UAAK,GAAL,KAAK,CAAa;QAClB,eAAU,GAAV,UAAU,CAAsB;QAChC,SAAI,GAAJ,IAAI,CAAsD;IAC1E,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,OAAiC;QAC7C,MAAM,MAAM,GAAG,2BAA2B,CAAC;QAE3C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,iBAAiB,EAAE,CAAC;QAC3D,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,eAAe,CACvB,8BAA8B,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,CACvE,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxD,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChD,IAAI,CAAC,0BAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAE/C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,GAAG,GAAG,sBAAsB,GAAG,IAAI,CAAC;QACtD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAExC,MAAM,YAAY,GAA2C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxF,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,gBAAgB;YAChE,IAAI,EAAE,CAAC,CAAC,IAAI;SACb,CAAC,CAAC,CAAC;QAEJ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YAC7C,aAAa;YACb,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,YAAY;YACtB,GAAG;YACH,SAAS;SACV,CAAC,CAAC;QAEH,MAAM,eAAe,GAA2B,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YACnF,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,YAAY,EAAE,YAAY,CAAC,CAAC,CAAE,CAAC,QAAQ;YACvC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAE,CAAC,QAAQ;SACpC,CAAC,CAAC,CAAC;QAEJ,MAAM,MAAM,GAAwB;YAClC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,eAAe;YACzB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,SAAS,EAAE,GAAG;SACf,CAAC;QAEF,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CACf,GAAG,wBAAwB,GAAG,MAAM,CAAC,aAAa,EAAE,EACpD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EACtB,EAAE,aAAa,EAAE,6BAA6B,EAAE,CACjD,CAAC;QAEF,OAAO;YACL,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC9C,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,eAAe,EAAE,GAAG,CAAC,eAAe;aACrC,CAAC,CAAC;YACH,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC;IACJ,CAAC;IAEO,yBAAyB,CAC/B,MAAc,EACd,OAAiC,EACjC,MAA0B;QAE1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,eAAe,CACvB,iCAAiC,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAClF,CAAC;QACJ,CAAC;QACD,6DAA6D;QAC7D,8DAA8D;QAC9D,8DAA8D;QAC9D,8DAA8D;QAC9D,6BAA6B;QAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QACxE,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,eAAe,CACvB,iCAAiC,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAClF,CAAC;QACJ,CAAC;QACD,qEAAqE;QACrE,gEAAgE;QAChE,sEAAsE;QACtE,gEAAgE;QAChE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QACvE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,eAAe,CACvB,iCAAiC,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAClF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,MAAM,IAAI,eAAe,CACvB,iCAAiC,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAClF,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IAED;;;;;;;;;wCASoC;IAC5B,0BAA0B,CAChC,MAAc,EACd,OAAiC,EACjC,MAA0B;QAE1B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;gBACzD,MAAM,IAAI,eAAe,CACvB,kCAAkC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CACtE,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YACxC,uDAAuD;YACvD,yDAAyD;YACzD,mCAAmC;YACnC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBACtB,MAAM,IAAI,eAAe,CACvB,kCAAkC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CACtE,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;gBACrB,MAAM,IAAI,eAAe,CACvB,kCAAkC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CACxE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAEO,yBAAyB,CAC/B,MAAc,EACd,OAAiC;QAEjC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,CAAC,QAAQ,KAAK,cAAc,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACzD,MAAM,IAAI,eAAe,CAAC,0BAA0B,CAAC,MAAM,CAAC,CAAC,CAAC;YAChE,CAAC;YACD,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnD,MAAM,IAAI,eAAe,CAAC,2BAA2B,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC7E,CAAC;QACH,CAAC;IACH,CAAC;IAEO,wBAAwB,CAC9B,MAAc,EACd,OAAiC;QAEjC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,QAAQ;gBAAE,SAAS;YAC7B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC9C,IAAI,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACnC,MAAM,IAAI,eAAe,CACvB,qCAAqC,CACnC,MAAM,EACN,GAAG,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,QAAQ,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,IAAI,CAChF,CACF,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED;;;sDAGsD;AACtD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;AAE7D;;;yCAGyC;AACzC,SAAS,qBAAqB,CAC5B,QAAoD;IAEpD,KAAK,MAAM,IAAI,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QACxD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -1,17 +1,36 @@
|
|
|
1
|
+
import type { MediaVariantRole } from "../../domain/port/MediaStorage.js";
|
|
1
2
|
/** KV record persisted by `CreateMediaUploadUseCase` and read by
|
|
2
3
|
* `CommitMediaUploadUseCase`. Stored under
|
|
3
|
-
* `${PENDING_UPLOAD_KV_PREFIX}${
|
|
4
|
-
* `PENDING_UPLOAD_KV_TTL_SECONDS`.
|
|
4
|
+
* `${PENDING_UPLOAD_KV_PREFIX}${uploadGroupId}` with a TTL set to
|
|
5
|
+
* `PENDING_UPLOAD_KV_TTL_SECONDS`.
|
|
6
|
+
*
|
|
7
|
+
* Multi-variant from the start (#272): one record tracks every
|
|
8
|
+
* variant the agent declared, so commit can HEAD all N R2 objects
|
|
9
|
+
* in one atomic shot.
|
|
10
|
+
*/
|
|
5
11
|
export interface PendingUploadRecord {
|
|
6
|
-
readonly
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly
|
|
12
|
+
readonly purpose: string;
|
|
13
|
+
/** Original filename — forwarded by commit into R2 customMetadata
|
|
14
|
+
* on every variant. Preserved here so the commit path sees it
|
|
15
|
+
* without a second hop. */
|
|
16
|
+
readonly filename: string;
|
|
17
|
+
readonly variants: ReadonlyArray<PendingUploadVariant>;
|
|
18
|
+
readonly alt?: string;
|
|
19
|
+
readonly caption?: string;
|
|
14
20
|
readonly expiresAt: number;
|
|
15
21
|
readonly createdAt: number;
|
|
16
22
|
}
|
|
23
|
+
export interface PendingUploadVariant {
|
|
24
|
+
readonly mimeType: string;
|
|
25
|
+
readonly role: MediaVariantRole;
|
|
26
|
+
readonly storageKey: string;
|
|
27
|
+
/** Caller-declared byte size, persisted at create time so commit
|
|
28
|
+
* can enforce a tight per-variant ceiling rather than just the
|
|
29
|
+
* per-purpose `maxBytes`. The adapter ultimately verifies actual
|
|
30
|
+
* stored bytes ≤ this. */
|
|
31
|
+
readonly expectedSize: number;
|
|
32
|
+
/** Per-purpose `maxBytes[mimeType]`. Forwarded to the adapter so
|
|
33
|
+
* HEAD-verify rejects oversized PUTs with the right cap message. */
|
|
34
|
+
readonly maxBytes: number;
|
|
35
|
+
}
|
|
17
36
|
//# sourceMappingURL=PendingUploadRecord.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingUploadRecord.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/PendingUploadRecord.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"PendingUploadRecord.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/PendingUploadRecord.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAE1E;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;gCAE4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IACvD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;+BAG2B;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;yEACqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -3,8 +3,8 @@ export declare function mediaNotConfiguredDiagnostic(opPath: string): Diagnostic
|
|
|
3
3
|
export declare function mediaMimeRejectedDiagnostic(opPath: string, mime: string): Diagnostic;
|
|
4
4
|
export declare function mediaSvgRejectedDiagnostic(opPath: string): Diagnostic;
|
|
5
5
|
export declare function mediaSizeExceededDiagnostic(opPath: string, byteSize: number, maxBytes: number): Diagnostic;
|
|
6
|
-
export declare function mediaUploadExpiredDiagnostic(opPath: string,
|
|
7
|
-
export declare function mediaObjectNotFoundDiagnostic(opPath: string,
|
|
6
|
+
export declare function mediaUploadExpiredDiagnostic(opPath: string, uploadGroupId: string): Diagnostic;
|
|
7
|
+
export declare function mediaObjectNotFoundDiagnostic(opPath: string, uploadGroupId: string): Diagnostic;
|
|
8
8
|
/** Caller supplied a `purpose` that this deployment did not declare in
|
|
9
9
|
* `siteDefaults.media.purposes`. Fail-closed per #262: alpha has no
|
|
10
10
|
* production consumers, so there is no warn-and-allow compatibility
|
|
@@ -12,4 +12,21 @@ export declare function mediaObjectNotFoundDiagnostic(opPath: string, uploadId:
|
|
|
12
12
|
* field carries the declared set so agents can self-correct without
|
|
13
13
|
* another round trip. */
|
|
14
14
|
export declare function mediaPurposeRejectedDiagnostic(opPath: string, purpose: string | undefined, declared: readonly string[]): Diagnostic;
|
|
15
|
+
/** Caller's variants manifest is missing one or more mimes the
|
|
16
|
+
* purpose policy declares as required. The expected field carries
|
|
17
|
+
* the declared required set + the supplied set so the agent script
|
|
18
|
+
* can self-correct without another round trip. */
|
|
19
|
+
export declare function mediaVariantsIncompleteDiagnostic(opPath: string, purpose: string, required: readonly string[], supplied: readonly string[]): Diagnostic;
|
|
20
|
+
export declare function mediaVariantSizeExceededDiagnostic(opPath: string, mime: string, byteSize: number, maxBytes: number): Diagnostic;
|
|
21
|
+
/**
|
|
22
|
+
* Variant byte sizes don't match the format hierarchy the uploader
|
|
23
|
+
* promised — typically avif > webp > jpeg (each modern format
|
|
24
|
+
* smaller). When the upload manifest claims a modern format that's
|
|
25
|
+
* larger than the universal-fallback (e.g. `avif` weighing more than
|
|
26
|
+
* `jpeg`), the uploader almost certainly skipped optimization for
|
|
27
|
+
* that variant. Fail hard rather than emit a `<picture>` that's
|
|
28
|
+
* slower than the bare `<img>` fallback.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mediaVariantsSuspiciousSizeDiagnostic(opPath: string, detail: string): Diagnostic;
|
|
31
|
+
export declare function mediaAssetNotFoundDiagnostic(opPath: string, id: string): Diagnostic;
|
|
15
32
|
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAItE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAUvE;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CASpF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAUrE;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,UAAU,CASZ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../../src/usecase/media/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAItE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAUvE;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CASpF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAUrE;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,UAAU,CASZ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,UAAU,CAU9F;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,UAAU,CAU/F;AAED;;;;;0BAK0B;AAC1B,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,UAAU,CAiBZ;AAED;;;mDAGmD;AACnD,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,UAAU,CAaZ;AAED,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,UAAU,CAWZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,UAAU,CAYZ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAWnF"}
|