@roottale/cms-media 0.72.1 → 0.73.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/index.js CHANGED
@@ -144,7 +144,7 @@ function normalizeFilename(original) {
144
144
  }
145
145
  function suggestAltFromFilename(original) {
146
146
  let text = stemOfFilename(original).normalize("NFKC").replace(/%[0-9a-f]{2}/gi, " ").replace(/[()[\]{}]/g, " ").replace(/[._-]+/g, " ").replace(/\s+/g, " ").trim();
147
- text = text.replace(/\b(copy|final|edited|edit|export|upload)\b/gi, " ").replace(/\b(screenshot|screen shot|image|img|photo|picture|pic)\b/gi, " ").replace(/\b(dsc|dcim|pxl|kakaotalk|wechatimg)\s*\d+\b/gi, " ").replace(/\b\d{3,4}\s*x\s*\d{3,4}\b/gi, " ").replace(/\b(19|20)\d{2}[ ._-]?(0[1-9]|1[0-2])[ ._-]?([0-2]\d|3[01])\b/g, " ").replace(/\b\d{4,}\b/g, " ").replace(/\s+/g, " ").trim();
147
+ text = text.replace(/\b(copy|final|edited|edit|export|upload)\b/gi, " ").replace(/\b(screenshot|screen shot|image|img|photo|picture|pic)\b/gi, " ").replace(/\b(dsc|dcim|pxl|kakaotalk|wechatimg)\s*\d+\b/gi, " ").replace(/\b[0-9a-f]{8}(?: [0-9a-f]{4}){3} [0-9a-f]{12}\b/gi, " ").replace(/\b(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{8,}\b/gi, " ").replace(/\b\d{3,4}\s*x\s*\d{3,4}\b/gi, " ").replace(/\b(19|20)\d{2}[ ._-]?(0[1-9]|1[0-2])[ ._-]?([0-2]\d|3[01])\b/g, " ").replace(/\b\d{4,}\b/g, " ").replace(/\s+/g, " ").trim();
148
148
  if (!text || !/[0-9A-Za-z가-힣一-龥ぁ-んァ-ン]/.test(text)) {
149
149
  return "\uC5C5\uB85C\uB4DC \uC774\uBBF8\uC9C0";
150
150
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/variants.ts","../src/cf-images.ts","../src/normalize-filename.ts","../src/r2-client.ts","../src/s3-client.ts","../src/archive-evidence.ts","../src/archive.ts","../src/preserve-object.ts"],"sourcesContent":["// ADR-0034 §7.1.1 test #4 — CF Images variant 4종 URL 생성기.\n//\n// Phase 1 = thumbnail/sm/md/lg. CF Images delivery URL pattern:\n// https://imagedelivery.net/<account-hash>/<image-id>/<variant>\n\nexport type ImageVariant = \"thumbnail\" | \"sm\" | \"md\" | \"lg\";\n\nexport const VARIANT_NAMES: readonly ImageVariant[] = [\n \"thumbnail\",\n \"sm\",\n \"md\",\n \"lg\",\n] as const;\n\nexport interface VariantSizes {\n thumbnail: { width: 96; height: 96 };\n sm: { width: 320; height: 320 };\n md: { width: 768; height: 768 };\n lg: { width: 1600; height: 1600 };\n}\n\nexport const VARIANT_SIZES: VariantSizes = {\n thumbnail: { width: 96, height: 96 },\n sm: { width: 320, height: 320 },\n md: { width: 768, height: 768 },\n lg: { width: 1600, height: 1600 },\n};\n\nexport interface CfImagesConfig {\n accountHash: string;\n}\n\n/** CF Images delivery URL 생성. */\nexport function variantUrl(\n config: CfImagesConfig,\n imageId: string,\n variant: ImageVariant,\n): string {\n return `https://imagedelivery.net/${config.accountHash}/${imageId}/${variant}`;\n}\n\n/** 모든 variant 의 URL map. */\nexport function allVariantUrls(\n config: CfImagesConfig,\n imageId: string,\n): Record<ImageVariant, string> {\n return Object.fromEntries(\n VARIANT_NAMES.map((v) => [v, variantUrl(config, imageId, v)]),\n ) as Record<ImageVariant, string>;\n}\n\n// ── delivery URL 의 variant 세그먼트 교체 헬퍼 (account-hash/image-id 를 몰라도\n// 이미 받은 full delivery URL 만으로 variant 를 바꾼다. CfImage 렌더용.) ──\n\nconst CF_HOST = \"imagedelivery.net\";\nconst KNOWN_VARIANT_SUFFIX =\n /\\/(public|lg|md|sm|thumbnail|jhwebp|jhfull|jhthumb|jhmobile)$/;\n\n/** imagedelivery delivery URL 여부. */\nexport function isCfImageUrl(url: string): boolean {\n return typeof url === \"string\" && url.includes(CF_HOST);\n}\n\n/**\n * delivery URL 의 끝 variant 세그먼트를 교체. 세그먼트가 없으면 덧붙인다.\n * CF URL 이 아니면 원본 그대로 반환(외부/로컬 URL 안전 통과).\n */\nexport function cfImageVariant(\n url: string,\n variant: ImageVariant | \"public\",\n): string {\n if (!isCfImageUrl(url)) return url;\n return KNOWN_VARIANT_SUFFIX.test(url)\n ? url.replace(KNOWN_VARIANT_SUFFIX, `/${variant}`)\n : `${url}/${variant}`;\n}\n\n/** sm/md/lg srcset 문자열(CF URL 일 때만, 아니면 undefined). 폭은 VARIANT_SIZES 기준. */\nexport function cfImageSrcSet(\n url: string,\n variants: readonly ImageVariant[] = [\"sm\", \"md\", \"lg\"],\n): string | undefined {\n if (!isCfImageUrl(url)) return undefined;\n return variants\n .map((v) => `${cfImageVariant(url, v)} ${VARIANT_SIZES[v].width}w`)\n .join(\", \");\n}\n","// CF Images 업로드/삭제 seam (ADR-0034 §7.1.1 test #4) — variants.ts 가 delivery\n// URL 을 *조립*만 하던 것을, 실제 CF Images 업로드(→ image id)로 뒷받침한다.\n//\n// **이 파일이 단일 seam이다.** 예전엔 이 구현이 tenant-admin 안에만 있어서, 같은 일을\n// 하는 REST 업로드 경로(api-core `/v1/cms/media/uploads/complete`)가 CF Images 단계를\n// 통째로 빠뜨렸다. 그 결과 REST 로 올린 미디어는 `variants_json` 이 `{}` 로 남고,\n// 공개 posts API 는 featured 이미지를 variants_json 에서만 해석하므로\n// `featured_image_url` 이 영원히 null 이었다 (2026-07-26 발견 · 업로드·글 생성이\n// 모두 201 로 성공해 화면에서만 이미지가 빠지는 조용한 실패였다).\n//\n// 새 소비자는 반드시 이 팩토리를 쓸 것. 자격증명은 주입받는다 — Workers(env 바인딩)와\n// Next.js(process.env) 양쪽에서 같은 코드가 돌아야 하므로 여기서 env 를 읽지 않는다.\n\nimport { allVariantUrls, type CfImagesConfig, type ImageVariant } from \"./variants\";\n\n/** CF 가 sourceUrl 을 서버측에서 fetch → 보통 빠르나 큰 이미지 대비 넉넉히. */\nconst UPLOAD_TIMEOUT_MS = 15_000;\nconst DELETE_TIMEOUT_MS = 10_000;\n\nexport interface CfImagesCredentials {\n /** Cloudflare account id. */\n accountId: string;\n /** Images 쓰기 권한 토큰. */\n apiToken: string;\n /** imagedelivery.net account hash. */\n accountHash: string;\n /** 테스트/워커 주입용. 미지정 시 전역 fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface CfImagesClient {\n /**\n * R2 public URL 의 이미지를 CF Images 로 \"upload via URL\" 한다.\n * @returns CF Images image id. 실패 시 throw (호출부가 명확히 인지).\n */\n uploadFromUrl(sourceUrl: string): Promise<string>;\n /** image id → 4 variant(thumbnail/sm/md/lg) delivery URL map. */\n variantUrls(imageId: string): Record<ImageVariant, string>;\n /** CF Images 이미지 삭제 (orphan 방지). 실패 시 throw — 호출부가 best-effort 처리. */\n deleteImage(imageId: string): Promise<void>;\n}\n\n/**\n * CF Images 클라이언트. 자격증명 3종을 모두 받아야 만들 수 있다 —\n * 미설정 판단(graceful skip)은 호출부 책임이다.\n */\nexport function createCfImagesClient(\n credentials: CfImagesCredentials,\n): CfImagesClient {\n const { accountId, apiToken, accountHash } = credentials;\n const config: CfImagesConfig = { accountHash };\n // 호출 시점에 해석해야 테스트의 global fetch 모킹이 적용된다.\n const doFetch = (input: RequestInfo | URL, init?: RequestInit) =>\n (credentials.fetchImpl ?? globalThis.fetch)(input, init);\n\n async function withTimeout<T>(\n timeoutMs: number,\n run: (signal: AbortSignal) => Promise<T>,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await run(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n }\n\n return {\n async uploadFromUrl(sourceUrl: string): Promise<string> {\n const form = new FormData();\n form.set(\"url\", sourceUrl);\n\n return withTimeout(UPLOAD_TIMEOUT_MS, async (signal) => {\n const res = await doFetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,\n {\n method: \"POST\",\n headers: { authorization: `Bearer ${apiToken}` },\n body: form,\n signal,\n },\n );\n const json = (await res.json().catch(() => null)) as {\n success?: boolean;\n result?: { id?: string };\n errors?: Array<{ message?: string }>;\n } | null;\n if (!res.ok || !json?.success || !json.result?.id) {\n const msg =\n json?.errors?.map((e) => e.message).join(\"; \") ||\n `HTTP ${res.status}`;\n throw new Error(`CF Images 업로드 실패: ${msg}`);\n }\n return json.result.id;\n });\n },\n\n variantUrls(imageId: string) {\n return allVariantUrls(config, imageId);\n },\n\n async deleteImage(imageId: string): Promise<void> {\n await withTimeout(DELETE_TIMEOUT_MS, async (signal) => {\n const res = await doFetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1/${encodeURIComponent(imageId)}`,\n { method: \"DELETE\", headers: { authorization: `Bearer ${apiToken}` }, signal },\n );\n // 404 = 이미 없음 → idempotent 성공으로 간주.\n if (!res.ok && res.status !== 404) {\n throw new Error(`CF Images 삭제 실패: HTTP ${res.status}`);\n }\n });\n },\n };\n}\n\nexport interface ImageVariantsResult {\n /** CF Images delivery URL map. 미연결·실패 시 undefined. */\n variantsJson?: Record<ImageVariant, string>;\n /** `{ cfImageId }` — 삭제 시 orphan 정리에 쓴다. 미연결·실패 시 undefined. */\n metaJson?: { cfImageId: string };\n}\n\n/**\n * 업로드된 이미지의 variant/meta 를 만든다 — **모든 업로드 경로의 공통 헬퍼**.\n *\n * 이미지가 아니거나 CF Images 미연결(client=null)이면 graceful skip, CF 실패도\n * 삼켜서 빈 결과로 떨어뜨린다(미디어 등록 자체는 성공시킨다 — variant 없이도\n * 원본은 R2 에 있다). 업로드 경로가 늘어나도 결과 모양이 갈라지지 않도록\n * variant 채우기는 이 헬퍼를 통해서만 한다.\n */\nexport async function buildImageVariants(\n client: CfImagesClient | null,\n sourceUrl: string,\n onError?: (err: unknown) => void,\n): Promise<ImageVariantsResult> {\n if (!client) return {};\n try {\n const imageId = await client.uploadFromUrl(sourceUrl);\n return {\n variantsJson: client.variantUrls(imageId),\n metaJson: { cfImageId: imageId },\n };\n } catch (err) {\n onError?.(err);\n return {};\n }\n}\n","// ADR-0034 §7.1.1 test #4 — Korean filename normalization (한글 → hash)\n//\n// R2 object key 안전성: `safe` 는 **항상** 단일 path segment 로 안전하다 —\n// - 허용 문자: [a-zA-Z0-9._-] 만 (slash·공백·제어문자·CJK 불가)\n// - 디렉터리 성분(`/`·`\\`)·traversal(`..`)은 basename 추출 단계에서 제거\n// - 영숫자를 최소 1개 보장(dot-only `.`·`..` 방지), 길이 상한 적용\n// - 한글/CJK·비ASCII 는 base32(sha256) hash 로 변환, 확장자는 보존\n// 호출부가 `tenants/<id>/<category>/<safe>` 형태로 써도 prefix 를 벗어날 수 없다.\n\nimport { createHash } from \"node:crypto\";\n\n/** safe stem 에 허용되는 문자 — 단일 segment 안전 집합. */\nconst ASCII_SAFE_PATTERN = /^[a-zA-Z0-9._-]+$/;\n/** 유효한 확장자 — dot + 영숫자만 (예: \".jpg\", \".webp\"). slash/공백 섞이면 무효. */\nconst EXT_PATTERN = /^\\.[a-z0-9]+$/;\n/** 영숫자 1개 이상 포함 여부 — dot-only/빈 stem 방지용. */\nconst HAS_ALNUM = /[a-z0-9]/i;\n/** safe stem 길이 상한 (R2 key 길이 예산). 확장자는 별도. */\nconst MAX_STEM_LEN = 80;\nconst MAX_ALT_LEN = 120;\nconst MAX_SEO_STEM_LEN = 96;\n\nexport interface NormalizedFilename {\n /** R2 key segment 로 안전한 파일명 (확장자 포함). 항상 [a-zA-Z0-9._-]+. */\n safe: string;\n /** 원본 파일명 (UI 표시용, DB media.original_name) */\n original: string;\n /** 확장자 (소문자, dot 포함; e.g. \".jpg\"). 없으면 \"\". */\n ext: string;\n}\n\n/** 짧은 base32 hash (16자). UUID 와 충돌 회피용 별도 namespace. */\nfunction shortHash(input: string): string {\n return createHash(\"sha256\")\n .update(input)\n .digest(\"base64url\")\n .slice(0, 16)\n .replace(/[-_]/g, \"x\");\n}\n\n/** 디렉터리 성분 제거 — basename 만. `/`·`\\` 모두 분리(크로스 플랫폼 파일명 방어). */\nfunction basenameOf(filename: string): string {\n const cut = Math.max(filename.lastIndexOf(\"/\"), filename.lastIndexOf(\"\\\\\"));\n return cut >= 0 ? filename.slice(cut + 1) : filename;\n}\n\n/**\n * 확장자 추출 — basename 기준 마지막 dot 이후. dot+영숫자(EXT_PATTERN) 만 유효로\n * 인정하고, 그 외(slash·공백 포함 등)는 확장자 없음으로 처리한다.\n */\nfunction extractExt(basename: string): string {\n const dotIdx = basename.lastIndexOf(\".\");\n if (dotIdx <= 0 || dotIdx === basename.length - 1) return \"\";\n const ext = basename.slice(dotIdx).toLowerCase();\n return EXT_PATTERN.test(ext) ? ext : \"\";\n}\n\nfunction stemOfFilename(filename: string): string {\n const basename = basenameOf(filename);\n const ext = extractExt(basename);\n return ext ? basename.slice(0, basename.length - ext.length) : basename;\n}\n\n/**\n * 파일명 정규화. ASCII-safe(영숫자 포함) 이면 소문자화해 그대로, 아니면 hash.\n * 결과 `safe` 는 항상 단일 segment 로 안전(위 모듈 주석 불변식).\n */\nexport function normalizeFilename(original: string): NormalizedFilename {\n const basename = basenameOf(original);\n const ext = extractExt(basename);\n const stem = stemOfFilename(original);\n\n let safeStem: string;\n if (ASCII_SAFE_PATTERN.test(stem) && HAS_ALNUM.test(stem)) {\n safeStem = stem.toLowerCase().slice(0, MAX_STEM_LEN);\n } else {\n // 비ASCII·slash·공백·dot-only·빈 stem → hash (원본 기준 안정적 변환).\n safeStem = shortHash(stem || basename || original);\n }\n\n // 방어적 재확인 — 어떤 경로로도 영숫자 없는 stem 은 남기지 않는다.\n if (!HAS_ALNUM.test(safeStem)) {\n safeStem = shortHash(original);\n }\n\n return { safe: `${safeStem}${ext}`, original, ext };\n}\n\n/**\n * 파일명에서 SEO/accessibility 기본 alt 를 제안한다.\n *\n * 원본 파일명을 그대로 노출하지 않고, 확장자·경로·구분자·카메라/스크린샷\n * 잡음을 제거해 사람이 읽는 짧은 설명으로 바꾼다. 실제 문맥이 있는 surface\n * (예: 작성자 프로필)는 호출부가 더 구체적인 alt 로 override 할 수 있다.\n */\nexport function suggestAltFromFilename(original: string): string {\n let text = stemOfFilename(original)\n .normalize(\"NFKC\")\n .replace(/%[0-9a-f]{2}/gi, \" \")\n .replace(/[()[\\]{}]/g, \" \")\n .replace(/[._-]+/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n text = text\n .replace(/\\b(copy|final|edited|edit|export|upload)\\b/gi, \" \")\n .replace(/\\b(screenshot|screen shot|image|img|photo|picture|pic)\\b/gi, \" \")\n .replace(/\\b(dsc|dcim|pxl|kakaotalk|wechatimg)\\s*\\d+\\b/gi, \" \")\n .replace(/\\b\\d{3,4}\\s*x\\s*\\d{3,4}\\b/gi, \" \")\n .replace(/\\b(19|20)\\d{2}[ ._-]?(0[1-9]|1[0-2])[ ._-]?([0-2]\\d|3[01])\\b/g, \" \")\n .replace(/\\b\\d{4,}\\b/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (!text || !/[0-9A-Za-z가-힣一-龥ぁ-んァ-ン]/.test(text)) {\n return \"업로드 이미지\";\n }\n\n const capped = text.slice(0, MAX_ALT_LEN).trim();\n return capped.charAt(0).toLocaleUpperCase(\"ko-KR\") + capped.slice(1);\n}\n\n/**\n * 업로드 객체명에 붙일 SEO-friendly filename.\n *\n * URL 경로에 의미 있는 단어가 남도록 한글/CJK/라틴 문자와 숫자는 보존하고,\n * 나머지는 hyphen 으로 접는다. 호출부는 앞에 UUID 를 붙여 충돌을 피한다.\n */\nexport function seoFilenameFromFilename(\n original: string,\n fallbackExt = \"\",\n): string {\n const ext = extractExt(basenameOf(original)) || fallbackExt.toLowerCase();\n const alt = suggestAltFromFilename(original);\n const source = alt === \"업로드 이미지\" ? \"uploaded image\" : alt;\n const stem = source\n .normalize(\"NFKC\")\n .toLocaleLowerCase(\"ko-KR\")\n .replace(/[^\\p{L}\\p{N}]+/gu, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, MAX_SEO_STEM_LEN)\n .replace(/-$/g, \"\");\n const safeStem = stem || \"uploaded-image\";\n return `${safeStem}${EXT_PATTERN.test(ext) ? ext : \"\"}`;\n}\n","// ADR-0034 §7.1.1 test #4 — R2 client wrapper (interface only, Phase 1 stub).\n//\n// Phase 1 environment-specific implementation:\n// - Cloudflare Workers/Pages = R2 binding (env.R2.put/get/delete)\n// - Vercel/Node = AWS SDK S3 client (S3-compatible API)\n//\n// 본 모듈은 interface + Korean filename + tenant prefix 강제 로직만.\n// 실 binding 은 tenant-admin 의 server action 또는 tenant-api 의 R2 handler 가 주입.\n\nimport { normalizeFilename } from \"./normalize-filename\";\n\n/** ADR-0016 multi-tenant 격리 의무 — r2_key prefix = `tenants/<tenant_id>/` */\nexport function r2KeyForTenant(\n tenantId: string,\n category: string,\n filename: string,\n): string {\n const { safe } = normalizeFilename(filename);\n return `tenants/${tenantId}/${category}/${safe}`;\n}\n\nexport interface R2PutOptions {\n contentType: string;\n cacheControl?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface R2Object {\n key: string;\n size: number;\n etag: string;\n uploaded: Date;\n contentType?: string;\n metadata?: Record<string, string>;\n}\n\n/**\n * R2 client interface — environment-specific binding 이 본 interface 구현.\n * tenant-admin 가 server action 안에서 inject (DI).\n */\nexport interface R2Client {\n put(key: string, body: ArrayBuffer | Uint8Array, options: R2PutOptions): Promise<R2Object>;\n get(key: string): Promise<ArrayBuffer | null>;\n delete(key: string): Promise<void>;\n /** presigned URL — 외부 캐싱/CDN 용. Phase 1 = direct R2 public bucket */\n getPublicUrl(key: string): string;\n}\n\n/**\n * Tenant-aware R2 helper — key prefix 강제 + Korean filename normalize 자동.\n * tenant 외부에서 본 wrapper 우회하면 ESLint 의 no-raw-db-client 와 비슷한 rule\n * 추가 검토 (Phase 1b).\n */\nexport class TenantR2 {\n constructor(\n private readonly client: R2Client,\n private readonly tenantId: string,\n ) {}\n\n async uploadMedia(\n filename: string,\n body: ArrayBuffer | Uint8Array,\n contentType: string,\n ): Promise<{ key: string; publicUrl: string; object: R2Object }> {\n const key = r2KeyForTenant(this.tenantId, \"media\", filename);\n const object = await this.client.put(key, body, { contentType });\n return { key, publicUrl: this.client.getPublicUrl(key), object };\n }\n\n async deleteByKey(key: string): Promise<void> {\n if (!key.startsWith(`tenants/${this.tenantId}/`)) {\n throw new Error(\n `tenant 격리 위반: ${key} 는 tenants/${this.tenantId}/ prefix 와 일치하지 않음`,\n );\n }\n await this.client.delete(key);\n }\n}\n","// ADR-0034 §7.1.1 test #4 — AWS S3 SDK based R2Client 실 구현.\n//\n// Cloudflare R2 는 S3-compatible API 노출. @aws-sdk/client-s3 wrapper.\n// caller 가 endpoint + access key (Doppler) 주입.\n\nimport type { R2Client, R2Object, R2PutOptions } from \"./r2-client\";\n\nexport interface S3ClientConfig {\n endpoint: string; // e.g. https://<account>.r2.cloudflarestorage.com\n region?: string; // R2 = \"auto\"\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n publicBaseUrl: string; // CDN 또는 R2 public URL prefix\n}\n\n/**\n * AWS SDK 의존을 minimal — type-only declaration.\n * 실 binding 은 tenant-admin 의 server action 또는 tenant-api 가 SDK 주입.\n * 본 module 은 R2Client interface 의 S3-style 구현 스켈레톤.\n */\ninterface MinimalS3Sdk {\n send(command: unknown): Promise<{ ETag?: string; ContentLength?: number; Body?: AsyncIterable<Uint8Array> }>;\n}\n\ninterface S3PutObjectCommandInput {\n Bucket: string;\n Key: string;\n Body: Uint8Array;\n ContentType: string;\n CacheControl?: string;\n Metadata?: Record<string, string>;\n}\n\ninterface S3GetObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\ninterface S3DeleteObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\nexport interface S3CommandFactories {\n PutObjectCommand: new (input: S3PutObjectCommandInput) => unknown;\n GetObjectCommand: new (input: S3GetObjectCommandInput) => unknown;\n DeleteObjectCommand: new (input: S3DeleteObjectCommandInput) => unknown;\n}\n\n/**\n * R2Client 구현 (S3 SDK based). caller 가 SDK + commands 주입.\n *\n * 사용 예시 (tenant-admin):\n * import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand }\n * from \"@aws-sdk/client-s3\";\n * const r2 = new S3R2Client(\n * new S3Client({ endpoint, region: \"auto\", credentials: {...} }),\n * { PutObjectCommand, GetObjectCommand, DeleteObjectCommand },\n * config,\n * );\n */\nexport class S3R2Client implements R2Client {\n constructor(\n private readonly sdk: MinimalS3Sdk,\n private readonly commands: S3CommandFactories,\n private readonly config: S3ClientConfig,\n ) {}\n\n async put(\n key: string,\n body: ArrayBuffer | Uint8Array,\n options: R2PutOptions,\n ): Promise<R2Object> {\n const buf = body instanceof Uint8Array ? body : new Uint8Array(body);\n const cmd = new this.commands.PutObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n Body: buf,\n ContentType: options.contentType,\n CacheControl: options.cacheControl,\n Metadata: options.metadata,\n });\n const result = await this.sdk.send(cmd);\n return {\n key,\n size: buf.byteLength,\n etag: result.ETag ?? \"\",\n uploaded: new Date(),\n contentType: options.contentType,\n metadata: options.metadata,\n };\n }\n\n async get(key: string): Promise<ArrayBuffer | null> {\n const cmd = new this.commands.GetObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n });\n try {\n const result = await this.sdk.send(cmd);\n if (!result.Body) return null;\n const chunks: Uint8Array[] = [];\n for await (const chunk of result.Body) {\n chunks.push(chunk);\n }\n const total = chunks.reduce((a, c) => a + c.byteLength, 0);\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return merged.buffer;\n } catch (_err) {\n return null;\n }\n }\n\n async delete(key: string): Promise<void> {\n const cmd = new this.commands.DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n });\n await this.sdk.send(cmd);\n }\n\n getPublicUrl(key: string): string {\n const base = this.config.publicBaseUrl.replace(/\\/$/, \"\");\n return `${base}/${key}`;\n }\n}\n","import type { ArchiveReceipt } from \"./archive-types\";\n\n/** Cached proof is a backup observation, never permission to create/delete media. */\nexport const ARCHIVE_IDENTITY_MAX_AGE_MS = 45 * 60_000;\nexport const ARCHIVE_BYTES_MAX_AGE_MS = 26 * 60 * 60_000;\nexport const ARCHIVE_FULL_SCRUB_INTERVAL_MS = 24 * 60 * 60_000;\nexport const ARCHIVE_EVIDENCE_BATCH_LIMIT = 10_000;\nexport interface ArchiveObjectIdentity { etag: string; bytes: number }\nexport interface ArchiveSourceLocator { tenantId: string; sourceBucket: string; sourceKey: string }\nexport interface ArchiveIdentityProof {\n method: \"receipt_identity\";\n byteVerifiedAt: string;\n identityCheckedAt: string;\n ciphertext: ArchiveObjectIdentity;\n metadata: ArchiveObjectIdentity;\n}\nexport interface ArchiveSourceEvidence {\n tenantId: string;\n sourceDigest: string;\n reason: \"verified\" | \"evidence_missing\" | \"evidence_stale\";\n receipt: ArchiveReceipt | null;\n proof: ArchiveIdentityProof | null;\n /** Last completed source reconciliation attempt; scheduling only, never verification proof. */\n lastAttemptAt?: string;\n}\nexport function archiveProofFresh(proof: ArchiveIdentityProof, now: number): boolean {\n const bytesAge = now - Date.parse(proof.byteVerifiedAt);\n const identityAge = now - Date.parse(proof.identityCheckedAt);\n return Number.isFinite(bytesAge) && bytesAge >= 0 && bytesAge <= ARCHIVE_BYTES_MAX_AGE_MS\n && Number.isFinite(identityAge) && identityAge >= 0 && identityAge <= ARCHIVE_IDENTITY_MAX_AGE_MS\n && Date.parse(proof.identityCheckedAt) >= Date.parse(proof.byteVerifiedAt);\n}\nexport function parseArchiveIdentityProof(value: unknown): ArchiveIdentityProof {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid archive proof\");\n const proof = value as Record<string, unknown>;\n function time(value: unknown): string {\n if (typeof value !== \"string\" || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) throw new Error(\"Invalid archive proof date\");\n return value;\n }\n function identity(value: unknown): ArchiveObjectIdentity {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid archive identity\");\n const object = value as Record<string, unknown>;\n if (typeof object.etag !== \"string\" || !object.etag || object.etag.length > 256 || /[\\x00-\\x20\\x7f]/.test(object.etag)\n || typeof object.bytes !== \"number\" || !Number.isSafeInteger(object.bytes) || object.bytes <= 0) throw new Error(\"Invalid archive identity\");\n return { etag: object.etag, bytes: object.bytes };\n }\n if (proof.method !== \"receipt_identity\") throw new Error(\"Invalid archive proof method\");\n return { method: proof.method, byteVerifiedAt: time(proof.byteVerifiedAt), identityCheckedAt: time(proof.identityCheckedAt),\n ciphertext: identity(proof.ciphertext), metadata: identity(proof.metadata) };\n}\n","import { ARCHIVE_EVIDENCE_BATCH_LIMIT, parseArchiveIdentityProof, type ArchiveSourceLocator, type ArchiveSourceEvidence } from \"./archive-evidence\";\nimport type { ArchiveReceipt } from \"./archive-types\";\nexport type { ArchiveReceipt } from \"./archive-types\";\nexport * from \"./archive-evidence\";\n/** Online archive stores only GPG ciphertext; applications never receive backup credentials. */\nexport interface MediaArchiveConfig {\n baseUrl: string;\n token: string;\n fetchImpl?: typeof fetch;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface PreserveMediaInput {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n contentType: string;\n bytes: ArrayBuffer;\n uploadId?: string;\n}\n\nexport interface MediaTombstoneInput {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n receipt: ArchiveReceipt;\n deletedAt: string;\n metadata: Record<string, unknown>;\n}\n\nexport interface MediaArchiveClient {\n preserve(input: PreserveMediaInput): Promise<ArchiveReceipt>;\n verify(receipt: ArchiveReceipt): Promise<ArchiveReceipt>;\n findSource(input: { tenantId: string; sourceBucket: string; sourceKey: string }): Promise<ArchiveReceipt | null>;\n tombstone(input: MediaTombstoneInput): Promise<void>;\n /** Read-only cached observation. Must never authorize an upload or deletion. */\n evidence?(sources: ArchiveSourceLocator[]): Promise<ArchiveSourceEvidence[]>;\n /** Separate scrub workload: verifies source/receipt identity and periodically bytes. */\n reconcileSource?(source: ArchiveSourceLocator): Promise<ArchiveSourceEvidence>;\n}\n\nexport const ARCHIVE_FAILURE_REASONS = [\"missing_receipt\", \"checksum_mismatch\", \"invalid_receipt\", \"auth\", \"throttled\", \"timeout\", \"transport\", \"busy\", \"unavailable\"] as const;\nexport type ArchiveFailureReason = typeof ARCHIVE_FAILURE_REASONS[number];\nexport class MediaPreservationError extends Error {\n constructor(message = \"파일 보존을 확인하지 못했습니다. 잠시 후 다시 시도해 주세요.\",\n readonly reason: ArchiveFailureReason = \"invalid_receipt\", readonly status?: number) {\n super(message);\n this.name = \"MediaPreservationError\";\n }\n}\n\n/** Organization support attachments have their own owner namespace, never a fake tenant. */\nexport function archiveOwnerPrefix(scopeId: string): string {\n if (/^org-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(scopeId)) {\n return `organizations/${scopeId.slice(4)}/`;\n }\n if (!/^[A-Za-z0-9_-]+$/.test(scopeId) || scopeId.startsWith(\"org-\")) throw new MediaPreservationError();\n return `tenants/${scopeId}/`;\n}\n\nexport function assertTenantObjectKey(tenantId: string, key: string): void {\n if (!/^[A-Za-z0-9_-]+$/.test(tenantId) ||\n !(key.startsWith(archiveOwnerPrefix(tenantId)) ||\n (tenantId.startsWith(\"org-\") && key.startsWith(`support/${tenantId.slice(4)}/`)) ||\n (!tenantId.startsWith(\"org-\") && key.startsWith(`${tenantId}/medical/records/`))) ||\n key.split(\"/\").some((part) => !part || part === \".\" || part === \"..\") ||\n /[\\\\\\x00-\\x1f\\x7f]/.test(key)) {\n throw new MediaPreservationError(\"잘못된 파일 소유 경로입니다.\");\n }\n}\n\nexport async function mediaSha256(bytes: ArrayBuffer): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes));\n return Array.from(digest, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction metadataHeader(value: unknown): string {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction readReceipt(value: unknown, tenantId: string, sha256: string, sizeBytes: number): ArchiveReceipt {\n if (!/^[a-f0-9]{64}$/.test(sha256) || !Number.isSafeInteger(sizeBytes) || sizeBytes < 0 ||\n !value || typeof value !== \"object\") throw new MediaPreservationError();\n const item = value as Record<string, unknown>;\n if (item.version !== 1 || item.tenantId !== tenantId || item.sha256 !== sha256 ||\n item.sizeBytes !== sizeBytes ||\n typeof item.archiveKey !== \"string\" ||\n !new RegExp(`^${archiveOwnerPrefix(tenantId)}media-archive/${sha256}/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/original\\\\.gpg$`).test(item.archiveKey) ||\n typeof item.cipherSha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(item.cipherSha256) ||\n item.metadataKey !== item.archiveKey.replace(/original\\.gpg$/, \"metadata.gpg\") ||\n typeof item.metadataCipherSha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(item.metadataCipherSha256) ||\n typeof item.verifiedAt !== \"string\" || !Number.isFinite(Date.parse(item.verifiedAt))) {\n throw new MediaPreservationError();\n }\n return { version: 1, tenantId, sha256, sizeBytes, archiveKey: item.archiveKey,\n cipherSha256: item.cipherSha256, metadataKey: item.metadataKey,\n metadataCipherSha256: item.metadataCipherSha256, verifiedAt: item.verifiedAt };\n}\n\nexport function createMediaArchiveClient(config: MediaArchiveConfig): MediaArchiveClient {\n const base = new URL(config.baseUrl);\n if (base.protocol !== \"https:\" || base.username || base.password || !config.token.trim()) {\n throw new MediaPreservationError(\"파일 보존 연결 설정을 확인해 주세요.\");\n }\n const fetchImpl = config.fetchImpl ?? fetch;\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n async function request(path: string, init: RequestInit, allowMissing = false, readOnly = false,\n timeoutMs = init.method === \"GET\" || readOnly ? 10_000 : 60_000, totalBudgetMs?: number): Promise<unknown> {\n const started = Date.now();\n for (let attempt = 1; ; attempt++) {\n const remaining = totalBudgetMs === undefined ? timeoutMs : totalBudgetMs - (Date.now() - started);\n if (remaining <= 0) throw new MediaPreservationError(undefined, \"timeout\");\n let retryAfterMs = 0;\n try {\n const headers = new Headers(init.headers);\n headers.set(\"Authorization\", `Bearer ${config.token}`);\n const response = await fetchImpl(new URL(path, base), {\n // Workers reject redirect:\"error\"; manual keeps credentials on this origin.\n ...init, headers, redirect: \"manual\", signal: AbortSignal.timeout(Math.min(timeoutMs, remaining)),\n });\n if (response.status === 404 && allowMissing) { await response.body?.cancel(); return null; }\n if (!response.ok) {\n const rawReason = response.headers.get(\"x-archive-failure\");\n const reason = ARCHIVE_FAILURE_REASONS.find((value) => value === rawReason)\n ?? ([401, 403].includes(response.status) ? \"auth\" : response.status === 429 ? \"throttled\"\n : [408, 500, 502, 503, 504].includes(response.status) ? \"unavailable\" : \"invalid_receipt\");\n const retryAfter = response.headers.get(\"retry-after\");\n retryAfterMs = retryAfter && /^\\d+$/.test(retryAfter) ? Number(retryAfter) * 1000\n : retryAfter && Number.isFinite(Date.parse(retryAfter)) ? Math.max(0, Date.parse(retryAfter) - Date.now()) : 0;\n await response.body?.cancel();\n throw new MediaPreservationError(undefined, reason, response.status);\n }\n return await response.json();\n } catch (error) {\n // Gateway failures must not leak a URL, token, or source metadata to client logs.\n const safe = error instanceof MediaPreservationError ? error : new MediaPreservationError(undefined,\n error instanceof Error && [\"TimeoutError\", \"AbortError\"].includes(error.name) ? \"timeout\"\n : error instanceof SyntaxError ? \"invalid_receipt\" : \"transport\");\n const delay = Math.max(retryAfterMs, 250 * 2 ** (attempt - 1) + Math.random() * 250);\n const budgetExceeded = totalBudgetMs === undefined\n ? Date.now() - started + delay + timeoutMs > 3 * timeoutMs + 5_000\n : Date.now() - started + delay >= totalBudgetMs;\n if ((!readOnly && init.method !== \"GET\") || attempt >= 3 || ![\"transport\", \"timeout\", \"throttled\", \"busy\", \"unavailable\"].includes(safe.reason)\n || budgetExceeded) throw safe;\n await sleep(delay);\n }\n }\n }\n function validateSource(input: ArchiveSourceLocator): void {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n if (!/^[a-z0-9][a-z0-9.-]{1,62}$/.test(input.sourceBucket)) throw new MediaPreservationError();\n }\n async function sourceDigest(input: ArchiveSourceLocator): Promise<string> {\n validateSource(input);\n return mediaSha256(new TextEncoder().encode(`${input.sourceBucket}\\n${input.sourceKey}`).buffer);\n }\n async function evidenceItem(value: unknown, input: ArchiveSourceLocator): Promise<ArchiveSourceEvidence> {\n try {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error();\n const item = value as Record<string, unknown>;\n const digest = await sourceDigest(input);\n if (item.tenantId !== input.tenantId || item.sourceDigest !== digest) throw new Error();\n let lastAttemptAt: string | undefined;\n if (item.lastAttemptAt !== undefined) {\n if (typeof item.lastAttemptAt !== \"string\" || !Number.isFinite(Date.parse(item.lastAttemptAt))\n || new Date(item.lastAttemptAt).toISOString() !== item.lastAttemptAt) throw new Error();\n lastAttemptAt = item.lastAttemptAt;\n }\n const attemptHint = lastAttemptAt === undefined ? {} : { lastAttemptAt };\n if (item.reason === \"evidence_missing\" || item.reason === \"evidence_stale\") {\n if (item.receipt !== null || item.proof !== null) throw new Error();\n return { tenantId: input.tenantId, sourceDigest: digest, reason: item.reason, receipt: null, proof: null, ...attemptHint };\n }\n if (item.reason !== \"verified\" || !item.receipt || typeof item.receipt !== \"object\") throw new Error();\n const raw = item.receipt as Record<string, unknown>;\n if (typeof raw.sha256 !== \"string\" || typeof raw.sizeBytes !== \"number\") throw new Error();\n return { tenantId: input.tenantId, sourceDigest: digest, reason: \"verified\",\n receipt: readReceipt(raw, input.tenantId, raw.sha256, raw.sizeBytes), proof: parseArchiveIdentityProof(item.proof), ...attemptHint };\n } catch { throw new MediaPreservationError(); }\n }\n return {\n async evidence(sources) {\n if (sources.length > ARCHIVE_EVIDENCE_BATCH_LIMIT) throw new MediaPreservationError();\n for (const source of sources) validateSource(source);\n const value = await request(\"/v2/source-evidence\", { method: \"POST\", headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ version: 2, sources }) }, false, true);\n if (!value || typeof value !== \"object\" || !(\"version\" in value) || value.version !== 2\n || !(\"items\" in value) || !Array.isArray(value.items) || value.items.length !== sources.length) throw new MediaPreservationError();\n const parsed: ArchiveSourceEvidence[] = [];\n // Bound WebCrypto jobs and temporary receipt objects as inventory grows.\n for (let start = 0; start < sources.length; start += 64) {\n parsed.push(...await Promise.all(value.items.slice(start, start + 64)\n .map((item, offset) => evidenceItem(item, sources[start + offset]!))));\n }\n return parsed;\n },\n async reconcileSource(source) {\n const digest = await sourceDigest(source);\n // Cold/periodic scrub may stream a complete object. Let the gateway's 55s\n // operation deadline settle. Retries share one 60s budget so the scrub can\n // still publish its result before shutdown; snapshot observation keeps 10s.\n const value = await request(`/v2/sources/${source.tenantId}/${digest}`, { method: \"GET\" }, true, false, 60_000, 60_000);\n if (value === null) throw new MediaPreservationError(undefined, \"missing_receipt\");\n return evidenceItem(value, source);\n },\n async preserve(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const sha256 = await mediaSha256(input.bytes);\n const value = await request(`/v1/objects/${input.tenantId}/${sha256}`, {\n method: \"PUT\",\n headers: { \"Content-Type\": input.contentType, \"X-Archive-Metadata\": metadataHeader({\n sourceKey: input.sourceKey, sourceBucket: input.sourceBucket, contentType: input.contentType, uploadId: input.uploadId,\n }) },\n body: input.bytes,\n });\n return readReceipt(value, input.tenantId, sha256, input.bytes.byteLength);\n },\n async findSource(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const hash = await mediaSha256(new TextEncoder().encode(`${input.sourceBucket}\\n${input.sourceKey}`).buffer);\n const value = await request(`/v1/sources/${input.tenantId}/${hash}`, { method: \"GET\" }, true);\n if (value === null) return null;\n if (!value || typeof value !== \"object\" || !(\"sha256\" in value) || typeof value.sha256 !== \"string\" ||\n !/^[a-f0-9]{64}$/.test(value.sha256) || !(\"sizeBytes\" in value) || typeof value.sizeBytes !== \"number\") {\n throw new MediaPreservationError();\n }\n return readReceipt(value, input.tenantId, value.sha256, value.sizeBytes);\n },\n async verify(receipt) {\n readReceipt(receipt, receipt.tenantId, receipt.sha256, receipt.sizeBytes);\n const value = await request(`/v1/objects/${receipt.tenantId}/${receipt.sha256}`, { method: \"GET\" });\n const verified = readReceipt(value, receipt.tenantId, receipt.sha256, receipt.sizeBytes);\n if (verified.cipherSha256 !== receipt.cipherSha256 || verified.metadataKey !== receipt.metadataKey ||\n verified.metadataCipherSha256 !== receipt.metadataCipherSha256) throw new MediaPreservationError();\n return verified;\n },\n async tombstone(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n if (input.receipt.tenantId !== input.tenantId) throw new MediaPreservationError();\n const value = await request(\"/v1/tombstones\", {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(input),\n });\n if (!value || typeof value !== \"object\" || !(\"recorded\" in value) || value.recorded !== true ||\n !(\"tombstoneKey\" in value) || typeof value.tombstoneKey !== \"string\" ||\n !value.tombstoneKey.startsWith(archiveOwnerPrefix(input.tenantId))) throw new MediaPreservationError();\n },\n };\n}\n\nexport interface PreservationStorage {\n get(key: string): Promise<{ bytes: ArrayBuffer; contentType: string } | null>;\n /** Conditional create only. Existing keys must never be overwritten. */\n putIfAbsent(key: string, bytes: ArrayBuffer, contentType: string): Promise<void>;\n}\n\nexport interface FinalizePreservedUploadInput {\n tenantId: string;\n siteId: string;\n stagingKey: string;\n expectedContentType: string;\n expectedSize: number;\n sourceBucket: string;\n storage: PreservationStorage;\n archive: MediaArchiveClient;\n}\n\n/** Staging upload ID is the media primary key, so retries cannot insert duplicate rows. */\nexport function mediaUploadId(stagingKey: string): string {\n const match = /\\/staging\\/([0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})[.-]/.exec(stagingKey);\n if (!match?.[1]) throw new MediaPreservationError(\"잘못된 업로드 경로입니다.\");\n return match[1];\n}\n\nexport async function finalizePreservedUpload(input: FinalizePreservedUploadInput): Promise<{\n id: string; key: string; receipt: ArchiveReceipt;\n}> {\n assertTenantObjectKey(input.tenantId, input.stagingKey);\n const id = mediaUploadId(input.stagingKey);\n const original = await input.storage.get(input.stagingKey);\n if (!original || original.bytes.byteLength !== input.expectedSize ||\n original.contentType.split(\";\", 1)[0]?.trim().toLowerCase() !== input.expectedContentType) {\n throw new MediaPreservationError(\"업로드된 파일의 형식과 크기를 확인해 주세요.\");\n }\n const sha256 = await mediaSha256(original.bytes);\n const filename = input.stagingKey.slice(input.stagingKey.lastIndexOf(\"/\") + 1);\n const key = `tenants/${input.tenantId}/sites/${input.siteId}/media/originals/${sha256}/${filename}`;\n await input.storage.putIfAbsent(key, original.bytes, input.expectedContentType);\n const final = await input.storage.get(key);\n if (!final || await mediaSha256(final.bytes) !== sha256) throw new MediaPreservationError();\n const receipt = await input.archive.preserve({ tenantId: input.tenantId, sourceBucket: input.sourceBucket,\n sourceKey: key, contentType: input.expectedContentType, bytes: final.bytes, uploadId: id });\n return { id, key, receipt };\n}\n\n/** Snapshot bytes once; later source mutations cannot change the encrypted copy. */\nexport async function preserveStoredObject(input: {\n tenantId: string; sourceBucket: string; sourceKey: string;\n storage: Pick<PreservationStorage, \"get\">; archive: MediaArchiveClient;\n}): Promise<ArchiveReceipt> {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const original = await input.storage.get(input.sourceKey);\n if (!original) {\n const existing = await input.archive.findSource(input);\n if (!existing) throw new MediaPreservationError(\"보존할 원본 파일을 찾을 수 없습니다.\");\n return input.archive.verify(existing);\n }\n return input.archive.preserve({ tenantId: input.tenantId, sourceBucket: input.sourceBucket,\n sourceKey: input.sourceKey, ...original });\n}\n","import {\n assertTenantObjectKey,\n mediaSha256,\n MediaPreservationError,\n type ArchiveReceipt,\n type MediaArchiveClient,\n type PreservationStorage,\n} from \"./archive\";\n\n/** Caller authorizes the staging locator; only the new tenant-owned key can be archived. */\nexport async function preserveStoredUpload(input: {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n expectedSize: number;\n allowedContentTypes: readonly string[];\n finalKey: (sha256: string) => string;\n storage: PreservationStorage;\n archive: MediaArchiveClient;\n}): Promise<{ key: string; receipt: ArchiveReceipt }> {\n const original = await input.storage.get(input.sourceKey);\n const contentType = original?.contentType.split(\";\", 1)[0]?.trim().toLowerCase();\n if (!original || original.bytes.byteLength !== input.expectedSize || !contentType ||\n !input.allowedContentTypes.includes(contentType)) {\n throw new MediaPreservationError(\"업로드된 파일의 형식과 크기를 확인해 주세요.\");\n }\n const sha256 = await mediaSha256(original.bytes);\n const key = input.finalKey(sha256);\n assertTenantObjectKey(input.tenantId, key);\n if (key === input.sourceKey) throw new MediaPreservationError(\"보존 파일은 별도 경로가 필요합니다.\");\n await input.storage.putIfAbsent(key, original.bytes, contentType);\n const stored = await input.storage.get(key);\n if (!stored || await mediaSha256(stored.bytes) !== sha256 ||\n stored.contentType.split(\";\", 1)[0]?.trim().toLowerCase() !== contentType) {\n throw new MediaPreservationError();\n }\n const receipt = await input.archive.preserve({\n tenantId: input.tenantId,\n sourceKey: key,\n sourceBucket: input.sourceBucket,\n contentType,\n bytes: stored.bytes,\n });\n return { key, receipt };\n}\n"],"mappings":"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","names":["value"]}
1
+ {"version":3,"sources":["../src/variants.ts","../src/cf-images.ts","../src/normalize-filename.ts","../src/r2-client.ts","../src/s3-client.ts","../src/archive-evidence.ts","../src/archive.ts","../src/preserve-object.ts"],"sourcesContent":["// ADR-0034 §7.1.1 test #4 — CF Images variant 4종 URL 생성기.\n//\n// Phase 1 = thumbnail/sm/md/lg. CF Images delivery URL pattern:\n// https://imagedelivery.net/<account-hash>/<image-id>/<variant>\n\nexport type ImageVariant = \"thumbnail\" | \"sm\" | \"md\" | \"lg\";\n\nexport const VARIANT_NAMES: readonly ImageVariant[] = [\n \"thumbnail\",\n \"sm\",\n \"md\",\n \"lg\",\n] as const;\n\nexport interface VariantSizes {\n thumbnail: { width: 96; height: 96 };\n sm: { width: 320; height: 320 };\n md: { width: 768; height: 768 };\n lg: { width: 1600; height: 1600 };\n}\n\nexport const VARIANT_SIZES: VariantSizes = {\n thumbnail: { width: 96, height: 96 },\n sm: { width: 320, height: 320 },\n md: { width: 768, height: 768 },\n lg: { width: 1600, height: 1600 },\n};\n\nexport interface CfImagesConfig {\n accountHash: string;\n}\n\n/** CF Images delivery URL 생성. */\nexport function variantUrl(\n config: CfImagesConfig,\n imageId: string,\n variant: ImageVariant,\n): string {\n return `https://imagedelivery.net/${config.accountHash}/${imageId}/${variant}`;\n}\n\n/** 모든 variant 의 URL map. */\nexport function allVariantUrls(\n config: CfImagesConfig,\n imageId: string,\n): Record<ImageVariant, string> {\n return Object.fromEntries(\n VARIANT_NAMES.map((v) => [v, variantUrl(config, imageId, v)]),\n ) as Record<ImageVariant, string>;\n}\n\n// ── delivery URL 의 variant 세그먼트 교체 헬퍼 (account-hash/image-id 를 몰라도\n// 이미 받은 full delivery URL 만으로 variant 를 바꾼다. CfImage 렌더용.) ──\n\nconst CF_HOST = \"imagedelivery.net\";\nconst KNOWN_VARIANT_SUFFIX =\n /\\/(public|lg|md|sm|thumbnail|jhwebp|jhfull|jhthumb|jhmobile)$/;\n\n/** imagedelivery delivery URL 여부. */\nexport function isCfImageUrl(url: string): boolean {\n return typeof url === \"string\" && url.includes(CF_HOST);\n}\n\n/**\n * delivery URL 의 끝 variant 세그먼트를 교체. 세그먼트가 없으면 덧붙인다.\n * CF URL 이 아니면 원본 그대로 반환(외부/로컬 URL 안전 통과).\n */\nexport function cfImageVariant(\n url: string,\n variant: ImageVariant | \"public\",\n): string {\n if (!isCfImageUrl(url)) return url;\n return KNOWN_VARIANT_SUFFIX.test(url)\n ? url.replace(KNOWN_VARIANT_SUFFIX, `/${variant}`)\n : `${url}/${variant}`;\n}\n\n/** sm/md/lg srcset 문자열(CF URL 일 때만, 아니면 undefined). 폭은 VARIANT_SIZES 기준. */\nexport function cfImageSrcSet(\n url: string,\n variants: readonly ImageVariant[] = [\"sm\", \"md\", \"lg\"],\n): string | undefined {\n if (!isCfImageUrl(url)) return undefined;\n return variants\n .map((v) => `${cfImageVariant(url, v)} ${VARIANT_SIZES[v].width}w`)\n .join(\", \");\n}\n","// CF Images 업로드/삭제 seam (ADR-0034 §7.1.1 test #4) — variants.ts 가 delivery\n// URL 을 *조립*만 하던 것을, 실제 CF Images 업로드(→ image id)로 뒷받침한다.\n//\n// **이 파일이 단일 seam이다.** 예전엔 이 구현이 tenant-admin 안에만 있어서, 같은 일을\n// 하는 REST 업로드 경로(api-core `/v1/cms/media/uploads/complete`)가 CF Images 단계를\n// 통째로 빠뜨렸다. 그 결과 REST 로 올린 미디어는 `variants_json` 이 `{}` 로 남고,\n// 공개 posts API 는 featured 이미지를 variants_json 에서만 해석하므로\n// `featured_image_url` 이 영원히 null 이었다 (2026-07-26 발견 · 업로드·글 생성이\n// 모두 201 로 성공해 화면에서만 이미지가 빠지는 조용한 실패였다).\n//\n// 새 소비자는 반드시 이 팩토리를 쓸 것. 자격증명은 주입받는다 — Workers(env 바인딩)와\n// Next.js(process.env) 양쪽에서 같은 코드가 돌아야 하므로 여기서 env 를 읽지 않는다.\n\nimport { allVariantUrls, type CfImagesConfig, type ImageVariant } from \"./variants\";\n\n/** CF 가 sourceUrl 을 서버측에서 fetch → 보통 빠르나 큰 이미지 대비 넉넉히. */\nconst UPLOAD_TIMEOUT_MS = 15_000;\nconst DELETE_TIMEOUT_MS = 10_000;\n\nexport interface CfImagesCredentials {\n /** Cloudflare account id. */\n accountId: string;\n /** Images 쓰기 권한 토큰. */\n apiToken: string;\n /** imagedelivery.net account hash. */\n accountHash: string;\n /** 테스트/워커 주입용. 미지정 시 전역 fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface CfImagesClient {\n /**\n * R2 public URL 의 이미지를 CF Images 로 \"upload via URL\" 한다.\n * @returns CF Images image id. 실패 시 throw (호출부가 명확히 인지).\n */\n uploadFromUrl(sourceUrl: string): Promise<string>;\n /** image id → 4 variant(thumbnail/sm/md/lg) delivery URL map. */\n variantUrls(imageId: string): Record<ImageVariant, string>;\n /** CF Images 이미지 삭제 (orphan 방지). 실패 시 throw — 호출부가 best-effort 처리. */\n deleteImage(imageId: string): Promise<void>;\n}\n\n/**\n * CF Images 클라이언트. 자격증명 3종을 모두 받아야 만들 수 있다 —\n * 미설정 판단(graceful skip)은 호출부 책임이다.\n */\nexport function createCfImagesClient(\n credentials: CfImagesCredentials,\n): CfImagesClient {\n const { accountId, apiToken, accountHash } = credentials;\n const config: CfImagesConfig = { accountHash };\n // 호출 시점에 해석해야 테스트의 global fetch 모킹이 적용된다.\n const doFetch = (input: RequestInfo | URL, init?: RequestInit) =>\n (credentials.fetchImpl ?? globalThis.fetch)(input, init);\n\n async function withTimeout<T>(\n timeoutMs: number,\n run: (signal: AbortSignal) => Promise<T>,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await run(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n }\n\n return {\n async uploadFromUrl(sourceUrl: string): Promise<string> {\n const form = new FormData();\n form.set(\"url\", sourceUrl);\n\n return withTimeout(UPLOAD_TIMEOUT_MS, async (signal) => {\n const res = await doFetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,\n {\n method: \"POST\",\n headers: { authorization: `Bearer ${apiToken}` },\n body: form,\n signal,\n },\n );\n const json = (await res.json().catch(() => null)) as {\n success?: boolean;\n result?: { id?: string };\n errors?: Array<{ message?: string }>;\n } | null;\n if (!res.ok || !json?.success || !json.result?.id) {\n const msg =\n json?.errors?.map((e) => e.message).join(\"; \") ||\n `HTTP ${res.status}`;\n throw new Error(`CF Images 업로드 실패: ${msg}`);\n }\n return json.result.id;\n });\n },\n\n variantUrls(imageId: string) {\n return allVariantUrls(config, imageId);\n },\n\n async deleteImage(imageId: string): Promise<void> {\n await withTimeout(DELETE_TIMEOUT_MS, async (signal) => {\n const res = await doFetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1/${encodeURIComponent(imageId)}`,\n { method: \"DELETE\", headers: { authorization: `Bearer ${apiToken}` }, signal },\n );\n // 404 = 이미 없음 → idempotent 성공으로 간주.\n if (!res.ok && res.status !== 404) {\n throw new Error(`CF Images 삭제 실패: HTTP ${res.status}`);\n }\n });\n },\n };\n}\n\nexport interface ImageVariantsResult {\n /** CF Images delivery URL map. 미연결·실패 시 undefined. */\n variantsJson?: Record<ImageVariant, string>;\n /** `{ cfImageId }` — 삭제 시 orphan 정리에 쓴다. 미연결·실패 시 undefined. */\n metaJson?: { cfImageId: string };\n}\n\n/**\n * 업로드된 이미지의 variant/meta 를 만든다 — **모든 업로드 경로의 공통 헬퍼**.\n *\n * 이미지가 아니거나 CF Images 미연결(client=null)이면 graceful skip, CF 실패도\n * 삼켜서 빈 결과로 떨어뜨린다(미디어 등록 자체는 성공시킨다 — variant 없이도\n * 원본은 R2 에 있다). 업로드 경로가 늘어나도 결과 모양이 갈라지지 않도록\n * variant 채우기는 이 헬퍼를 통해서만 한다.\n */\nexport async function buildImageVariants(\n client: CfImagesClient | null,\n sourceUrl: string,\n onError?: (err: unknown) => void,\n): Promise<ImageVariantsResult> {\n if (!client) return {};\n try {\n const imageId = await client.uploadFromUrl(sourceUrl);\n return {\n variantsJson: client.variantUrls(imageId),\n metaJson: { cfImageId: imageId },\n };\n } catch (err) {\n onError?.(err);\n return {};\n }\n}\n","// ADR-0034 §7.1.1 test #4 — Korean filename normalization (한글 → hash)\n//\n// R2 object key 안전성: `safe` 는 **항상** 단일 path segment 로 안전하다 —\n// - 허용 문자: [a-zA-Z0-9._-] 만 (slash·공백·제어문자·CJK 불가)\n// - 디렉터리 성분(`/`·`\\`)·traversal(`..`)은 basename 추출 단계에서 제거\n// - 영숫자를 최소 1개 보장(dot-only `.`·`..` 방지), 길이 상한 적용\n// - 한글/CJK·비ASCII 는 base32(sha256) hash 로 변환, 확장자는 보존\n// 호출부가 `tenants/<id>/<category>/<safe>` 형태로 써도 prefix 를 벗어날 수 없다.\n\nimport { createHash } from \"node:crypto\";\n\n/** safe stem 에 허용되는 문자 — 단일 segment 안전 집합. */\nconst ASCII_SAFE_PATTERN = /^[a-zA-Z0-9._-]+$/;\n/** 유효한 확장자 — dot + 영숫자만 (예: \".jpg\", \".webp\"). slash/공백 섞이면 무효. */\nconst EXT_PATTERN = /^\\.[a-z0-9]+$/;\n/** 영숫자 1개 이상 포함 여부 — dot-only/빈 stem 방지용. */\nconst HAS_ALNUM = /[a-z0-9]/i;\n/** safe stem 길이 상한 (R2 key 길이 예산). 확장자는 별도. */\nconst MAX_STEM_LEN = 80;\nconst MAX_ALT_LEN = 120;\nconst MAX_SEO_STEM_LEN = 96;\n\nexport interface NormalizedFilename {\n /** R2 key segment 로 안전한 파일명 (확장자 포함). 항상 [a-zA-Z0-9._-]+. */\n safe: string;\n /** 원본 파일명 (UI 표시용, DB media.original_name) */\n original: string;\n /** 확장자 (소문자, dot 포함; e.g. \".jpg\"). 없으면 \"\". */\n ext: string;\n}\n\n/** 짧은 base32 hash (16자). UUID 와 충돌 회피용 별도 namespace. */\nfunction shortHash(input: string): string {\n return createHash(\"sha256\")\n .update(input)\n .digest(\"base64url\")\n .slice(0, 16)\n .replace(/[-_]/g, \"x\");\n}\n\n/** 디렉터리 성분 제거 — basename 만. `/`·`\\` 모두 분리(크로스 플랫폼 파일명 방어). */\nfunction basenameOf(filename: string): string {\n const cut = Math.max(filename.lastIndexOf(\"/\"), filename.lastIndexOf(\"\\\\\"));\n return cut >= 0 ? filename.slice(cut + 1) : filename;\n}\n\n/**\n * 확장자 추출 — basename 기준 마지막 dot 이후. dot+영숫자(EXT_PATTERN) 만 유효로\n * 인정하고, 그 외(slash·공백 포함 등)는 확장자 없음으로 처리한다.\n */\nfunction extractExt(basename: string): string {\n const dotIdx = basename.lastIndexOf(\".\");\n if (dotIdx <= 0 || dotIdx === basename.length - 1) return \"\";\n const ext = basename.slice(dotIdx).toLowerCase();\n return EXT_PATTERN.test(ext) ? ext : \"\";\n}\n\nfunction stemOfFilename(filename: string): string {\n const basename = basenameOf(filename);\n const ext = extractExt(basename);\n return ext ? basename.slice(0, basename.length - ext.length) : basename;\n}\n\n/**\n * 파일명 정규화. ASCII-safe(영숫자 포함) 이면 소문자화해 그대로, 아니면 hash.\n * 결과 `safe` 는 항상 단일 segment 로 안전(위 모듈 주석 불변식).\n */\nexport function normalizeFilename(original: string): NormalizedFilename {\n const basename = basenameOf(original);\n const ext = extractExt(basename);\n const stem = stemOfFilename(original);\n\n let safeStem: string;\n if (ASCII_SAFE_PATTERN.test(stem) && HAS_ALNUM.test(stem)) {\n safeStem = stem.toLowerCase().slice(0, MAX_STEM_LEN);\n } else {\n // 비ASCII·slash·공백·dot-only·빈 stem → hash (원본 기준 안정적 변환).\n safeStem = shortHash(stem || basename || original);\n }\n\n // 방어적 재확인 — 어떤 경로로도 영숫자 없는 stem 은 남기지 않는다.\n if (!HAS_ALNUM.test(safeStem)) {\n safeStem = shortHash(original);\n }\n\n return { safe: `${safeStem}${ext}`, original, ext };\n}\n\n/**\n * 파일명에서 SEO/accessibility 기본 alt 를 제안한다.\n *\n * 원본 파일명을 그대로 노출하지 않고, 확장자·경로·구분자·카메라/스크린샷\n * 잡음을 제거해 사람이 읽는 짧은 설명으로 바꾼다. 실제 문맥이 있는 surface\n * (예: 작성자 프로필)는 호출부가 더 구체적인 alt 로 override 할 수 있다.\n */\nexport function suggestAltFromFilename(original: string): string {\n let text = stemOfFilename(original)\n .normalize(\"NFKC\")\n .replace(/%[0-9a-f]{2}/gi, \" \")\n .replace(/[()[\\]{}]/g, \" \")\n .replace(/[._-]+/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n text = text\n .replace(/\\b(copy|final|edited|edit|export|upload)\\b/gi, \" \")\n .replace(/\\b(screenshot|screen shot|image|img|photo|picture|pic)\\b/gi, \" \")\n .replace(/\\b(dsc|dcim|pxl|kakaotalk|wechatimg)\\s*\\d+\\b/gi, \" \")\n // 내려받기·내보내기 도구가 붙인 UUID·해시 이름(예: b50de5de4a2ced4a7807)은\n // 이미지 내용과 무관하다. 숫자와 a–f가 섞인 8자 이상 16진 토큰을 지운다.\n .replace(/\\b[0-9a-f]{8}(?: [0-9a-f]{4}){3} [0-9a-f]{12}\\b/gi, \" \")\n .replace(/\\b(?=[0-9a-f]*\\d)(?=[0-9a-f]*[a-f])[0-9a-f]{8,}\\b/gi, \" \")\n .replace(/\\b\\d{3,4}\\s*x\\s*\\d{3,4}\\b/gi, \" \")\n .replace(/\\b(19|20)\\d{2}[ ._-]?(0[1-9]|1[0-2])[ ._-]?([0-2]\\d|3[01])\\b/g, \" \")\n .replace(/\\b\\d{4,}\\b/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (!text || !/[0-9A-Za-z가-힣一-龥ぁ-んァ-ン]/.test(text)) {\n return \"업로드 이미지\";\n }\n\n const capped = text.slice(0, MAX_ALT_LEN).trim();\n return capped.charAt(0).toLocaleUpperCase(\"ko-KR\") + capped.slice(1);\n}\n\n/**\n * 업로드 객체명에 붙일 SEO-friendly filename.\n *\n * URL 경로에 의미 있는 단어가 남도록 한글/CJK/라틴 문자와 숫자는 보존하고,\n * 나머지는 hyphen 으로 접는다. 호출부는 앞에 UUID 를 붙여 충돌을 피한다.\n */\nexport function seoFilenameFromFilename(\n original: string,\n fallbackExt = \"\",\n): string {\n const ext = extractExt(basenameOf(original)) || fallbackExt.toLowerCase();\n const alt = suggestAltFromFilename(original);\n const source = alt === \"업로드 이미지\" ? \"uploaded image\" : alt;\n const stem = source\n .normalize(\"NFKC\")\n .toLocaleLowerCase(\"ko-KR\")\n .replace(/[^\\p{L}\\p{N}]+/gu, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, MAX_SEO_STEM_LEN)\n .replace(/-$/g, \"\");\n const safeStem = stem || \"uploaded-image\";\n return `${safeStem}${EXT_PATTERN.test(ext) ? ext : \"\"}`;\n}\n","// ADR-0034 §7.1.1 test #4 — R2 client wrapper (interface only, Phase 1 stub).\n//\n// Phase 1 environment-specific implementation:\n// - Cloudflare Workers/Pages = R2 binding (env.R2.put/get/delete)\n// - Vercel/Node = AWS SDK S3 client (S3-compatible API)\n//\n// 본 모듈은 interface + Korean filename + tenant prefix 강제 로직만.\n// 실 binding 은 tenant-admin 의 server action 또는 tenant-api 의 R2 handler 가 주입.\n\nimport { normalizeFilename } from \"./normalize-filename\";\n\n/** ADR-0016 multi-tenant 격리 의무 — r2_key prefix = `tenants/<tenant_id>/` */\nexport function r2KeyForTenant(\n tenantId: string,\n category: string,\n filename: string,\n): string {\n const { safe } = normalizeFilename(filename);\n return `tenants/${tenantId}/${category}/${safe}`;\n}\n\nexport interface R2PutOptions {\n contentType: string;\n cacheControl?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface R2Object {\n key: string;\n size: number;\n etag: string;\n uploaded: Date;\n contentType?: string;\n metadata?: Record<string, string>;\n}\n\n/**\n * R2 client interface — environment-specific binding 이 본 interface 구현.\n * tenant-admin 가 server action 안에서 inject (DI).\n */\nexport interface R2Client {\n put(key: string, body: ArrayBuffer | Uint8Array, options: R2PutOptions): Promise<R2Object>;\n get(key: string): Promise<ArrayBuffer | null>;\n delete(key: string): Promise<void>;\n /** presigned URL — 외부 캐싱/CDN 용. Phase 1 = direct R2 public bucket */\n getPublicUrl(key: string): string;\n}\n\n/**\n * Tenant-aware R2 helper — key prefix 강제 + Korean filename normalize 자동.\n * tenant 외부에서 본 wrapper 우회하면 ESLint 의 no-raw-db-client 와 비슷한 rule\n * 추가 검토 (Phase 1b).\n */\nexport class TenantR2 {\n constructor(\n private readonly client: R2Client,\n private readonly tenantId: string,\n ) {}\n\n async uploadMedia(\n filename: string,\n body: ArrayBuffer | Uint8Array,\n contentType: string,\n ): Promise<{ key: string; publicUrl: string; object: R2Object }> {\n const key = r2KeyForTenant(this.tenantId, \"media\", filename);\n const object = await this.client.put(key, body, { contentType });\n return { key, publicUrl: this.client.getPublicUrl(key), object };\n }\n\n async deleteByKey(key: string): Promise<void> {\n if (!key.startsWith(`tenants/${this.tenantId}/`)) {\n throw new Error(\n `tenant 격리 위반: ${key} 는 tenants/${this.tenantId}/ prefix 와 일치하지 않음`,\n );\n }\n await this.client.delete(key);\n }\n}\n","// ADR-0034 §7.1.1 test #4 — AWS S3 SDK based R2Client 실 구현.\n//\n// Cloudflare R2 는 S3-compatible API 노출. @aws-sdk/client-s3 wrapper.\n// caller 가 endpoint + access key (Doppler) 주입.\n\nimport type { R2Client, R2Object, R2PutOptions } from \"./r2-client\";\n\nexport interface S3ClientConfig {\n endpoint: string; // e.g. https://<account>.r2.cloudflarestorage.com\n region?: string; // R2 = \"auto\"\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n publicBaseUrl: string; // CDN 또는 R2 public URL prefix\n}\n\n/**\n * AWS SDK 의존을 minimal — type-only declaration.\n * 실 binding 은 tenant-admin 의 server action 또는 tenant-api 가 SDK 주입.\n * 본 module 은 R2Client interface 의 S3-style 구현 스켈레톤.\n */\ninterface MinimalS3Sdk {\n send(command: unknown): Promise<{ ETag?: string; ContentLength?: number; Body?: AsyncIterable<Uint8Array> }>;\n}\n\ninterface S3PutObjectCommandInput {\n Bucket: string;\n Key: string;\n Body: Uint8Array;\n ContentType: string;\n CacheControl?: string;\n Metadata?: Record<string, string>;\n}\n\ninterface S3GetObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\ninterface S3DeleteObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\nexport interface S3CommandFactories {\n PutObjectCommand: new (input: S3PutObjectCommandInput) => unknown;\n GetObjectCommand: new (input: S3GetObjectCommandInput) => unknown;\n DeleteObjectCommand: new (input: S3DeleteObjectCommandInput) => unknown;\n}\n\n/**\n * R2Client 구현 (S3 SDK based). caller 가 SDK + commands 주입.\n *\n * 사용 예시 (tenant-admin):\n * import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand }\n * from \"@aws-sdk/client-s3\";\n * const r2 = new S3R2Client(\n * new S3Client({ endpoint, region: \"auto\", credentials: {...} }),\n * { PutObjectCommand, GetObjectCommand, DeleteObjectCommand },\n * config,\n * );\n */\nexport class S3R2Client implements R2Client {\n constructor(\n private readonly sdk: MinimalS3Sdk,\n private readonly commands: S3CommandFactories,\n private readonly config: S3ClientConfig,\n ) {}\n\n async put(\n key: string,\n body: ArrayBuffer | Uint8Array,\n options: R2PutOptions,\n ): Promise<R2Object> {\n const buf = body instanceof Uint8Array ? body : new Uint8Array(body);\n const cmd = new this.commands.PutObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n Body: buf,\n ContentType: options.contentType,\n CacheControl: options.cacheControl,\n Metadata: options.metadata,\n });\n const result = await this.sdk.send(cmd);\n return {\n key,\n size: buf.byteLength,\n etag: result.ETag ?? \"\",\n uploaded: new Date(),\n contentType: options.contentType,\n metadata: options.metadata,\n };\n }\n\n async get(key: string): Promise<ArrayBuffer | null> {\n const cmd = new this.commands.GetObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n });\n try {\n const result = await this.sdk.send(cmd);\n if (!result.Body) return null;\n const chunks: Uint8Array[] = [];\n for await (const chunk of result.Body) {\n chunks.push(chunk);\n }\n const total = chunks.reduce((a, c) => a + c.byteLength, 0);\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return merged.buffer;\n } catch (_err) {\n return null;\n }\n }\n\n async delete(key: string): Promise<void> {\n const cmd = new this.commands.DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n });\n await this.sdk.send(cmd);\n }\n\n getPublicUrl(key: string): string {\n const base = this.config.publicBaseUrl.replace(/\\/$/, \"\");\n return `${base}/${key}`;\n }\n}\n","import type { ArchiveReceipt } from \"./archive-types\";\n\n/** Cached proof is a backup observation, never permission to create/delete media. */\nexport const ARCHIVE_IDENTITY_MAX_AGE_MS = 45 * 60_000;\nexport const ARCHIVE_BYTES_MAX_AGE_MS = 26 * 60 * 60_000;\nexport const ARCHIVE_FULL_SCRUB_INTERVAL_MS = 24 * 60 * 60_000;\nexport const ARCHIVE_EVIDENCE_BATCH_LIMIT = 10_000;\nexport interface ArchiveObjectIdentity { etag: string; bytes: number }\nexport interface ArchiveSourceLocator { tenantId: string; sourceBucket: string; sourceKey: string }\nexport interface ArchiveIdentityProof {\n method: \"receipt_identity\";\n byteVerifiedAt: string;\n identityCheckedAt: string;\n ciphertext: ArchiveObjectIdentity;\n metadata: ArchiveObjectIdentity;\n}\nexport interface ArchiveSourceEvidence {\n tenantId: string;\n sourceDigest: string;\n reason: \"verified\" | \"evidence_missing\" | \"evidence_stale\";\n receipt: ArchiveReceipt | null;\n proof: ArchiveIdentityProof | null;\n /** Last completed source reconciliation attempt; scheduling only, never verification proof. */\n lastAttemptAt?: string;\n}\nexport function archiveProofFresh(proof: ArchiveIdentityProof, now: number): boolean {\n const bytesAge = now - Date.parse(proof.byteVerifiedAt);\n const identityAge = now - Date.parse(proof.identityCheckedAt);\n return Number.isFinite(bytesAge) && bytesAge >= 0 && bytesAge <= ARCHIVE_BYTES_MAX_AGE_MS\n && Number.isFinite(identityAge) && identityAge >= 0 && identityAge <= ARCHIVE_IDENTITY_MAX_AGE_MS\n && Date.parse(proof.identityCheckedAt) >= Date.parse(proof.byteVerifiedAt);\n}\nexport function parseArchiveIdentityProof(value: unknown): ArchiveIdentityProof {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid archive proof\");\n const proof = value as Record<string, unknown>;\n function time(value: unknown): string {\n if (typeof value !== \"string\" || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) throw new Error(\"Invalid archive proof date\");\n return value;\n }\n function identity(value: unknown): ArchiveObjectIdentity {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid archive identity\");\n const object = value as Record<string, unknown>;\n if (typeof object.etag !== \"string\" || !object.etag || object.etag.length > 256 || /[\\x00-\\x20\\x7f]/.test(object.etag)\n || typeof object.bytes !== \"number\" || !Number.isSafeInteger(object.bytes) || object.bytes <= 0) throw new Error(\"Invalid archive identity\");\n return { etag: object.etag, bytes: object.bytes };\n }\n if (proof.method !== \"receipt_identity\") throw new Error(\"Invalid archive proof method\");\n return { method: proof.method, byteVerifiedAt: time(proof.byteVerifiedAt), identityCheckedAt: time(proof.identityCheckedAt),\n ciphertext: identity(proof.ciphertext), metadata: identity(proof.metadata) };\n}\n","import { ARCHIVE_EVIDENCE_BATCH_LIMIT, parseArchiveIdentityProof, type ArchiveSourceLocator, type ArchiveSourceEvidence } from \"./archive-evidence\";\nimport type { ArchiveReceipt } from \"./archive-types\";\nexport type { ArchiveReceipt } from \"./archive-types\";\nexport * from \"./archive-evidence\";\n/** Online archive stores only GPG ciphertext; applications never receive backup credentials. */\nexport interface MediaArchiveConfig {\n baseUrl: string;\n token: string;\n fetchImpl?: typeof fetch;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface PreserveMediaInput {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n contentType: string;\n bytes: ArrayBuffer;\n uploadId?: string;\n}\n\nexport interface MediaTombstoneInput {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n receipt: ArchiveReceipt;\n deletedAt: string;\n metadata: Record<string, unknown>;\n}\n\nexport interface MediaArchiveClient {\n preserve(input: PreserveMediaInput): Promise<ArchiveReceipt>;\n verify(receipt: ArchiveReceipt): Promise<ArchiveReceipt>;\n findSource(input: { tenantId: string; sourceBucket: string; sourceKey: string }): Promise<ArchiveReceipt | null>;\n tombstone(input: MediaTombstoneInput): Promise<void>;\n /** Read-only cached observation. Must never authorize an upload or deletion. */\n evidence?(sources: ArchiveSourceLocator[]): Promise<ArchiveSourceEvidence[]>;\n /** Separate scrub workload: verifies source/receipt identity and periodically bytes. */\n reconcileSource?(source: ArchiveSourceLocator): Promise<ArchiveSourceEvidence>;\n}\n\nexport const ARCHIVE_FAILURE_REASONS = [\"missing_receipt\", \"checksum_mismatch\", \"invalid_receipt\", \"auth\", \"throttled\", \"timeout\", \"transport\", \"busy\", \"unavailable\"] as const;\nexport type ArchiveFailureReason = typeof ARCHIVE_FAILURE_REASONS[number];\nexport class MediaPreservationError extends Error {\n constructor(message = \"파일 보존을 확인하지 못했습니다. 잠시 후 다시 시도해 주세요.\",\n readonly reason: ArchiveFailureReason = \"invalid_receipt\", readonly status?: number) {\n super(message);\n this.name = \"MediaPreservationError\";\n }\n}\n\n/** Organization support attachments have their own owner namespace, never a fake tenant. */\nexport function archiveOwnerPrefix(scopeId: string): string {\n if (/^org-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(scopeId)) {\n return `organizations/${scopeId.slice(4)}/`;\n }\n if (!/^[A-Za-z0-9_-]+$/.test(scopeId) || scopeId.startsWith(\"org-\")) throw new MediaPreservationError();\n return `tenants/${scopeId}/`;\n}\n\nexport function assertTenantObjectKey(tenantId: string, key: string): void {\n if (!/^[A-Za-z0-9_-]+$/.test(tenantId) ||\n !(key.startsWith(archiveOwnerPrefix(tenantId)) ||\n (tenantId.startsWith(\"org-\") && key.startsWith(`support/${tenantId.slice(4)}/`)) ||\n (!tenantId.startsWith(\"org-\") && key.startsWith(`${tenantId}/medical/records/`))) ||\n key.split(\"/\").some((part) => !part || part === \".\" || part === \"..\") ||\n /[\\\\\\x00-\\x1f\\x7f]/.test(key)) {\n throw new MediaPreservationError(\"잘못된 파일 소유 경로입니다.\");\n }\n}\n\nexport async function mediaSha256(bytes: ArrayBuffer): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes));\n return Array.from(digest, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction metadataHeader(value: unknown): string {\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction readReceipt(value: unknown, tenantId: string, sha256: string, sizeBytes: number): ArchiveReceipt {\n if (!/^[a-f0-9]{64}$/.test(sha256) || !Number.isSafeInteger(sizeBytes) || sizeBytes < 0 ||\n !value || typeof value !== \"object\") throw new MediaPreservationError();\n const item = value as Record<string, unknown>;\n if (item.version !== 1 || item.tenantId !== tenantId || item.sha256 !== sha256 ||\n item.sizeBytes !== sizeBytes ||\n typeof item.archiveKey !== \"string\" ||\n !new RegExp(`^${archiveOwnerPrefix(tenantId)}media-archive/${sha256}/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/original\\\\.gpg$`).test(item.archiveKey) ||\n typeof item.cipherSha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(item.cipherSha256) ||\n item.metadataKey !== item.archiveKey.replace(/original\\.gpg$/, \"metadata.gpg\") ||\n typeof item.metadataCipherSha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(item.metadataCipherSha256) ||\n typeof item.verifiedAt !== \"string\" || !Number.isFinite(Date.parse(item.verifiedAt))) {\n throw new MediaPreservationError();\n }\n return { version: 1, tenantId, sha256, sizeBytes, archiveKey: item.archiveKey,\n cipherSha256: item.cipherSha256, metadataKey: item.metadataKey,\n metadataCipherSha256: item.metadataCipherSha256, verifiedAt: item.verifiedAt };\n}\n\nexport function createMediaArchiveClient(config: MediaArchiveConfig): MediaArchiveClient {\n const base = new URL(config.baseUrl);\n if (base.protocol !== \"https:\" || base.username || base.password || !config.token.trim()) {\n throw new MediaPreservationError(\"파일 보존 연결 설정을 확인해 주세요.\");\n }\n const fetchImpl = config.fetchImpl ?? fetch;\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n async function request(path: string, init: RequestInit, allowMissing = false, readOnly = false,\n timeoutMs = init.method === \"GET\" || readOnly ? 10_000 : 60_000, totalBudgetMs?: number): Promise<unknown> {\n const started = Date.now();\n for (let attempt = 1; ; attempt++) {\n const remaining = totalBudgetMs === undefined ? timeoutMs : totalBudgetMs - (Date.now() - started);\n if (remaining <= 0) throw new MediaPreservationError(undefined, \"timeout\");\n let retryAfterMs = 0;\n try {\n const headers = new Headers(init.headers);\n headers.set(\"Authorization\", `Bearer ${config.token}`);\n const response = await fetchImpl(new URL(path, base), {\n // Workers reject redirect:\"error\"; manual keeps credentials on this origin.\n ...init, headers, redirect: \"manual\", signal: AbortSignal.timeout(Math.min(timeoutMs, remaining)),\n });\n if (response.status === 404 && allowMissing) { await response.body?.cancel(); return null; }\n if (!response.ok) {\n const rawReason = response.headers.get(\"x-archive-failure\");\n const reason = ARCHIVE_FAILURE_REASONS.find((value) => value === rawReason)\n ?? ([401, 403].includes(response.status) ? \"auth\" : response.status === 429 ? \"throttled\"\n : [408, 500, 502, 503, 504].includes(response.status) ? \"unavailable\" : \"invalid_receipt\");\n const retryAfter = response.headers.get(\"retry-after\");\n retryAfterMs = retryAfter && /^\\d+$/.test(retryAfter) ? Number(retryAfter) * 1000\n : retryAfter && Number.isFinite(Date.parse(retryAfter)) ? Math.max(0, Date.parse(retryAfter) - Date.now()) : 0;\n await response.body?.cancel();\n throw new MediaPreservationError(undefined, reason, response.status);\n }\n return await response.json();\n } catch (error) {\n // Gateway failures must not leak a URL, token, or source metadata to client logs.\n const safe = error instanceof MediaPreservationError ? error : new MediaPreservationError(undefined,\n error instanceof Error && [\"TimeoutError\", \"AbortError\"].includes(error.name) ? \"timeout\"\n : error instanceof SyntaxError ? \"invalid_receipt\" : \"transport\");\n const delay = Math.max(retryAfterMs, 250 * 2 ** (attempt - 1) + Math.random() * 250);\n const budgetExceeded = totalBudgetMs === undefined\n ? Date.now() - started + delay + timeoutMs > 3 * timeoutMs + 5_000\n : Date.now() - started + delay >= totalBudgetMs;\n if ((!readOnly && init.method !== \"GET\") || attempt >= 3 || ![\"transport\", \"timeout\", \"throttled\", \"busy\", \"unavailable\"].includes(safe.reason)\n || budgetExceeded) throw safe;\n await sleep(delay);\n }\n }\n }\n function validateSource(input: ArchiveSourceLocator): void {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n if (!/^[a-z0-9][a-z0-9.-]{1,62}$/.test(input.sourceBucket)) throw new MediaPreservationError();\n }\n async function sourceDigest(input: ArchiveSourceLocator): Promise<string> {\n validateSource(input);\n return mediaSha256(new TextEncoder().encode(`${input.sourceBucket}\\n${input.sourceKey}`).buffer);\n }\n async function evidenceItem(value: unknown, input: ArchiveSourceLocator): Promise<ArchiveSourceEvidence> {\n try {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error();\n const item = value as Record<string, unknown>;\n const digest = await sourceDigest(input);\n if (item.tenantId !== input.tenantId || item.sourceDigest !== digest) throw new Error();\n let lastAttemptAt: string | undefined;\n if (item.lastAttemptAt !== undefined) {\n if (typeof item.lastAttemptAt !== \"string\" || !Number.isFinite(Date.parse(item.lastAttemptAt))\n || new Date(item.lastAttemptAt).toISOString() !== item.lastAttemptAt) throw new Error();\n lastAttemptAt = item.lastAttemptAt;\n }\n const attemptHint = lastAttemptAt === undefined ? {} : { lastAttemptAt };\n if (item.reason === \"evidence_missing\" || item.reason === \"evidence_stale\") {\n if (item.receipt !== null || item.proof !== null) throw new Error();\n return { tenantId: input.tenantId, sourceDigest: digest, reason: item.reason, receipt: null, proof: null, ...attemptHint };\n }\n if (item.reason !== \"verified\" || !item.receipt || typeof item.receipt !== \"object\") throw new Error();\n const raw = item.receipt as Record<string, unknown>;\n if (typeof raw.sha256 !== \"string\" || typeof raw.sizeBytes !== \"number\") throw new Error();\n return { tenantId: input.tenantId, sourceDigest: digest, reason: \"verified\",\n receipt: readReceipt(raw, input.tenantId, raw.sha256, raw.sizeBytes), proof: parseArchiveIdentityProof(item.proof), ...attemptHint };\n } catch { throw new MediaPreservationError(); }\n }\n return {\n async evidence(sources) {\n if (sources.length > ARCHIVE_EVIDENCE_BATCH_LIMIT) throw new MediaPreservationError();\n for (const source of sources) validateSource(source);\n const value = await request(\"/v2/source-evidence\", { method: \"POST\", headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ version: 2, sources }) }, false, true);\n if (!value || typeof value !== \"object\" || !(\"version\" in value) || value.version !== 2\n || !(\"items\" in value) || !Array.isArray(value.items) || value.items.length !== sources.length) throw new MediaPreservationError();\n const parsed: ArchiveSourceEvidence[] = [];\n // Bound WebCrypto jobs and temporary receipt objects as inventory grows.\n for (let start = 0; start < sources.length; start += 64) {\n parsed.push(...await Promise.all(value.items.slice(start, start + 64)\n .map((item, offset) => evidenceItem(item, sources[start + offset]!))));\n }\n return parsed;\n },\n async reconcileSource(source) {\n const digest = await sourceDigest(source);\n // Cold/periodic scrub may stream a complete object. Let the gateway's 55s\n // operation deadline settle. Retries share one 60s budget so the scrub can\n // still publish its result before shutdown; snapshot observation keeps 10s.\n const value = await request(`/v2/sources/${source.tenantId}/${digest}`, { method: \"GET\" }, true, false, 60_000, 60_000);\n if (value === null) throw new MediaPreservationError(undefined, \"missing_receipt\");\n return evidenceItem(value, source);\n },\n async preserve(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const sha256 = await mediaSha256(input.bytes);\n const value = await request(`/v1/objects/${input.tenantId}/${sha256}`, {\n method: \"PUT\",\n headers: { \"Content-Type\": input.contentType, \"X-Archive-Metadata\": metadataHeader({\n sourceKey: input.sourceKey, sourceBucket: input.sourceBucket, contentType: input.contentType, uploadId: input.uploadId,\n }) },\n body: input.bytes,\n });\n return readReceipt(value, input.tenantId, sha256, input.bytes.byteLength);\n },\n async findSource(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const hash = await mediaSha256(new TextEncoder().encode(`${input.sourceBucket}\\n${input.sourceKey}`).buffer);\n const value = await request(`/v1/sources/${input.tenantId}/${hash}`, { method: \"GET\" }, true);\n if (value === null) return null;\n if (!value || typeof value !== \"object\" || !(\"sha256\" in value) || typeof value.sha256 !== \"string\" ||\n !/^[a-f0-9]{64}$/.test(value.sha256) || !(\"sizeBytes\" in value) || typeof value.sizeBytes !== \"number\") {\n throw new MediaPreservationError();\n }\n return readReceipt(value, input.tenantId, value.sha256, value.sizeBytes);\n },\n async verify(receipt) {\n readReceipt(receipt, receipt.tenantId, receipt.sha256, receipt.sizeBytes);\n const value = await request(`/v1/objects/${receipt.tenantId}/${receipt.sha256}`, { method: \"GET\" });\n const verified = readReceipt(value, receipt.tenantId, receipt.sha256, receipt.sizeBytes);\n if (verified.cipherSha256 !== receipt.cipherSha256 || verified.metadataKey !== receipt.metadataKey ||\n verified.metadataCipherSha256 !== receipt.metadataCipherSha256) throw new MediaPreservationError();\n return verified;\n },\n async tombstone(input) {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n if (input.receipt.tenantId !== input.tenantId) throw new MediaPreservationError();\n const value = await request(\"/v1/tombstones\", {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(input),\n });\n if (!value || typeof value !== \"object\" || !(\"recorded\" in value) || value.recorded !== true ||\n !(\"tombstoneKey\" in value) || typeof value.tombstoneKey !== \"string\" ||\n !value.tombstoneKey.startsWith(archiveOwnerPrefix(input.tenantId))) throw new MediaPreservationError();\n },\n };\n}\n\nexport interface PreservationStorage {\n get(key: string): Promise<{ bytes: ArrayBuffer; contentType: string } | null>;\n /** Conditional create only. Existing keys must never be overwritten. */\n putIfAbsent(key: string, bytes: ArrayBuffer, contentType: string): Promise<void>;\n}\n\nexport interface FinalizePreservedUploadInput {\n tenantId: string;\n siteId: string;\n stagingKey: string;\n expectedContentType: string;\n expectedSize: number;\n sourceBucket: string;\n storage: PreservationStorage;\n archive: MediaArchiveClient;\n}\n\n/** Staging upload ID is the media primary key, so retries cannot insert duplicate rows. */\nexport function mediaUploadId(stagingKey: string): string {\n const match = /\\/staging\\/([0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})[.-]/.exec(stagingKey);\n if (!match?.[1]) throw new MediaPreservationError(\"잘못된 업로드 경로입니다.\");\n return match[1];\n}\n\nexport async function finalizePreservedUpload(input: FinalizePreservedUploadInput): Promise<{\n id: string; key: string; receipt: ArchiveReceipt;\n}> {\n assertTenantObjectKey(input.tenantId, input.stagingKey);\n const id = mediaUploadId(input.stagingKey);\n const original = await input.storage.get(input.stagingKey);\n if (!original || original.bytes.byteLength !== input.expectedSize ||\n original.contentType.split(\";\", 1)[0]?.trim().toLowerCase() !== input.expectedContentType) {\n throw new MediaPreservationError(\"업로드된 파일의 형식과 크기를 확인해 주세요.\");\n }\n const sha256 = await mediaSha256(original.bytes);\n const filename = input.stagingKey.slice(input.stagingKey.lastIndexOf(\"/\") + 1);\n const key = `tenants/${input.tenantId}/sites/${input.siteId}/media/originals/${sha256}/${filename}`;\n await input.storage.putIfAbsent(key, original.bytes, input.expectedContentType);\n const final = await input.storage.get(key);\n if (!final || await mediaSha256(final.bytes) !== sha256) throw new MediaPreservationError();\n const receipt = await input.archive.preserve({ tenantId: input.tenantId, sourceBucket: input.sourceBucket,\n sourceKey: key, contentType: input.expectedContentType, bytes: final.bytes, uploadId: id });\n return { id, key, receipt };\n}\n\n/** Snapshot bytes once; later source mutations cannot change the encrypted copy. */\nexport async function preserveStoredObject(input: {\n tenantId: string; sourceBucket: string; sourceKey: string;\n storage: Pick<PreservationStorage, \"get\">; archive: MediaArchiveClient;\n}): Promise<ArchiveReceipt> {\n assertTenantObjectKey(input.tenantId, input.sourceKey);\n const original = await input.storage.get(input.sourceKey);\n if (!original) {\n const existing = await input.archive.findSource(input);\n if (!existing) throw new MediaPreservationError(\"보존할 원본 파일을 찾을 수 없습니다.\");\n return input.archive.verify(existing);\n }\n return input.archive.preserve({ tenantId: input.tenantId, sourceBucket: input.sourceBucket,\n sourceKey: input.sourceKey, ...original });\n}\n","import {\n assertTenantObjectKey,\n mediaSha256,\n MediaPreservationError,\n type ArchiveReceipt,\n type MediaArchiveClient,\n type PreservationStorage,\n} from \"./archive\";\n\n/** Caller authorizes the staging locator; only the new tenant-owned key can be archived. */\nexport async function preserveStoredUpload(input: {\n tenantId: string;\n sourceKey: string;\n sourceBucket: string;\n expectedSize: number;\n allowedContentTypes: readonly string[];\n finalKey: (sha256: string) => string;\n storage: PreservationStorage;\n archive: MediaArchiveClient;\n}): Promise<{ key: string; receipt: ArchiveReceipt }> {\n const original = await input.storage.get(input.sourceKey);\n const contentType = original?.contentType.split(\";\", 1)[0]?.trim().toLowerCase();\n if (!original || original.bytes.byteLength !== input.expectedSize || !contentType ||\n !input.allowedContentTypes.includes(contentType)) {\n throw new MediaPreservationError(\"업로드된 파일의 형식과 크기를 확인해 주세요.\");\n }\n const sha256 = await mediaSha256(original.bytes);\n const key = input.finalKey(sha256);\n assertTenantObjectKey(input.tenantId, key);\n if (key === input.sourceKey) throw new MediaPreservationError(\"보존 파일은 별도 경로가 필요합니다.\");\n await input.storage.putIfAbsent(key, original.bytes, contentType);\n const stored = await input.storage.get(key);\n if (!stored || await mediaSha256(stored.bytes) !== sha256 ||\n stored.contentType.split(\";\", 1)[0]?.trim().toLowerCase() !== contentType) {\n throw new MediaPreservationError();\n }\n const receipt = await input.archive.preserve({\n tenantId: input.tenantId,\n sourceKey: key,\n sourceBucket: input.sourceBucket,\n contentType,\n bytes: stored.bytes,\n });\n return { key, receipt };\n}\n"],"mappings":"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","names":["value"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-media",
3
- "version": "0.72.1",
3
+ "version": "0.73.0",
4
4
  "type": "module",
5
5
  "description": "RootTale 미디어 — R2 원본 보관 + CF Images variants + 한글 파일명 정규화 + CfImage 렌더 컴포넌트(@roottale/cms-media/react). ADR-0034 §3 / docs/image-standard.md.",
6
6
  "main": "./dist/index.js",