Install the REST auth profile registry once at composition and make it the single transport authority for V3. Contract composition now rejects an unregistered authProfileId, so the executor never resolves a profile at runtime. The credential collaborator contributes proof headers only: Fetch credentials come from the resolved profile, transport-owned and forbidden headers are rejected, headers outside the profile's allowed set are rejected, and a missing required header fails closed as AUTH_INTEGRATION_FAILURE with zero fetch calls. The final invariant re-proves credentials mode and the exact header sets. Demo mode satisfies the strict bearer profile with a fixed non-secret marker instead of weakening REFERENCE_EXTERNAL_BEARER. Credential owners now receive the operation lifetime through AuthOperationContext. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 KiB
Storage / browser-file adapters 구현 준비 코드 리뷰
검토 저장소: /home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template
검토 범위: src/adapters/storage/**, src/adapters/browser-files/**, src/adapters/browser-file-storage/**, src/adapters/cache-storage/** 및 직접 연결된 application port, contract, bootstrap, test, architecture/operations 문서
검토 방식: 구현 파일을 수정하지 않은 read-only 리뷰. 아래 line은 현재 worktree 기준이다.
0. 결론과 우선순위
| ID | 판정 | 심각도 | 확신도 | 요약 |
|---|---|---|---|---|
| STO-01 | 확정 결함 | Critical | 높음 | OPFS pre-commit 보상 cleanup 실패/취소를 무시하고 journal을 rollback한다. 늦게 도착한 generation-only cleanup이 후속 write의 같은 logical generation을 삭제할 수 있고, 그렇지 않아도 복구 근거와 quota를 잃는다. |
| STO-02 | 확정 결함 | High | 높음 | browser-managed download는 baseOrigin으로 상대 URL을 검증하지만 원문 href를 document.baseURI로 실행한다. <base>가 있으면 검증한 origin과 실제 navigation origin이 달라진다. |
| STO-03 | 확정 결함 | Medium | 높음 | public cache policy가 allowedVaryHeaderNames를 허용하면서 response allowlist에서 vary를 제거하는 모순을 허용한다. stage는 성공할 수 있지만 저장 variant가 충돌하고 activation이 실패한다. |
| STO-04 | 확정 결함 | Medium | 높음 | 동일 manifest 재-stage가 marker와 count만 신뢰한다. marker 작성 뒤 browser eviction/부분 손상된 candidate를 성공으로 재사용하여 self-heal하지 못한다. activation은 fail-closed지만 staging success 의미가 약해진다. |
| STO-05 | 확정 결함 | Medium | 높음 | cache activateRelease/cleanupOwned가 네트워크 fetch를 쓰지 않는데도 공통 availability guard가 fetcher를 필수로 요구한다. offline activation/rollback/cleanup이 불필요하게 UNSUPPORTED가 된다. |
| STO-06 | 확정 계약 위반 | Medium | 높음 | IndexedDB codec migration은 commit transaction 내부의 연속 native operation 사이에 monotonic deadline을 재확인하지 않는다. 문서/port의 cooperative duration contract보다 오래 실행될 수 있다. |
| STO-07 | hardening 후보 | Medium | 높음 | OPFS worker envelope에 protocol version/response kind가 없고 client response parser가 {requestId, ok}만 검사한다. page/worker release 불일치와 malformed response를 INCOMPATIBLE로 닫을 수 없다. |
| STO-08 | 브라우저 검증 필요 | Low | 중간 | enhanced open/save picker 함수를 Window가 아니라 options 객체에 bind한다. Web IDL brand check가 있는 engine에서는 Illegal invocation 가능성이 있으나 현재 unit fake는 이를 검증하지 않는다. 실제 browser test로 먼저 확정한다. |
| GAP-01 | 문서화된 미구현 | High readiness gap | 높음 | preview pixel/decoded-byte/frame/decode probe가 없다. VD-15가 이미 DESIGNED_NOT_IMPLEMENTED로 명시했으므로 regression으로 오인하지 말고, untrusted image preview 조립의 promotion blocker로 취급한다. |
| GAP-02 | 문서화된 미구현 | High readiness gap | 높음 | Cache inspect/cleanup은 cursor/count/deadline 없이 전체 owned namespace를 순회한다. VD-15가 정확히 현 상태를 기록한다. |
| GAP-03 | 문서화된 미구현 | High readiness gap | 높음 | origin-wide pressure/write-admission/GC, OPFS/Cache forward migration, real OPFS preflight가 아직 없다. 기존 per-store primitive를 완성 증거로 삼지 않는다. |
즉시 순서는 STO-01 write 차단/수정 → STO-02 canonical URL 실행 → STO-03~05 cache 불변식 → STO-06/07 hardening이다. GAP 항목은 해당 capability를 제품에 선택·조립하기 전에 별도 promotion gate로 구현한다.
1. 누락 없는 범위 inventory: 책임과 의존성
1.1 browser-file-storage
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
|---|---|---|
src/adapters/browser-file-storage/index.ts |
browser data 공통 Result와 StorageManager adapter barrel export | 내부 두 모듈만 export. 경계가 작고 유지 대상. |
src/adapters/browser-file-storage/result.ts |
native 예외를 closed BrowserDataFailure로 정규화하고 안전한 observation 제공 |
application/ports/browser-file-storage/shared.ts; raw path/name/message 비노출, observer 예외 격리가 좋다. |
src/adapters/browser-file-storage/storage-manager-adapter.ts |
estimate/persisted/persist snapshot, pressure bucket, user-activation-bound persistence 요청 |
storage durability port/result. estimate를 예약량으로 오인하지 않고 irreversible persist() truth를 보존한다. origin coordinator는 의도적으로 없음(GAP-03). |
1.2 browser-files
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
|---|---|---|
src/adapters/browser-files/browser-file-picker.ts |
native input baseline 및 enhanced system picker, activation/abort/dismissal, vault capture | file port, vault, policy registry. baseline/enhancement 분리가 좋다. showOpenFilePicker.bind(options)는 STO-08. |
src/adapters/browser-files/browser-file-policy-registry.ts |
composition-owned selection/inspection/preview/download policy 등록·identity 확인·hard-cap reduction | file contracts, file-policy.ts. WeakSet/identity binding과 frozen snapshot을 유지한다. |
src/adapters/browser-files/browser-file-vault.ts |
transient native File/handle 보관, opaque ref, inspection receipt, bounded range/source | file port/shared/result/policy registry. File이 application 경계를 넘지 않고 receipt가 exact file/profile에 묶이는 설계가 좋다. |
src/adapters/browser-files/create-browser-file-runtime.ts |
vault/picker/preview/download를 선택적으로 조립하고 일괄 dispose | 위 adapters 및 application contracts. optional capability를 제품 선택 없이 bootstrap에 암묵 조립하지 않는 점을 유지. preview 조립 전 GAP-01 gate 필요. |
src/adapters/browser-files/download-delivery-adapter.ts |
browser handoff, foreground save stream, bounded object URL download, integrity/progress/cancellation | file/authorized-download ports, policy registry, object URL lease, Result. STO-02와 STO-08; stream close truth/backpressure는 유지. |
src/adapters/browser-files/file-observer.ts |
file-safe observation DTO를 공통 browser observation으로 변환 | shared port/result. raw filename/ref 비노출 유지. |
src/adapters/browser-files/file-policy.ts |
policy input validation, MIME/extension/signature/hard byte caps, immutable resolved policy | file/shared contracts. closed allowlist 및 absolute ceiling을 유지. |
src/adapters/browser-files/index.ts |
browser-file public exports | 위 모듈. native implementation detail export 확장을 피한다. |
src/adapters/browser-files/object-url-lease.ts |
중앙 object URL lease cap/registry, transient preview, idempotent revoke/dispose | file/shared contracts, vault, policy registry. URL lifecycle은 좋으나 create() 256-303은 decode probe 없이 URL을 발급(GAP-01). |
1.3 cache-storage
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
|---|---|---|
src/adapters/cache-storage/index.ts |
public cache policy/adapter barrel | optional public static cache만 export; private/range cache로 일반화하지 않는다. |
src/adapters/cache-storage/public-cache-policy.ts |
same-origin/public-only release 정책, URL/header/query/size/retention hard limits | cache ports/shared. STO-03 policy cross-field invariant 누락. 기본 policy에는 vary가 있어 기본-path 테스트는 통과한다. |
src/adapters/cache-storage/public-response-cache-adapter.ts |
manifest canonicalization/digest, anonymous fetch, bounded body 검증, candidate marker-last staging, explicit activation, exact lookup/reverify, owned cleanup/inspect | cache ports/result/policy, CacheStorage/fetch/Crypto/Web Lock snapshot. STO-03~05 및 GAP-02. private/auth/opaque/206 거절과 current+previous 보존은 유지. |
1.4 storage root / IndexedDB
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
|---|---|---|
src/adapters/storage/browser-storage-adapter.ts |
registry key별 local/session/memory 저장, TTL, failure overlay/tombstone, quota fallback | storage-keys, StoragePort, codec, diagnostics. strict registry와 stale persistent suppression을 유지. adjacent physical-key migration/sweep는 문서상 미구현. |
src/adapters/storage/browser-storage-codec.ts |
bounded exact JSON envelope, exotic/accessor/unsafe-key/cycle/depth/node 거절 | 독립 codec. prototype pollution/JSON silent coercion 방어가 좋다. |
src/adapters/storage/indexeddb/index.ts |
IndexedDB runtime/maintenance/governance/migration export | native IDB type을 application port 밖으로 내보내지 않는 구조 유지. |
src/adapters/storage/indexeddb/indexeddb-failure.ts |
IDB/DOM failure를 closed browser failure로 변환 | common Result. raw native detail 비노출 유지. |
src/adapters/storage/indexeddb/indexeddb-governance.ts |
opaque dataset scope/physical DB identity 및 frozen policy binding | indexeddb/shared ports. account/business ID를 physical name에 쓰지 않는 양방향 binding 유지. |
src/adapters/storage/indexeddb/indexeddb-maintenance.ts |
post-open codec migration 및 idempotency receipt prune, keyset checkpoint, budget/revision fencing | IndexedDB port/types/failure/governance. async transform outside tx, row+sidecar+budget+checkpoint atomic commit은 좋다. STO-06 및 temporal drain lease 개선 후보. |
src/adapters/storage/indexeddb/indexeddb-migrations.ts |
additive-only contiguous DDL planner/validator | indexeddb types. destructive DDL 거절 유지. |
src/adapters/storage/indexeddb/indexeddb-runtime.ts |
generic repository open/read/query/CAS/delete, idempotency, retention, lifecycle purge, connection lifecycle | indexeddb ports/types/governance/failure/migrations. transaction complete truth, versionchange close, shared open/abort isolation, exact budgets 유지. lifecycle proof는 현재 문서 계약(형식 검증 후 폐기)과 일치하므로 결함으로 분류하지 않았다. |
src/adapters/storage/indexeddb/indexeddb-types.ts |
adapter-local codec/query/schema/dependency contracts | application indexeddb/shared ports. isOldWriterDrainConfirmed() boolean은 provider가 전체 window를 보장한다는 문서 전제; lease형으로 강화 권고. |
1.5 storage/opfs
| 파일 | 책임 | 주요 의존성 / 리뷰 결과 |
|---|---|---|
src/adapters/storage/opfs/browser-opfs-runtime.ts |
OPFS support inspection 및 journal/worker/byte-store composition | OPFS ports, journal, byte-store, policy, worker client. property probe를 real readiness로 주장하지 않음(GAP-03). |
src/adapters/storage/opfs/index.ts |
OPFS runtime/journal/policy/protocol/client exports | optional capability barrel. |
src/adapters/storage/opfs/indexeddb-opfs-journal.ts |
logical object/journal/budget/chunk refcount의 IDB authority; begin/files-ready/commit/rollback/reconcile pages | OPFS ports, IDB failure, policy. journal+object+budget CAS atomicity가 좋다. STO-01 수정에서 incomplete row를 cleanup 확인 전 삭제하지 않아야 한다. |
src/adapters/storage/opfs/opfs-byte-store-adapter.ts |
logical journal과 physical worker를 saga로 조정, put/open/remove, reconcile/policy maintenance | OPFS/shared ports, journal, worker gateway, policy. STO-01의 journal/physical compensation ordering 결함 위치. |
src/adapters/storage/opfs/opfs-policy.ts |
root/lock/chunk/object/RPC/reconcile/GC hard limits 및 scope validation | shared/opfs ports. opaque physical path와 absolute caps 유지. |
src/adapters/storage/opfs/opfs-worker-client.ts |
request correlation/timeout/abort/transferable chunking, worker gateway, streamed reads | protocol/policy/shared Result. STO-01의 untracked abort cleanup 및 STO-07의 shallow response parse. |
src/adapters/storage/opfs/opfs-worker-protocol.ts |
page↔DedicatedWorker request/response union 및 gateway contract | OPFS/shared ports. STO-07; protocol version/kind/effect certainty 추가 필요. |
src/adapters/storage/opfs/opfs-worker-runtime.ts |
DedicatedWorker OPFS physical layout, lock lease, immutable chunk, manifest/staging receipt, abort/finalize/remove/GC | protocol/policy/OPFS ports/Web Lock/Crypto. STO-01의 generation-only cleanup과 lease release 순서. sync handle finally close 등은 유지. |
1.6 직접 연결 경계와 조립
src/application/ports/browser-file-storage/{shared,file,indexeddb-port,opfs-ports,cache-storage-ports,storage-durability-port}.ts와 barrel을 읽었다. nativeFile/Blob/Cache/IDB*/Response/ReadableStream을 application으로 노출하지 않는 포트 방향은 올바르다.src/application/ports/storage-port.ts,src/contracts/storage-keys.ts를 대조했다. Web Storage는 registry-owned typed key만 허용한다.src/bootstrap/runtime-adapters.ts:17,260-266은 Web Storage만 기본 조립한다. file/IndexedDB/OPFS/Cache가 없는 것은 문서의AVAILABLE_NOT_COMPOSED와 일치하며 결함이 아니다.
2. 구체적 findings와 구현 방법
STO-01 — OPFS 보상 cleanup이 journal보다 늦게 완료되거나 실패할 때 후속 generation 삭제 가능
근거와 실패 연쇄
- 새 logical generation은 현재 committed generation+1로 재사용된다:
src/adapters/storage/opfs/opfs-byte-store-adapter.ts:158-195(특히 183-195). preparePut또는markFilesReady실패 시rollbackBestEffort를 호출한다: 같은 파일222-242.rollbackBestEffort는worker.cleanupTransaction(..., callerSignal)의BrowserDataResult를 검사하지 않고, 곧바로journal.rollback을 호출한다:833-845. caller signal이 이미 abort되었으면 cleanup RPC는 시작조차 못 한다.- worker client도 prepare 단계 실패/timeout 때 별도의 un-signaled
ABORT_PUT을 보내지만 timeout/실패를 삼키며 “journal reconciliation이 반복한다”고 가정한다:src/adapters/storage/opfs/opfs-worker-client.ts:190-198,229-307. 그런데 3번이 journal row를 삭제한다. - physical cleanup은 staging receipt에서
(scope, objectId, generation)만 읽어 해당 generation 디렉터리를 삭제한다:src/adapters/storage/opfs/opfs-worker-runtime.ts:584-607,717-761. manifest/path에 transaction-unique physical generation identity가 없다. abortPut은 mutation lease를 먼저 release한 뒤 generation 삭제를 수행한다: 같은 파일501-529(특히 519-527).cleanupTransaction자체도 mutation lease를 얻지 않는다.
따라서 T1 cleanup RPC가 timeout 뒤 worker에서 계속되거나 T1 ABORT_PUT이 늦게 실행되는 동안 coordinator가 T1 journal을 rollback하면 T2가 같은 object의 동일 logical generation을 다시 시작할 수 있다. 늦은 T1 cleanup은 T2의 물리 디렉터리를 삭제할 수 있다. 삭제까지 겹치지 않아도 journal 부재로 stale staging/immutable chunks가 영구 잔존해 quota pressure를 만든다.
패턴과 수정
- cross-API ACID를 주장하지 말고 durable saga + transactional outbox/compensation state를 유지한다.
- “physical cleanup confirmed” 전에는 PREPARING/FILES_READY journal row와 budget reservation을 rollback하지 않는다. cleanup은 caller signal과 분리한 composition-owned bounded signal을 사용한다.
- worker client 내부에서 fire-and-forget abort를 중복 발행하지 않는다. coordinator가
abortPreparedPut()한 번을 소유하고 결과가CLEANED|ALREADY_CLEAN일 때만 journal rollback한다. timeout/crash는EFFECT_UNKNOWN으로 남겨 reconcile한다. - 장기적으로 transaction-unique physical generation/fencing token을 path, receipt, manifest, journal에 저장한다. stale T1 cleanup은 T1 token 경로만 삭제하고 T2를 건드릴 수 없어야 한다.
- cleanup/abort는 같은 origin mutation Web Lock을 physical 삭제 완료까지 보유한다. lease를 먼저 release하지 않는다.
권장 새/변경 signature
declare const opfsPhysicalGenerationBrand: unique symbol;
export type OpfsPhysicalGenerationId = string & {
readonly [opfsPhysicalGenerationBrand]: "OpfsPhysicalGenerationId";
};
export type OpfsPreparedObjectV2 = Readonly<{
physicalSchemaVersion: 2;
physicalGenerationId: OpfsPhysicalGenerationId;
descriptor: DurableObjectDescriptor; // logical generation은 그대로 유지
chunks: readonly OpfsChunkReference[];
}>;
export type OpfsCleanupEffect =
| Readonly<{ kind: "CLEANED" | "ALREADY_CLEAN" }>
| Readonly<{ kind: "EFFECT_UNKNOWN" }>;
export interface OpfsWorkerGateway {
abortPreparedPut(request: Readonly<{
scope: OpfsStorageScope;
transactionId: string;
physicalGenerationId: OpfsPhysicalGenerationId;
signal?: AbortSignal; // coordinator-owned compensation signal만 전달
}>): Promise<BrowserDataResult<OpfsCleanupEffect>>;
}
P0에서는 v1 read를 유지하면서 새 write만 v2/token path로 쓴다. EFFECT_UNKNOWN은 성공 Result로 취급하지 말고 journal 유지 + OBJECT_RECONCILE를 반환한다.
기존 테스트와 false-positive 방지
tests/unit/opfs-byte-store.test.ts:504-540의 “keeps a committed journal row for reconciliation when cleanup fails”는 logical commit 뒤 finalize 실패만 검증한다. PREPARING/FILES_READY 보상 실패를 다루지 않는다.tests/unit/opfs-worker-runtime.test.ts:232-408은 BEGIN cancel/APPEND-vs-ABORT serialization/authority isolation을 검증하지만, journal rollback 뒤 다른 worker/context가 재사용한 generation에 대한 늦은 cleanup을 만들지 않는다.indexeddb-opfs-journal.ts:997-1008의 uniquelogicalKeyindex는 journal row가 남아 있는 동안 T2를 막는다. 바로 그 row를 조기에 삭제하는 것이 문제이므로 이 index가 반증이 아니다.
STO-02 — 검증 URL과 실제 download navigation URL의 base가 다름
근거
safeBrowserManagedTarget은new URL(href, new URL(baseOrigin))으로 protocol/origin/query/hash를 검증한다:src/adapters/browser-files/download-delivery-adapter.ts:1248-1269.- 성공 후 canonical
URL.href가 아니라 원문 문자열을 host로 넘긴다:435-459. - 실제 anchor는
anchor.href = href라서 document의 currentbaseURI를 기준으로 해석한다:47-68.
예: configured baseOrigin=https://app.example, capability href="downloads/report", document에 <base href="https://evil.example/">가 있으면 검증은 app origin을 통과하지만 실제 anchor는 evil origin으로 향한다. capability receipt의 server binding이 있더라도 adapter의 same-origin 정책 주장이 깨진다.
패턴과 수정
- Parse once / canonicalize then execute 패턴을 적용한다. validator가 boolean이 아니라 canonical absolute URL을 반환하고 정확히 그 값을 handoff한다.
- cross-origin을 허용하는 별도 policy에서도 username/password/hash/query 규칙을 적용한 canonical string만 실행한다.
type ResolvedBrowserManagedTarget = Readonly<{ absoluteHref: string }>;
function resolveBrowserManagedTarget(
href: string,
baseOrigin: string,
policy: Readonly<{ allowCrossOrigin: boolean; allowQuery: boolean }>,
): BrowserDataResult<ResolvedBrowserManagedTarget>;
context.options.host.handoff(target.value.absoluteHref, fileName)로 변경한다. 더 엄격한 선택은 capability resolver가 absolute https: URL만 발행하게 하고 상대 URL을 거절하는 것이다.
기존 테스트 대조
tests/unit/browser-file-download.test.ts:222-255는 raw 상대 path가 host에 그대로 전달된다고 고정한다. 이 기대값을 canonicalhttps://app.example/downloads/artifact-1로 바꿔야 한다.257-285는 이미 absolute evil/query URL 거절만 검증해<base>불일치를 잡지 못한다.
STO-03 — Vary 허용/보존 policy가 모순될 수 있음
근거
- policy validation은 vary name이 request allowlist에 포함되는지만 본다:
src/adapters/cache-storage/public-cache-policy.ts:110-141, 특히132-134. response allowlist에vary가 있는지는 확인하지 않는다. - network response의 Vary는 exact request headers와 검증한다:
src/adapters/cache-storage/public-response-cache-adapter.ts:1140,1228-1262. - 이후
unknownResponseHeaderAction="STRIP"이면 response allowlist에 없는vary를 제거하고(1264-1280), 제거된 headers로 Cache에 put한다(472-479). 동일 URL variant가 충돌한다. - activation은 모든 entry를 다시 digest/type/Vary 검증하므로
592-617에서 fail-closed한다. 따라서 현재 증거로 private-data disclosure를 주장하면 과장이다. 실제 영향은 impossible candidate에 대한 stage 성공, variant loss, activation/rollback availability 저하다.
수정
if (
policy.allowedVaryHeaderNames.length > 0 &&
!policy.allowedResponseHeaderNames.includes("vary")
) throw new TypeError("Vary must be preserved when variants are enabled.");
방어를 겹치려면 sanitizedResponseHeaders가 검증된 Vary를 generic strip과 무관하게 반드시 보존하도록 한다. Policy cross-field invariant + fail-fast composition 패턴이다.
tests/unit/public-response-cache.test.ts:1030-1155는 default response allowlist가 이미 vary를 포함(public-cache-policy.ts:54-63)하므로 이 custom-policy 조합을 놓친다.
STO-04 — existing cache marker만 확인하는 stage idempotence
src/adapters/cache-storage/public-response-cache-adapter.ts:424-442는 cache name이 있고 marker의 release ID/digest/count가 맞으면 모든 cached response의 존재/내용을 보지 않고 stage 성공을 반환한다. marker-last는 첫 stage crash에는 강하지만 marker 이후 browser pressure eviction, manual deletion, partial corruption에는 충분하지 않다. activation이 592-617에서 재검증하므로 unsafe publish는 막지만, 같은 manifest로 restage해도 손상 candidate를 복구하지 못한다.
수정: verifyReleaseCandidate(cache, normalized, policy, crypto, signal)를 factor하고 stage fast path와 activation이 공유한다. 기존 candidate가 missing/mismatch면 owned candidate만 삭제하고 network restage한다. verification 중 abort/unknown error면 active pointer는 건드리지 않고 candidate를 유지 또는 정책대로 삭제하되 성공을 반환하지 않는다. 이는 idempotent repair, marker as claim not evidence 패턴이다.
STO-05 — cache mutation availability가 fetcher에 과결합
mutationAvailability는 storage+fetcher+lock 모두를 요구한다: public-response-cache-adapter.ts:1643-1651. stage 호출 392-396에는 맞지만, fetch하지 않는 activate 538-542와 cleanup 695-699에도 같은 guard를 쓴다. 이미 검증된 release를 offline에서 활성화/rollback하거나 quota recovery cleanup하는 기능을 차단한다.
수정: operation별 capability guard로 분리한다.
function stageAvailability(d: Dependencies): BrowserFailureResult | null;
// cacheStorage + mutationLock + fetcher
function localMutationAvailability(
d: Dependencies,
operation: "CACHE_ACTIVATE" | "CACHE_DELETE",
): BrowserFailureResult | null;
// cacheStorage + mutationLock
Dependency segregation을 적용하고 recovery도 ONLINE_ONLY가 아니라 실제 operation에 맞는 RETRY/REHYDRATE로 유지한다.
STO-06 — IndexedDB migration commit 중 duration budget 재확인 없음
- port는 async storage operation 사이 cooperative duration budget을 명시한다:
src/application/ports/browser-file-storage/indexeddb-port.ts:81-89. - docs도 각 native operation 사이 monotonic deadline 확인을 요구한다:
docs/architecture/browser-file-and-origin-storage.md:611-615. - transform phase는 clock을 확인한다:
src/adapters/storage/indexeddb/indexeddb-maintenance.ts:940-966. - 그러나
commitPrepared의 read/write/budget/sidecar/checkpoint chain은969-1233동안 clock을 호출하지 않는다. 최대 500 rows의 IDB callbacks가 invocation deadline 이후에도 계속될 수 있다.
수정: transaction을 시작하기 전 composition-owned minimumCommitReserveMs를 확인하고, prepared row 수를 budget에 맞춰 더 작게 제한한다. transaction을 연 뒤에는 각 record 시작 시 monotonic deadline을 확인하여 아직 어떤 write도 시작하지 않은 다음 record에서 transaction을 정상 종료하고 last-safe checkpoint까지만 commit한다. 이미 시작한 record의 row/sidecar/budget은 원자 완료하거나 tx 전체 abort해야 하며 부분 truth를 반환하면 안 된다. clock failure는 transaction abort + UNAVAILABLE다.
tests/unit/indexeddb-maintenance.test.ts:562-597은 transform 시작 전 budget exhaustion만 검증하므로 commit callback 중 clock advance 케이스를 추가한다.
STO-07 — OPFS worker protocol version/strict response correlation 부재
- request/response envelope에
protocolVersion과 echoedkind가 없다:src/adapters/storage/opfs/opfs-worker-protocol.ts:15-137. - worker는 requestId+known kind만 1차 검사한다:
opfs-worker-runtime.ts:1643-1669. - client는
{requestId:string, ok:boolean}만 검사한다:opfs-worker-client.ts:666-675. 실패 object/failure code/kind를 strict validate하지 않고response.failure.code를 사용(171-184)한다. - VD-15는 real preflight에서 protocol/schema mismatch를
INCOMPATIBLE로 닫으라고 한다:docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md:476-484.
수정: OPFS_WORKER_PROTOCOL_VERSION = 2 as const; 모든 request/response에 version과 kind를 넣고 pending request가 expected kind를 보관한다. closed failure-code set과 per-kind value parser를 적용한다. 먼저 HELLO/CAPABILITIES handshake에서 supported physical schema와 protocol version을 교환하고 mismatch면 write/read를 금지한다. generic cancel은 초기 correctness 필수가 아니다. PUT은 effect certainty가 필요한 명시적 ABORT_PUT; read/verify RPC는 client-side abandon으로 충분하며, 자원 최적화가 필요할 때만 CANCEL_REQUEST { targetRequestId }를 추가한다.
STO-08 — picker function receiver binding은 browser test로 먼저 확정
- open:
src/adapters/browser-files/browser-file-picker.ts:431-436 - save/open-authorized callbacks:
src/adapters/browser-files/download-delivery-adapter.ts:201-235
platform Window.showOpenFilePicker/showSaveFilePicker를 options object에 bind할 이유가 없고 Web IDL receiver brand check 가능성이 있다. 다만 현재 코드가 host facade 콜백을 의도했을 수도 있어 확정 전 browser matrix가 필요하다. 우선 실제 window.showOpenFilePicker를 전달한 capability test를 추가한다. 실패가 재현되면 API를 SystemPickerHost { open; save? }로 만들고 composition에서 올바른 owner에 bind한 host만 주입한다. arbitrary callback(openAuthorizedSource, integrity factory)은 bind하지 않고 함수 snapshot 그대로 호출한다.
3. 명시적 architecture 결정
Transaction / crash recovery
- IndexedDB 한 domain mutation은 한 native transaction으로 row, retention sidecar, budget, idempotency receipt/checkpoint를 commit한다. request success가 아니라 transaction
complete가 성공 truth다. - IDB와 OPFS/Cache 사이에는 atomic transaction이 없다. OPFS는 journal-authoritative durable saga다. phase는 monotonic이고 physical side effect가 불명확하면 incomplete journal을 유지한다.
- compensation은 원 caller abort와 분리된 bounded signal로 실행한다. cleanup success가 확인될 때만 journal/budget rollback; unknown이면 reconcile owner에게 넘긴다.
- committed object를 in-place repair하지 않는다. 새 physical token/generation에 copy/verify 후 logical CAS publish한다.
Migration / rollback
- 독립 version 축(IDB DDL, record codec, OPFS journal, OPFS physical, Cache control/release)을 합치지 않는다.
- 공통 순서는 expand → old-writer drain lease → bounded migrate/copy → atomic publish → N-1 observe/rollback window → 별도 contract release다.
- schema downgrade, whole DB/root/cache delete, read-time unbounded rewrite는 금지한다.
- IDB
isOldWriterDrainConfirmed()는 현재 provider가 전체 migration/contract window를 보장한다는 문서 전제라 현 결함은 아니다. 다음 interface로 temporal guarantee를 실행 가능하게 강화한다:
export interface OldWriterDrainLease {
readonly leaseId: string;
readonly validUntilEpochMs: number;
assertValid(signal?: AbortSignal): Promise<BrowserDataResult<void>>;
release(): Promise<void>;
}
export interface IndexedDbDataMigrationPolicy<WireValue> {
acquireOldWriterDrainLease(input: Readonly<{
migrationId: string;
targetCodecVersion: number;
scope: IndexedDbDatasetScope;
signal?: AbortSignal;
}>): Promise<BrowserDataResult<OldWriterDrainLease>>;
// migrate/measure 기존 계약 유지
}
lease는 batch commit 직전 재검증하고, product rollout owner는 migration 완료 후 rollback/contract window까지 global fence를 유지한다.
Quota / pressure / eviction
- StorageManager estimate는 rough signal일 뿐 free-space reservation이 아니다. 실제
QuotaExceededError가 authority다. - per-dataset hard budget은 그대로 유지하고, origin coordinator는 Web Lock leader 한 개가 hysteresis(
70/85%, 하향65/80%2회)를 적용한다. - GC 순서: incomplete candidate/stale staging → expired reconstructable → grace 지난 unreferenced chunk → inactive public release → confirmed synced copy → 중지. user-authored/unsynced는 자동 삭제 금지.
- 기본 invocation 100 items/5s, 절대 500/30s. cursor는 owner/policy/release epoch에 binding한다.
- quota retry는 실제 quota rollback, 동일 idempotency/revision/digest, external publish 없음, GC가 실제 제거/pressure 하향, 새 admission token 조건을 모두 만족할 때 정확히 1회만 허용한다.
Lease / destructive authority
- OPFS mutation Web Lock은 physical delete/cleanup 완료까지 보유한다. transaction-unique physical token이 stale cleanup fencing이다.
- object URL은 registry lease로만 만들고 persistence/log/analytics/global cache에 넣지 않는다. release/dispose는 idempotent다.
- IndexedDB lifecycle authority는 현재 문서대로 composition callback의 short-lived proof를 형식 검증 후 즉시 폐기한다. OPFS와 동일한 replay 방지가 제품 threat model에 필요하면 provider+atomic consumer의 one-shot lease로 별도 강화하되 application caller에게 token을 노출하지 않는다.
Object URL / preview
- 현재 encoded size/signature/media/active-content denylist는 유지한다.
- 제품 untrusted image preview를 선택하기 전 object URL 발급 앞에 bounded header parser + native decode probe를 둔다. static JPEG/PNG/WebP/AVIF 등 명시 allowlist만; SVG/PDF/HTML/XML과 animated image는 별도 격리/re-encode capability가 없으면 attachment-only다.
export interface PreviewSafetyProbe {
inspect(input: Readonly<{
file: File; // adapter-local only
mediaType: string;
maxEncodedBytes: number;
maxPixels: number;
maxDecodedBytes: number;
maxFrames: number;
deadlineMs: number;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Readonly<{
width: number;
height: number;
frameCount: number;
decodedBytes: number;
}>>>;
}
parser 산술은 overflow-safe여야 하고 native createImageBitmap 결과는 항상 close(). timeout/abort/failure면 createObjectURL을 호출하지 않는다.
Stream / cancellation
- application boundary는
ByteSource.stream(signal): AsyncIterable<BrowserDataResult<Uint8Array>>를 유지한다. 첫 failure에서 producer/reader/writer를 모두 닫고 raw DOMException/EOF 성공으로 바꾸지 않는다. - save stream은 backpressure를 따르고
writer.close()완료 truth가 늦은 abort보다 우선한다. partial destination append/resume로 주장하지 않는다. - Blob/object URL buffer는 hard cap 아래 fallback에서만 허용한다. public cache는 exact length/digest 검증 때문에 bounded buffer를 유지하되 cap을 넘으면 reader cancel.
- pre-start abort는 side effect 0. IDB 중간 abort는 tx abort. irreversible prompt/persist/close가 완료된 뒤에는 platform truth가 이긴다.
- worker mutation timeout은 effect unknown이지 rollback 확인이 아니다. read RPC는 응답을 버릴 수 있지만 mutation은 journal/explicit abort protocol로 종결한다.
Worker protocol
- versioned handshake, request kind echo, requestId+kind correlation, strict discriminated parser, closed error set을 채택한다.
- wrong version/schema는
INCOMPATIBLEhealth로 write/read 금지. 이를 failure surface에 노출할 필요가 있으면BrowserDataFailureCode에INCOMPATIBLE을 추가하고 모든 exhaustive mapper/fixture를 함께 갱신한다. 단순UNAVAILABLEretry loop로 숨기지 않는다. - generic
CANCEL_REQUEST는 read CPU/resource 최적화로 후순위. PUT correctness는 transaction-scopedABORT_PUT과 durable journal이 담당한다.
Cache security / eviction
- anonymous same-origin public GET, credentials omit, exact query/request headers/Vary/type/length/digest만 cache한다. auth/private/no-store/no-cache/opaque/redirect/206/range는 계속 금지한다.
- verified marker는 모든 entries 이후 마지막에 쓰되 marker만 증거로 믿지 않는다. stage reuse와 activation/lookup에서 response를 재검증한다.
- current+verified previous release를 유지하고 rollback도 동일 activation validation을 다시 통과한다.
- partial eviction/miss는
STORAGE_EVICTED또는 integrity failure로 fail-closed하고 network rehydrate한다. owned prefix 밖 cache나 user data는 절대 삭제하지 않는다.
4. 정확한 파일 변경 계획
Phase 0 — 즉시 correctness/security fix
수정
src/application/ports/browser-file-storage/opfs-ports.ts: v1|v2 prepared object read union,OpfsPhysicalGenerationId, journal row physical identity.src/adapters/storage/opfs/opfs-worker-protocol.ts: explicit abort/cleanup effect, protocol v2 envelope/kind correlation.src/adapters/storage/opfs/opfs-worker-client.ts: fire-and-forget duplicate abort 제거, strict response parser, coordinator-owned confirmed abort.src/adapters/storage/opfs/opfs-worker-runtime.ts: tokenized physical path/receipt/manifest, cleanup lock 보유, exact token delete.src/adapters/storage/opfs/opfs-byte-store-adapter.ts: cleanup result 확인 전 journal rollback 금지; independent compensation deadline; unknown effect reconcile.src/adapters/storage/opfs/indexeddb-opfs-journal.ts: v2 prepared/journal validation, incomplete row 유지 및 migration metadata.tests/unit/opfs-byte-store.test.ts,tests/unit/opfs-worker-runtime.test.ts,tests/unit/indexeddb-opfs-journal.test.ts: 아래 race/crash tests.src/adapters/browser-files/download-delivery-adapter.ts: boolean validator를 canonical resolver로 변경; absolute URL 실행.tests/unit/browser-file-download.test.ts: canonical URL 및 hostile base regression.src/adapters/cache-storage/public-cache-policy.ts: Vary preservation cross-field invariant.src/adapters/cache-storage/public-response-cache-adapter.ts: stage candidate full verify/self-repair, availability 분리.tests/unit/public-response-cache.test.ts: custom Vary, damaged candidate, no-fetcher activate/cleanup.
Phase 1 — bounded lifecycle / protocol / preview promotion
생성
src/application/ports/browser-file-storage/origin-storage-lifecycle-port.tssrc/adapters/storage/origin-storage-lifecycle-coordinator.tstests/unit/origin-storage-lifecycle-coordinator.test.tssrc/adapters/browser-files/browser-image-preview-probe.tstests/unit/browser-image-preview-probe.test.tssrc/adapters/storage/opfs/opfs-physical-migration.tstests/unit/opfs-physical-migration.test.tstests/fixtures/origin-storage/opfs-v1-populated.tstests/fixtures/origin-storage/cache-v1-populated.ts
수정
src/application/ports/browser-file-storage/index.ts: 새 lifecycle port export.src/application/ports/browser-file-storage/file.ts: preview safety policy/result를 native-free 형태로 추가하거나 probe를 adapter-internal dependency로 유지.src/application/ports/browser-file-storage/cache-storage-ports.ts: bounded maintenance page/cursor input.src/application/ports/browser-file-storage/indexeddb-port.ts,src/adapters/storage/indexeddb/indexeddb-types.ts: drain lease contract.src/adapters/storage/indexeddb/indexeddb-maintenance.ts: commit reserve/deadline checks 및 lease revalidation.src/adapters/browser-files/object-url-lease.ts,src/adapters/browser-files/create-browser-file-runtime.ts: probe success 전 URL 생성 금지.src/adapters/cache-storage/public-response-cache-adapter.ts: cursor/deadline bounded inspect/cleanup.src/adapters/storage/opfs/browser-opfs-runtime.ts: real worker/lock/journal/write-read-delete-cleanup preflight 조립 hook.tests/browser-capabilities/{browser-files,opfs-runtime,public-cache-storage,indexeddb-runtime}.spec.ts와opfs-test.worker.ts: real engine evidence.docs/architecture/browser-file-and-origin-storage.md,docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md,docs/operations/browser-file-storage-recovery.md,docs/operations/client-cache-and-storage-recovery.md: 상태를 구현 후에만AVAILABLE_NOT_COMPOSED로 승격.
삭제/이동: 없음. v1 reader/fixtures와 old cache prefix는 rollback window 종료 전 삭제하지 않는다. barrel 재배치도 불필요하다.
Cache bounded port signature
declare const publicCacheCursorBrand: unique symbol;
export type PublicCacheMaintenanceCursor = string & {
readonly [publicCacheCursorBrand]: "PublicCacheMaintenanceCursor";
};
export type PublicCacheMaintenanceInput = Readonly<{
maxCaches?: number; // default 100, absolute 500
maxDurationMs?: number; // default 5_000, absolute 30_000
cursor?: PublicCacheMaintenanceCursor;
signal?: AbortSignal;
}>;
export type PublicCacheMaintenancePage = Readonly<{
inspectedCaches: number;
deletedCaches: number;
retainedCaches: number;
unreadableCaches: number;
nextCursor: PublicCacheMaintenanceCursor | null;
moreAvailable: boolean;
deadlineReached: boolean;
}>;
cleanupOwned(input?: PublicCacheMaintenanceInput):
Promise<BrowserDataResult<PublicCacheMaintenancePage>>;
inspectOwned(input?: PublicCacheMaintenanceInput):
Promise<BrowserDataResult<PublicCacheMaintenancePage>>;
cursor는 caller-readable cache name이 아니며 owned prefix, active pointer epoch, policy fingerprint에 서명/opaque binding한다. stale cursor는 STALE_RESULT.
5. TDD 계획: 이름, 입력, 기대 결과
| 테스트 이름 | 핵심 입력/fixture | 기대 결과 |
|---|---|---|
keeps PREPARING journal when compensating cleanup is aborted or unavailable |
preparePut failure; caller signal aborted; worker cleanup ABORTED/UNAVAILABLE |
journal.rollback 미호출, reservation/journal 유지, OBJECT_RECONCILE recovery; 후속 same object begin conflict |
delayed stale cleanup cannot delete a reused logical generation |
T1 generation 1 abort RPC 지연; T2 generation 1 v2 token으로 commit; T1 cleanup resume | T1 token path만 제거; T2 verify/open bytes 성공; T2 manifest/chunks 유지 |
holds the OPFS mutation lease until exact physical cleanup completes |
cleanup delete promise를 gate하고 concurrent begin 시도 | delete 완료 전 T2 lease 미획득; release 후 진행 |
does not roll back journal after an unknown worker mutation effect |
cleanup RPC timeout 후 worker operation pending | incomplete journal 유지; reconcile가 exact transaction을 종결 |
rejects mismatched OPFS worker protocol and response kind |
v1 response 또는 requestId는 같지만 wrong kind/malformed failure | INCOMPATIBLE/closed failure; pending request success로 resolve하지 않음; write side effect 0 |
hands off the canonical URL validated against baseOrigin |
href="downloads/a", baseOrigin app, document base evil |
host receives https://app.example/downloads/a; evil URL never assigned |
rejects a policy that enables variants but strips Vary |
allowed vary accept-language, allowed response headers without vary, STRIP |
composition TypeError, Cache/fetch side effect 0 |
preserves Vary for every stored custom variant |
en/ko same URL with exact request header | stage+activate+both exact match succeed; stored response has Vary |
restages an evicted entry even when the release marker remains |
successful stage 후 one asset delete, same manifest stage again | missing asset re-fetch; all entries reverify; success only after repair |
activates and cleans a prestaged cache without a fetcher |
seeded valid cache/pointer, cacheStorage+lock, no fetcher | activate/cleanup success; no network call |
stops codec migration commit at the cooperative deadline |
fake clock advances during IDB record callbacks, prepared N rows | only last atomically safe prefix+checkpoint commit; MORE, budgetExhausted; no orphan sidecar/budget delta |
requires an old-writer drain lease to remain valid before batch commit |
lease valid at acquire, expires before commit | tx write 0/abort; BLOCKED; checkpoint unchanged |
rejects oversized raster dimensions before object URL creation |
small encoded PNG with huge width/height or overflow dimensions | LIMIT_EXCEEDED/POLICY_REJECTED; createObjectURL 0 calls |
closes a decoded bitmap on preview abort and failure |
probe aborts after native decode begins | bitmap close once, URL 0, closed ABORTED |
rejects animated and truncated preview containers |
animated WebP/GIF, truncated PNG/JPEG | fail before URL, no leaked decoder resource |
pages cache cleanup by count deadline and opaque cursor |
700 owned caches + foreign caches; max 100/5s | <=100 inspected, foreign untouched, moreAvailable, bound cursor; repeated pages converge |
rejects cache maintenance cursor after active pointer epoch changes |
page1 cursor 후 activation | STALE_RESULT, delete 0 |
retries quota failure exactly once only after productive GC |
reconstructable write quota fail, GC deleted >0, same idempotency/digest | attempt 2 최대 한 번; second fail no third; user-authored untouched |
uses the real Window receiver for enhanced system pickers |
actual browser window.showOpenFilePicker/showSaveFilePicker facade (feature-gated) |
supported engine에서 illegal invocation 없음; dismissal closed outcome |
실행 명령
# 가장 빠른 red/green loop
corepack pnpm exec vitest run \
tests/unit/opfs-byte-store.test.ts \
tests/unit/opfs-worker-runtime.test.ts \
tests/unit/indexeddb-opfs-journal.test.ts \
tests/unit/browser-file-download.test.ts \
tests/unit/public-response-cache.test.ts \
tests/unit/indexeddb-maintenance.test.ts \
tests/unit/browser-image-preview-probe.test.ts \
tests/unit/origin-storage-lifecycle-coordinator.test.ts
# 정적 경계
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm check:browser-file-storage-boundaries
corepack pnpm lint
# 실제 browser/storage semantics
corepack pnpm exec playwright test --config playwright.capabilities.config.ts \
tests/browser-capabilities/browser-files.spec.ts \
tests/browser-capabilities/indexeddb-runtime.spec.ts \
tests/browser-capabilities/opfs-runtime.spec.ts \
tests/browser-capabilities/public-cache-storage.spec.ts \
tests/browser-capabilities/storage-manager.spec.ts
# 전체 회귀
corepack pnpm test:unit
corepack pnpm test:browser-file-storage-removal
corepack pnpm verify:documentation
6. 데이터 호환성, migration, deployment, rollback 순서
- 즉시 containment: 제품에 OPFS v1 write가 조립돼 있다면 kill switch로 신규 write를 read-only/export-required로 전환한다. read/export와 journal reconcile는 유지한다. file/IDB/OPFS/cache가 template bootstrap 기본 조립이 아니라는 사실은 영향 범위를 줄이지만 product-specific composition을 확인해야 한다.
- N expand release: journal DDL을 additive upgrade하고 v1+v2
OpfsPreparedObjectreader를 배포한다. worker protocol v2 handshake를 먼저 넣되 v1 data read는 지원한다. v2 physical path는 unique token을 포함하고 새 write만 v2로 쓴다. - old writer drain: 모든 N-1 page/worker가 write를 중단했다는 release/lease evidence를 확인한다. BroadcastChannel hint만으로 판단하지 않는다. v2 write traffic은 SHADOW/canary부터 연다.
- resume/reconcile: PREPARING/FILES_READY v1 journal을 bounded하게 처리한다. cleanup effect가 불명확하면 row를 삭제하지 않는다. logical committed v1은 authority이며 in-place 수정하지 않는다.
- copy-on-write migration: v1 committed object → v2 staging/token path → bounded chunk read/copy → manifest/tree digest verify → journal generation/fencing CAS publish. publish 전 crash는 v1, publish 후 crash는 v2가 authority다.
- Cache migration: old active verified release를 byte rewrite하지 말고 새 prefix/control schema에 network restage → full verify → explicit activation. current+previous와 old prefix를 rollback/grace window 동안 유지한다.
- Web Storage: current keys는 registry
DISCARDsemantics를 유지한다. adjacent migration이 제품에 필요할 때만 exact owned old physical key를 read-once/validate/write-current/delete-old한다. 전체 localStorage sweep 금지. - Canary observation: multi-tab/worker timeout, crash between every phase, partial eviction, quota fault, N-1 read-only/online-only fixture를 통과한다. user-authored bytes export/sync path도 확인한다.
- Rollback: traffic admission과 새 writer부터 끈다. schema/database version을 내리지 않는다. compatible N reader 또는 N-1 online-only/read-only bundle로 전환하고, OPFS는 publish authority에 따라 v1/v2 source를 선택한다. Cache는 검증된 previous release로 같은 activate protocol을 실행한다.
- Contract release: 모든 active/rollback clients drain, grace/authority evidence, historical fixtures 후에만 v1 physical generation/old cache prefix를 bounded cursor cleanup한다. DB/root/cache blanket delete는 하지 않는다.
7. 유지해야 할 좋은 설계
- closed
BrowserDataResult, safe recovery vocabulary, observer exception 격리 및 PII/path/name 비노출. - File policy가 composition-owned immutable identity이고 selection/inspection/preview/download receipt가 exact file/profile에 binding되는 구조.
- native input baseline과 optional enhanced picker 분리, user activation 전에 await하지 않는 규칙, dismissal과 failure 구분.
- 중앙 object URL lease cap, idempotent revoke/dispose, typed Blob, active-content denylist.
ByteSourcechunk별 Result/cancellation, download backpressure, close 완료 truth, bounded object URL fallback.- Web Storage의 typed registry, physical key versioning, strict exact JSON codec, TTL, quota memory overlay와 tombstone.
- IndexedDB의 opaque physical identity/governance binding, additive-only planner, transaction-complete semantics, CAS/idempotency/retention/budget atomicity, versionchange late-close.
- OPFS의 IDB logical authority, phase journal, immutable digest chunks/refcount, hard budget reservation, fail-closed staging GC, no user-readable physical paths.
- Cache의 anonymous public-only same-origin policy, exact query/header/Vary/type/length/digest, marker-last candidate, explicit activation, current+previous retention, owned-prefix-only cleanup, read/activate 재검증.
- optional adapters를 bootstrap에서 자동 조립하지 않고
AVAILABLE_NOT_COMPOSED로 남긴 현재 composition posture.
8. 기존 테스트·문서 대조와 false-positive 경계
실행한 기존 검증
다음 명령을 이 리뷰 중 실행했고 7 files / 105 tests 전부 통과했다.
corepack pnpm exec vitest run \
tests/unit/opfs-byte-store.test.ts \
tests/unit/opfs-worker-runtime.test.ts \
tests/unit/public-response-cache.test.ts \
tests/unit/browser-file-download.test.ts \
tests/unit/indexeddb-maintenance.test.ts \
tests/unit/indexeddb-runtime.test.ts \
tests/unit/storage-registry.test.ts --reporter=default
이는 finding이 현재 green suite가 보호하지 않는 interleaving/custom-policy/browser-base case임을 뜻하며, 기존 behavior가 전반적으로 깨졌다는 뜻은 아니다.
반증/과장 방지 표
| 의심 항목 | 기존 증거 | 최종 판단 |
|---|---|---|
| OPFS commit 뒤 finalize cleanup 실패 | opfs-byte-store.test.ts:504-540가 COMMITTED row 보존 검증 |
보호됨. STO-01은 commit 전 cleanup 실패/늦은 RPC + generation reuse로 좁힘. |
| OPFS concurrent operations | opfs-worker-runtime.test.ts:232-408가 lock wait cancel, APPEND/ABORT, authority isolation 검증 |
같은 worker의 active put 일부는 보호됨. journal 조기 rollback 후 cross-context late cleanup은 미검증. |
| Cache Vary가 곧 private leak | activation/lookup이 response를 재검증(public-response-cache-adapter.ts:592-617,341-365) |
직접 disclosure 주장은 철회. stage success/variant loss/activation availability 결함으로 Medium. |
| Cache 기본 policy Vary | default response allowlist에 vary 포함(public-cache-policy.ts:54-63), unit 1030-1155 green |
기본은 보호됨. custom policy cross-field invariant만 결함. |
| damaged cache가 active로 publish | activation full reverify | publish는 fail-closed. STO-04는 idempotent stage/self-repair contract. |
| Web Storage schema mismatch | storage-registry.test.ts:231-244가 current physical key의 old envelope discard 검증 |
보호됨. old physical key sweep/adjacent migration은 문서상 미구현이며 현재 작은 preference의 readiness gap. |
| IndexedDB transaction success/abort | indexeddb-runtime.test.ts:319-380가 commit failure rollback과 abort 검증 |
보호됨. STO-06은 migration commit-loop duration budget에 한정. |
| IndexedDB old-writer drain이 전혀 없음 | maintenance test 269-301, docs 604-609가 provider confirmation을 전제 |
현 계약상 provider 책임이므로 결함으로 세지 않음. temporal lease는 enforceability 강화. |
| preview decode safety가 몰래 누락 | browser-file-and-origin-storage.md:360-365, VD-15 19-31,574+, runbook 96-108가 미구현을 명시 |
regression 아님. 제품 preview promotion blocker(GAP-01). |
| Cache unbounded cleanup이 발견되지 않은 bug | VD-15 486-515, runbook 382-429가 정확히 명시 |
known DESIGNED_NOT_IMPLEMENTED readiness gap(GAP-02). |
| origin pressure/migration coordinator 부재 | VD-15 19-31,90-103, browser-file-storage-recovery.md:10-14 |
known gap. 기존 per-store maintenance를 coordinator로 오인하지 않는다. |
| optional adapters가 bootstrap에 없음 | runtime-adapters.ts:260-266; docs status AVAILABLE_NOT_COMPOSED |
의도된 skeleton posture, 결함 아님. |
| picker receiver | unit tests가 모두 arrow/fake callback을 사용 | 확정 증거 부족. STO-08은 browser test 선행의 낮은 심각도 hypothesis로 격리. |
9. 리뷰 범위 밖으로 확장하지 않은 항목
- Service Worker lifecycle, private/range cache, persistent directory/file handles, Range resumable download는 문서상 별도
NOT_SELECTED/DESIGNED_NOT_IMPLEMENTEDcapability다. public cache/file adapter에 섞어 고치지 않는다. - application/product dataset, schema, rollout authority가 없으므로 optional IndexedDB/OPFS/Cache를 현재 default bootstrap에 새로 조립하지 않는다.
- 전체 origin eviction은 모든 IndexedDB/OPFS/Cache metadata가 함께 사라질 수 있어 client-only로 완전 판별할 수 없다. server rehydrate/export UX와 generation/session authority가 필요하다.
최종 권고: STO-01은 production composition이 하나라도 있으면 release blocker로 취급한다. STO-02는 작은 canonicalization patch로 즉시 닫을 수 있다. Cache 세 항목은 동일 변경 묶음으로 TDD하고, VD-15 gap들은 상태 문서를 먼저 바꾸지 말고 executable unit+browser evidence와 rollback fixture가 생긴 후에만 승격한다.