1178 lines
61 KiB
Markdown
1178 lines
61 KiB
Markdown
# Browser file and origin-storage platform
|
||
|
||
이 문서는 File, Blob, 파일 선택기, 다운로드, IndexedDB, OPFS, Cache Storage를
|
||
프로덕션에 도입할 때의 경계, 프로토콜, 실패·복구 정책과 promotion evidence를
|
||
정의한다. 최초 결정 기준일은 2026-07-27이며 lifecycle/migration 설계는
|
||
2026-07-28에 갱신했다.
|
||
|
||
현재 skeleton에는 요청된 browser API를 직접 호출하는 **정책 주입형 reference
|
||
runtime**이 `AVAILABLE_NOT_COMPOSED` 상태로 들어 있다. catalog exposure의
|
||
recipe availability는 `RECIPE_AVAILABLE`이지만 이는 primary status나 product
|
||
selection 값이 아니다. 제품 capability는 아직 선택·조합하지 않았고
|
||
bootstrap/installed feature에는 연결하지 않았다.
|
||
따라서 production build에는 포함되지 않고, 실제 제품 요구·dataset owner와
|
||
해당 capability가 server 경계를 넘는 경우 backend protocol까지 정한 프로젝트가
|
||
필요한 adapter만 조립한다. Service Worker 등록,
|
||
제품 데이터 schema와 owner-specific migration policy는 아직 조합하지 않았다.
|
||
이 상태는 catalog의 `productionComposition: false`만 믿지 않는다. build가
|
||
생성하는 `artifacts/quality/vite-module-inventory.json`과
|
||
`check:optional-recipes`가 아래 runtime source root가 production chunk에 없음을
|
||
검증한다. IndexedDB reference runtime 자체에는 additive DDL planner,
|
||
codecVersion 기반 resumable maintenance, dataset budget, retention lifecycle과
|
||
receipt retention/prune가 구현되어 있으며 bootstrap에서만 분리되어 있다.
|
||
|
||
실행 가능한 계약·runtime·evidence는 다음 위치에 있다.
|
||
|
||
- `src/application/ports/browser-file-storage/`
|
||
- `src/adapters/browser-files/`
|
||
- `src/adapters/storage/indexeddb/`
|
||
- `src/adapters/storage/opfs/`
|
||
- `src/adapters/cache-storage/`
|
||
- `src/adapters/browser-file-storage/storage-manager-adapter.ts`
|
||
- `tests/browser-capabilities/`
|
||
- `recipes/frontend-capabilities/browser-file-storage-contracts.ts`
|
||
- `recipes/frontend-capabilities/browser-file-storage-fakes.ts`
|
||
- `tests/recipes/browser-file-storage-contracts.test.ts`
|
||
- `config/recipes/frontend-capability-recipes.json`
|
||
- `artifacts/quality/vite-module-inventory.json` (build-generated evidence)
|
||
- `scripts/test-browser-file-storage-runtime-removal.ts`
|
||
|
||
## 0. 후속 lifecycle/migration 결정과 현재 delta
|
||
|
||
origin storage를 제품에 composition하기 전 남은 공통 lifecycle 설계는
|
||
[VD-15: Origin storage lifecycle, migration, and optional file capabilities](./decisions/VD-15-origin-storage-lifecycle-and-migration.md)에
|
||
고정한다. VD-15가 Accepted됐다는 사실은 해당 runtime이 구현 또는 조립됐다는
|
||
뜻이 아니다.
|
||
|
||
현재와 목표를 구분하면 다음과 같다.
|
||
|
||
| 항목 | 현재 reference runtime | VD-15 목표 |
|
||
| --- | --- | --- |
|
||
| StorageManager | `estimate/persisted/persist`와 pressure 분류 구현 | IDB·OPFS·Cache write admission, hysteresis, bounded GC와 quota failure 뒤 exact 1회 retry coordinator |
|
||
| eviction | OPFS logical/physical 및 Cache pointer/candidate의 partial mismatch 감지 | dataset sentinel/backend epoch 연계; origin 전체 marker 소실은 first install과 완전 구분할 수 없음을 유지 |
|
||
| IndexedDB migration | additive DDL, codecVersion, bounded resumable maintenance 구현 | origin coordinator와 N-1 promotion/rollback evidence 연계 |
|
||
| OPFS migration | journal/physical manifest v1, reconciliation 구현 | copy-on-write physical/journal forward migration과 historical fixture |
|
||
| OPFS readiness | API property와 required primitive 확인, 실제 operation은 native browser test에서 검증 | composition readiness에서 worker/lock/journal/small write-read-delete-cleanup real preflight |
|
||
| Cache Storage | public static release stage/verify/activate, previous retain, owned cleanup 구현 | cursor/count/deadline이 있는 bounded inspect/cleanup과 control-schema migration |
|
||
| Service Worker | 미조립·미구현 | 제품이 PWA를 선택할 때만 별도 update/client-drain controller |
|
||
| local preview | byte/signature/media/active-content/object URL lease 구현 | object URL 발급 전 pixel/decoded-byte/animation/real-decode safety probe |
|
||
| directory/persistent handle/drop | 미구현, transient file picker만 제공 | 제품 workspace/consent 요구가 있을 때 별도 optional capability |
|
||
| Range resumable download | 현재 download/public cache에 없음 | `DESIGNED_NOT_IMPLEMENTED`; VD-14의 별도 runtime/provider 계약 |
|
||
| private/sparse Range cache | public cache에서 명시적으로 거부 | 제품이 별도 선택하기 전 `NOT_SELECTED` |
|
||
| app-managed background download | 현재 미선택 | `NOT_SELECTED`; 지원 browser용 별도 optional capability |
|
||
| cross-browser app-managed background download guarantee | 대상 browser 전체에서 지속 실행을 보장할 수 없음 | `PLATFORM_LIMITED`; browser-managed handoff와 명시적 progressive enhancement |
|
||
|
||
따라서 이 문서 아래에서 “해야 한다”로 표현된 항목 중 위 표의 VD-15 목표는
|
||
아직 executable implementation이 아니다. primary current-status literal은
|
||
`COMPOSED`, `AVAILABLE_NOT_COMPOSED`, `DESIGNED_NOT_IMPLEMENTED`, `NOT_SELECTED`,
|
||
`PLATFORM_LIMITED` 다섯 값만 사용하며 선형 maturity로 해석하지 않는다. 현재 기존
|
||
native runtime은 `AVAILABLE_NOT_COMPOSED`, 새 coordinator/migrator/probe와 Range
|
||
resumable download는 `DESIGNED_NOT_IMPLEMENTED`,
|
||
directory/persistent handle/Service Worker/private·sparse Range cache와
|
||
app-managed background download capability는 `NOT_SELECTED`, 그 cross-browser
|
||
guarantee는 `PLATFORM_LIMITED`다. selection, traffic admission, runtime health와
|
||
promotion evidence는 이 primary status와 별도 축이다.
|
||
|
||
축의 canonical 이름과 literal은 completion ledger의 `Selection`,
|
||
`TrafficAdmission`, `RuntimeHealth`, `PromotionEvidence`를 그대로 사용한다.
|
||
contract/provider/browser/operations는 별도 축 이름을 만들지 않고
|
||
`PromotionEvidence`를 계산하는 component gate다. projection과 evidence expiry도
|
||
ledger 규칙을 따른다.
|
||
|
||
## 1. 변경할 수 없는 경계 결정
|
||
|
||
### 1.1 기술별 소유권
|
||
|
||
| 기술 | 소유하는 것 | 소유하지 않는 것 |
|
||
| --- | --- | --- |
|
||
| `File`/`Blob` | 현재 사용자가 선택했거나 현재 작업이 생성한 transient bytes | 영구 ID, 권한, 신뢰 가능한 MIME, persistence |
|
||
| native file input/picker | user activation 안의 선택 의도와 transient handle | application/domain model, boot permission |
|
||
| download/save | 검증된 artifact를 브라우저 또는 사용자가 고른 파일에 전달 | server authorization, 파일 처리 완료를 가장한 anchor click |
|
||
| IndexedDB | 구조화 record, index, revision, idempotency, OPFS journal; 명시적으로 선택된 feature command queue의 storage mechanism이 될 수 있음 | command/sync protocol authority, 큰 immutable payload, HTTP response cache, credential |
|
||
| OPFS | 큰 opaque immutable bytes, chunk, integrity manifest, staging | query/index, domain metadata authority, 사용자에게 보이는 파일 경로 |
|
||
| Cache Storage | 정책이 승인한 public HTTP `Request`/`Response` representation | domain repository, TanStack Query cache, 인증/개인 API response |
|
||
| HTTP cache | 서버 header가 소유하는 일반 freshness/revalidation | application offline database |
|
||
| 기존 `StoragePort` | 작은 동기식 public preference | IndexedDB, OPFS, Cache Storage |
|
||
|
||
이 표가 가장 중요한 설계 규칙이다. `StorageAdapter`, `FileManager`,
|
||
`BrowserPersistence` 같은 범용 mega-port 하나로 합치지 않는다. 각 API는
|
||
transaction, 수명, quota, 일관성, 복구 방식이 다르다.
|
||
|
||
### 1.2 source of truth
|
||
|
||
- 서버는 명시적으로 승인된 offline-first 동기화 protocol을 제외하면 업무
|
||
entity와 authorization의 source of truth다.
|
||
- IndexedDB record는 server state 전체 복제가 아니라 offline projection,
|
||
unsynced command, local-only product state 중 승인된 하나다.
|
||
- OPFS의 physical manifest는 integrity/recovery 자료다. 논리적 object의 commit
|
||
authority는 IndexedDB journal과 logical row다.
|
||
- Cache Storage hit는 authorization proof가 아니다. 저장 대상 자체를 anonymous
|
||
public representation으로 제한한다.
|
||
- `File.name`, `File.type`, extension, `lastModified`와 picker permission은
|
||
authorization 또는 content safety의 근거가 아니다.
|
||
|
||
### 1.3 clean architecture 배치
|
||
|
||
실제 capability를 선택한 프로젝트의 권장 배치는 다음과 같다.
|
||
|
||
```text
|
||
src/features/<feature>/
|
||
application/
|
||
<aggregate>-offline-repository.ts
|
||
file-intake.ts
|
||
file-delivery.ts
|
||
adapters/
|
||
indexeddb/
|
||
codec.ts
|
||
schema.ts
|
||
migrations.ts
|
||
repository.ts
|
||
file/
|
||
browser-file-vault.ts
|
||
download-delivery-adapter.ts
|
||
upload-example/
|
||
quarantined-upload-http-adapter.ts
|
||
|
||
src/adapters/storage/opfs/
|
||
opfs-object-store.ts
|
||
opfs.worker.ts
|
||
worker-protocol.ts
|
||
journal-repository.ts
|
||
|
||
src/platform/offline/
|
||
cache-policy.ts
|
||
cache-storage-adapter.ts
|
||
service-worker-runtime.ts
|
||
```
|
||
|
||
공통 reference runtime은 domain-neutral한 low-level policy port로 제공한다.
|
||
제품 feature는 이를 직접 UI에 노출하지 않고, feature-specific repository/use
|
||
case가 schema, codec, query plan, authority와 retention policy를 주입해 더 좁은
|
||
domain contract로 감싼다. application/domain에는 `IDBDatabase`, transaction
|
||
callback, store/index 이름, `File`, `Blob`, `FileSystemHandle`, `Request`,
|
||
`Response`, `Cache`가 보이면 안 된다.
|
||
|
||
### 1.4 정책과 도메인의 분리
|
||
|
||
브라우저 API의 안전한 실행 규칙은 도메인과 무관한 공통 정책이다. 예를 들면
|
||
transaction-complete 이후 성공 처리, picker user activation, stream chunk 상한,
|
||
object URL lease, blocked/versionchange 처리, OPFS staging/journal 순서, Cache
|
||
Storage의 exact match와 fail-closed 검증이 여기에 속한다.
|
||
|
||
반대로 모든 값을 하나의 전역 정책으로 고정할 수는 없다. 어떤 record를 저장할지,
|
||
어떤 index/query가 필요한지, server와 local 중 누가 authority인지, data
|
||
classification·retention·quota 우선순위, 허용 MIME과 공개 cache 대상은
|
||
dataset/use-case owner가 결정한다. 코드에서는 전자는 공통 adapter에, 후자는
|
||
주입되는 `*Policy`, codec, query planner와 feature facade에 둔다.
|
||
|
||
따라서 reference runtime이 제공하는 것은 transaction 완료 판정, bounded stream,
|
||
scope/policy binding, integrity와 recovery 같은 **공통 메커니즘**이다. 제품
|
||
composition root는 dataset별 owner, opaque scope, `BrowserStoragePolicy`,
|
||
schema/codec/query planner, lifecycle authority와 backend protocol을 명시적으로
|
||
주입한다. runtime은 이 구성 객체를 검증한 뒤 깊은 snapshot/freeze하고 persisted
|
||
binding과 대조한다. 서로 다른 dataset을 하나의 편의상 전역 repository나
|
||
가변 policy 객체로 합치지 않는다.
|
||
|
||
## 2. 공통 데이터 정책
|
||
|
||
browser persistence를 선택하기 전에 dataset별로 아래 catalog를 채운다. 한
|
||
항목이라도 비어 있으면 runtime을 설치하지 않는다.
|
||
|
||
| 필드 | 필수 결정 |
|
||
| --- | --- |
|
||
| owner | product owner와 operational owner |
|
||
| namespace | 고정된 application/feature/dataset ID |
|
||
| classification | public, internal, personal, confidential; credential은 금지 |
|
||
| authority | server, local-first, reconstructable 중 하나 |
|
||
| retention | session, TTL, until-synced, explicit delete |
|
||
| soft/hard budget | origin 전체와 dataset별 bytes/entries/object limit |
|
||
| persistence | best-effort 허용 여부와 `persist()` 요청 조건 |
|
||
| eviction priority | reconstructable → synced copy → user-authored |
|
||
| account scope | opaque partition key, logout/account deletion 처리 |
|
||
| failure UX | online-only, read-only, export-required 중 하나 |
|
||
| recovery | re-fetch, replay, quarantine, operator/user action |
|
||
| observability | allowlisted event와 bucket attribute |
|
||
| rollback | N-1 reader, destructive migration 보류 기간 |
|
||
|
||
credential, session token, password, signing key, raw authorization header는
|
||
IndexedDB, OPFS, Cache Storage 모두에서 금지한다. 동일 origin JavaScript가
|
||
암·복호화 key를 사용할 수 있는 client-side encryption은 XSS에 대한 권한
|
||
경계가 아니다.
|
||
|
||
`navigator.storage.estimate()`는 IndexedDB, OPFS, Cache Storage 등이 공유하는
|
||
origin storage의 거친 추정치다. free-space 예약이나 기술별 정확한 사용량으로
|
||
사용하지 않는다. 기본 pressure policy의 출발점은 다음과 같고 실제 field
|
||
evidence로 조정한다.
|
||
|
||
| 추정 사용률 | 상태 | 동작 |
|
||
| --- | --- | --- |
|
||
| `< 70%` | normal | 승인된 write 계속 |
|
||
| `70–85%` | pressure | expired/reconstructable data bounded GC |
|
||
| `>= 85%` | critical | nonessential cache write 중지, sync/export 유도 |
|
||
| `QuotaExceededError` | authoritative failure | transaction rollback, GC, idempotent operation만 최대 1회 재시도 |
|
||
|
||
추정치는 hysteresis trigger일 뿐이다. user-authored committed data를 quota 대응
|
||
명목으로 자동 삭제하거나 DB 전체를 자동 `deleteDatabase()`하지 않는다.
|
||
|
||
현재 `storage-manager-adapter.ts`는 이 표의 한 시점 pressure 분류와 명시적
|
||
`persist()` 요청만 제공한다. threshold 하향 hysteresis, 기술 간 write admission,
|
||
GC ordering과 `QuotaExceededError` 뒤 idempotent operation의 최대 1회 retry는
|
||
아직 구현되지 않았고 VD-15 coordinator의 목표다. estimate 결과만으로 native
|
||
free space를 예약했다고 간주해서는 안 된다.
|
||
|
||
## 3. File, Blob, 파일 선택기
|
||
|
||
### 3.1 native type의 수명
|
||
|
||
`File`은 `Blob`에 이름과 수정 시각 metadata가 추가된 browser object다.
|
||
application에는 native object 대신 opaque `LocalFileRef`와 정규화된 metadata를
|
||
전달한다. adapter의 transient vault가 ref에서 native object를 찾고 bounded
|
||
inspection/range read를 수행한다.
|
||
|
||
```text
|
||
click/user activation
|
||
-> native input 또는 enhanced picker
|
||
-> transient browser file vault
|
||
-> opaque LocalFileRef + untrusted metadata
|
||
-> bounded inspection
|
||
-> preview 또는 download 또는 별도 선택한 backend workflow
|
||
-> ref release
|
||
```
|
||
|
||
`File`, `FileList`, `Blob`, `FileSystemFileHandle`, 로컬 경로와 object URL은
|
||
domain, application state, query cache, global store, diagnostics에 넣지 않는다.
|
||
같은 파일을 다시 선택할 수 있도록 input value reset 책임도 picker adapter가
|
||
소유한다.
|
||
|
||
### 3.2 picker baseline과 enhancement
|
||
|
||
- 접근 가능한 `<input type="file">`가 모든 browser의 canonical baseline이다.
|
||
- `showOpenFilePicker()`와 `showSaveFilePicker()`는 runtime method별 feature
|
||
detection을 거친 progressive enhancement다. UA sniffing을 사용하지 않는다.
|
||
- picker 호출은 click/keyboard handler의 첫 browser action이어야 한다. 그 전에
|
||
config fetch, analytics flush, permission query를 `await`하면 transient user
|
||
activation을 잃을 수 있다.
|
||
- boot, route mount 또는 background effect에서 permission을 요청하지 않는다.
|
||
- 선택/저장 대화상자 취소는 정상적인 `DISMISSED` outcome이다. error toast,
|
||
retry alert, error telemetry로 기록하지 않는다.
|
||
- native input의 `cancel` event는 즉시 authoritative dismissal로 처리한다.
|
||
`window.focus` 기반 fallback만 사용하는 engine은 focus 복귀 뒤 기본 1초 동안
|
||
늦은 `change`/`FileList` 반영을 기다린 다음 dismissal로 확정한다.
|
||
- permission denied, unsupported, user dismissal, active transfer abort를 서로 다른
|
||
outcome으로 다룬다.
|
||
- enhanced picker가 실패한 같은 click에서 native picker를 자동으로 다시 열면
|
||
activation이 소진될 수 있다. fallback 버튼을 보여 다음 명시적 사용자 동작에서
|
||
baseline을 실행한다.
|
||
- directory handle이나 persistent file handle은 별도 제품 capability와 별도
|
||
consent/retention decision 없이는 저장하지 않는다.
|
||
|
||
### 3.3 intake policy와 검증
|
||
|
||
선택 전에 composition root가 `BrowserFilePolicyProfile`에 다음을 고정한다.
|
||
application port의 caller는 raw selection/inspection/preview/download 정책을
|
||
전달하지 않는다. composition에서 발급·등록한 정확한 `FilePolicyReference`
|
||
객체와 더 작은 count/byte limit만 전달한다. registry는 배열, signature pattern,
|
||
MIME allowlist까지 깊게 snapshot하며 동일한 `policyKey`/`intention` 문자열로 새
|
||
reference를 만들어도 권한으로 인정하지 않는다. 따라서 presentation이 다른
|
||
feature의 전략, MIME, extension, integrity 또는 byte ceiling을 선택할 수 없다.
|
||
|
||
- purpose/policy version
|
||
- single/multiple, 최대 file count
|
||
- 개별 byte limit과 누적 byte limit
|
||
- zero-byte 허용 여부
|
||
- allowlisted extension과 reported MIME pair
|
||
- bounded inspection bytes와 content signature rule
|
||
- active content/archive 처리 방식
|
||
- classification, retention, telemetry redaction
|
||
|
||
inspection 성공 receipt는 `policyId` 문자열이 아니라 정확한 registered profile과
|
||
file snapshot에 묶인다. 같은 inspection rule ID를 공유하는 다른 preview
|
||
profile로 receipt를 replay하면 `POLICY_REJECTED`다. picker/save dialog를 기다리는
|
||
동안 caller request, handle array, byte-source method 또는 composition dependency가
|
||
바뀌어도 이미 snapshot/bind한 ref, signal, limits, handle loader와 stream만
|
||
사용한다.
|
||
|
||
`accept`, extension, `File.type`과 magic byte는 모두 client-side 조기 UX다.
|
||
선택·preview·client-generated download만 도입한 feature에는 backend upload가
|
||
필수가 아니다. 업로드를 별도 선택했다면 최종 backend는 authorization과 함께
|
||
다음을 다시 검증한다.
|
||
|
||
- 실제 byte length, MIME sniffing, allowlisted parser
|
||
- image dimensions/pixel budget, PDF active content
|
||
- archive entry count, nesting, expanded size/ratio, path traversal와 symlink
|
||
- malware scan/CDR/quarantine
|
||
- polyglot/malformed input과 decompression bomb
|
||
- resource owner와 upload session scope
|
||
|
||
그 별도 upload workflow에서 서버 검증이 끝나기 전 resource 상태는
|
||
`QUARANTINED`다. public URL이나 active preview를 발급하지 않는다.
|
||
|
||
### 3.4 Blob과 memory budget
|
||
|
||
다음 API는 전체 payload를 메모리에 올릴 수 있으므로 제품별 hard cap 아래에서만
|
||
허용한다.
|
||
|
||
- `Blob.arrayBuffer()`, `Blob.text()`, `Blob.bytes()`
|
||
- `Response.arrayBuffer()`, `Response.blob()`, `Response.text()`
|
||
- base64/Data URL conversion
|
||
- 대형 `Uint8Array` 하나로 합치기
|
||
|
||
대용량 경로는 `Blob.stream()`/`Response.body`와 bounded chunk 또는 multipart를
|
||
사용한다. application 계약에는 `readRange` 또는 닫힌
|
||
`AsyncIterable<BrowserDataResult<Uint8Array>>`만 노출하며 adapter가 browser
|
||
`ReadableStream`/`WritableStream`과 연결한다. recipe contract의 동일 경계는
|
||
`CapabilityResult<Uint8Array>`를 쓴다. 각 chunk는 성공 또는 allowlisted failure고,
|
||
consumer는 첫 실패에서 소비를 중지한다. native `DOMException`, rejection 또는
|
||
부분 byte 뒤의 raw throw가 application 경계를 통과해서는 안 된다.
|
||
|
||
memory upper bound는 적어도 다음 식으로 review한다.
|
||
|
||
```text
|
||
part size × concurrent parts × implementation copy factor
|
||
+ preview decode surface
|
||
+ framework/network buffering
|
||
<= approved foreground memory budget
|
||
```
|
||
|
||
progress의 total은 `null`일 수 있다. unknown total을 억지 percent로 표시하지
|
||
않고 indeterminate UI를 사용한다. progress event는 animation frame 또는
|
||
4–10Hz 정도로 throttle하고 항상 단조 증가해야 한다.
|
||
|
||
Web Crypto `digest()`는 incremental stream hash가 아니다. 대용량 whole-file을
|
||
한 번에 넣지 않는다. 업로드는 bounded part SHA-256, OPFS는 명시적인 chunk-tree
|
||
digest 또는 별도 검토된 streaming hash adapter를 사용한다.
|
||
|
||
### 3.5 object URL lease
|
||
|
||
`URL.createObjectURL()`은 호출마다 새 URL과 Blob 수명 lease를 만든다.
|
||
`TransientPreviewPort` 한 곳만 생성 권한을 갖고 `{url, release}`를 반환한다.
|
||
|
||
- preview 대상 교체, load error, unmount에서 정확히 한 번 revoke한다.
|
||
- one-shot 작은 download는 click handoff 뒤 cross-engine scheduling을 위해 기본
|
||
30초 grace를 두고 revoke한다. feature/runtime dispose는 grace를 기다리지 않고
|
||
모든 active lease를 즉시 revoke한다.
|
||
- URL을 global state, IndexedDB, query cache, log, analytics에 보관하지 않는다.
|
||
- Service Worker에서 object URL을 사용하지 않는다.
|
||
- SVG, HTML, untrusted PDF 같은 active content를 same-origin object URL iframe에
|
||
직접 preview하지 않는다. 별도 격리 origin 또는 안전한 decode/re-encode 결과,
|
||
아니면 attachment-only 정책을 사용한다.
|
||
|
||
현재 reference preview는 encoded byte/signature/media allowlist와 위
|
||
active-content denylist까지만 runtime에서 강제한다. static raster의 intrinsic
|
||
width/height, pixel 수, decoded surface, animation과 실제 decode 성공은 아직
|
||
검증하지 않는다. preview를 제품에 조립하기 전 VD-15의 bounded header parser와
|
||
decode probe를 구현해야 하며, 그 전에는 byte cap을 decode-memory 안전성으로
|
||
해석하지 않는다.
|
||
|
||
runtime factory는 `hardMaxPreviewBytes`, `hardMaxObjectUrlBytes`,
|
||
`hardMaxTransferBytes` absolute ceiling을 필수로 소유한다. registered profile과
|
||
caller의 optional preview/buffer/transfer reduction은 이 ceiling을 낮출 수만
|
||
있으며 초과 값은 native picker, stream 또는 object URL side effect 전에
|
||
fail-closed한다.
|
||
|
||
## 4. 별도 backend upload workflow 예시
|
||
|
||
이 절은 File/Blob/picker/download runtime의 dependency가 아니다.
|
||
`BrowserFileComposition`에는 picker, content, preview, download만 들어간다.
|
||
제품이 backend upload를 따로 선택하고 아래 서버 책임을 구현할 때에만 recipe의
|
||
`ExampleQuarantinedUploadPort`를 feature-local 계약으로 복사한다. 단순 request가
|
||
실제 size/latency 요구를 만족하면 bounded upload를 쓰고, 재개·대용량이 필요하면
|
||
이 예시를 제품 protocol에 맞게 좁힌다.
|
||
|
||
1. backend가 purpose, detected type, declared bytes와 authorization을 검증해
|
||
short-lived session을 만든다.
|
||
2. backend가 part size, concurrency upper bound, expiry와 checksum algorithm을
|
||
반환한다.
|
||
3. client는 `FileContentPort.readRange()`로 bounded part만 읽는다.
|
||
4. 각 part는 part number, offset, checksum, idempotency key로 전송한다.
|
||
5. complete는 ordered part receipt와 checksum을 검증한다.
|
||
6. 성공 resource는 scan 완료 전 `QUARANTINED`다.
|
||
7. user abort는 현재 request의 `AbortSignal`뿐 아니라 server session의 explicit
|
||
`abort()`도 호출한다. unload cleanup은 신뢰하지 않으므로 backend TTL job이
|
||
orphan session을 최종 정리한다.
|
||
|
||
object key, storage bucket, signed URL과 long-lived credential을 client가 결정하거나
|
||
log하지 않는다. session 생성, part, complete 단계마다 authorization을 다시
|
||
확인한다. retry는 같은 idempotency key와 같은 part checksum에만 허용한다.
|
||
이 workflow의 설치, kill switch, 관측성, runbook과 제거는 browser-file runtime과
|
||
독립적이어야 한다.
|
||
|
||
## 5. 다운로드와 저장
|
||
|
||
### 5.1 전략 선택
|
||
|
||
| 상황 | 기본 전략 | 의미 |
|
||
| --- | --- | --- |
|
||
| server artifact, 모든 browser | authorized endpoint를 실제 anchor/navigation으로 전달 | `BROWSER_HANDOFF`; 완료를 관측했다고 주장하지 않음 |
|
||
| 큰 artifact + save picker 지원 | picker를 먼저 열고 response stream을 writable에 전달 | close/integrity 이후 `SAVED` |
|
||
| 작은 client-generated artifact | hard cap 이내 Blob + object URL | `SAVED`가 아니라 browser handoff일 수 있음 |
|
||
| 큰 client-generated + picker 미지원 | server-side artifact 생성 후 direct download | 전체 memory Blob fallback 금지 |
|
||
|
||
`<a download>` click은 browser에게 전달했음을 의미할 뿐 disk write 완료가 아니다.
|
||
따라서 `DownloadOutcome`은 `BROWSER_HANDOFF`와 `SAVED`를 분리한다. 이미 close/
|
||
commit된 뒤 늦게 AbortSignal이 발생하면 성공을 취소로 거짓 보고하지 않는다.
|
||
|
||
stream-to-file에서 integrity를 close 전에 확인해야 하면 writable을 열린 상태로
|
||
유지하고, digest가 맞을 때만 close한다. mismatch/abort/partial write면 abort하고
|
||
`INTEGRITY_FAILED` 또는 partial-save failure를 반환한다.
|
||
|
||
전략, media type, safe extension, integrity mode와 최대 bytes는 composition
|
||
registry가 소유한다. caller는 composition이 등록한 정확한
|
||
`FilePolicyReference` 객체, 아래 source union의 최소 입력, filename과 limit
|
||
reduction만 전달한다. `policyKey`/`intention`이 같은 새 객체나 raw 전략을
|
||
제출할 수 없다. source는 다음처럼 분리한다.
|
||
|
||
- `BROWSER_MANAGED_RESOURCE`: resource ID와 서버 발급 capability receipt
|
||
- `AUTHORIZED_STREAM_RESOURCE`: 별도 authorization adapter가 여는 stream
|
||
- `GENERATED`: runtime이 snapshot한 closed-Result byte source
|
||
|
||
`BROWSER_MANAGED_RESOURCE`는 user action 전에 준비된 synchronous resolver로만
|
||
처리한다. caller의 branded `BrowserManagedDownloadCapabilityReceipt`와 resolver가
|
||
돌려주는 receipt는 정확히 같아야 한다. capability는 그 receipt, resource ID,
|
||
media type, safe extension, server-enforced max bytes, optional SHA-256와 expiry를
|
||
정확히 묶어야 한다. 하나라도 registered profile/source와 다르거나 expired이면
|
||
navigation handoff를 하지 않는다. async signed-URL fetch를 click 뒤 기다리거나
|
||
단순 `resourceId -> href` 함수를 authorization으로 간주하지 않는다.
|
||
|
||
### 5.2 filename과 response contract
|
||
|
||
filename은 advisory metadata다. client와 server 모두 다음을 처리한다.
|
||
|
||
- `/`, `\`, path segment, NUL/control, bidi override 제거
|
||
- leading/trailing space와 dot 제거
|
||
- `.`, `..`, Windows device name 거절
|
||
- Unicode NFC 정규화와 byte-length limit
|
||
- content type과 맞지 않는 executable/double extension 교체
|
||
- 안전한 fallback filename
|
||
|
||
서버가 download를 소유하면 `Content-Disposition: attachment`와 UTF-8
|
||
`filename*`, ASCII `filename` fallback을 함께 제공한다. cross-origin download는
|
||
`download` attribute만 믿지 않는다. JS가 header를 읽어야 하면 CORS
|
||
`Access-Control-Expose-Headers`도 계약에 넣는다.
|
||
|
||
민감 artifact의 권장 header 시작점은 다음과 같다. 실제 caching 정책은 제품
|
||
분류에 맞춰 backend가 결정한다.
|
||
|
||
```http
|
||
Content-Type: application/pdf
|
||
Content-Disposition: attachment; filename="report.pdf";
|
||
filename*=UTF-8''report.pdf
|
||
X-Content-Type-Options: nosniff
|
||
Cache-Control: private, no-store
|
||
Cross-Origin-Resource-Policy: same-origin
|
||
```
|
||
|
||
## 6. IndexedDB
|
||
|
||
### 6.1 port와 wire model
|
||
|
||
IndexedDB는 기존 동기식 `StoragePort` backend가 아니다. feature application은
|
||
업무 intent형 async repository를 소유하고 adapter만 IndexedDB를 안다.
|
||
`StructuredOfflineStore<T>`는 복사 후 feature query와 mutation으로 더 좁힐
|
||
recipe다.
|
||
|
||
reference runtime의 실제 wire layout은 하나의 conceptual envelope가 아니라
|
||
분리된 store record다. store 이름은 composition-owned safe identifier이며 다음
|
||
shape와 역할을 유지한다.
|
||
|
||
```ts
|
||
type StoredRecord = Readonly<{
|
||
key: string;
|
||
codecVersion: number;
|
||
revision: number;
|
||
payload: unknown;
|
||
}>;
|
||
|
||
type StoredRetentionRecord = Readonly<{
|
||
recordKey: string;
|
||
writtenAtEpochMs: number;
|
||
synchronization: "PENDING" | "CONFIRMED" | "NONE";
|
||
measuredBytes: number;
|
||
eligibleAtEpochMs?: number;
|
||
}>;
|
||
|
||
type StoredIdempotencyReceipt = Readonly<{
|
||
idempotencyKey: string;
|
||
operation: "PUT" | "DELETE";
|
||
recordKey: string;
|
||
expectedRevision: number | null;
|
||
fingerprint: string;
|
||
synchronization: "PENDING" | "CONFIRMED" | "NONE";
|
||
revision: number;
|
||
expiresAtEpochMs: number;
|
||
}>;
|
||
|
||
type StoredDatasetBudget = Readonly<{
|
||
bindingKey: "dataset-budget";
|
||
budgetVersion: 1;
|
||
usedBytes: number;
|
||
receiptCount: number;
|
||
}>;
|
||
```
|
||
|
||
- `recordStore`: `StoredRecord`
|
||
- `retentionStore`: `StoredRetentionRecord`와 eligibility index
|
||
- `idempotencyStore`: `StoredIdempotencyReceipt`와 expiry index
|
||
- `governanceStore`: immutable `dataset-binding`과 `StoredDatasetBudget`
|
||
- `lifecycleMetadataStores`: migration checkpoint처럼 partition/session purge 때
|
||
함께 비워야 하는 adapter-owned metadata. immutable governance binding은 이
|
||
목록에 넣지 않는다.
|
||
|
||
위 `StoredRecord`는 reference runtime의 실제 adapter-private wire shape다.
|
||
recipe의 feature-facing `StoredRecord<T>` view model과 혼동하지 않는다.
|
||
`writtenAtEpochMs`, synchronization, `measuredBytes`, `eligibleAtEpochMs`는 record
|
||
envelope에 중복 저장하지 않고 `retentionStore` sidecar에 둔다.
|
||
|
||
- structured clone 성공을 업무 schema 검증으로 간주하지 않는다.
|
||
- 모든 read는 `unknown`에서 runtime codec으로 envelope와 payload version을
|
||
검증한 뒤 domain value로 mapping한다.
|
||
- class instance와 암묵적 Date 직렬화를 피하고 primitive wire object와 명시적
|
||
epoch/ISO 값을 사용한다.
|
||
- database DDL schema version과 record codec version을 분리한다.
|
||
- `IndexedDbDatasetScope`의 `authorityToken`, `namespaceToken`,
|
||
`partitionToken`은 registry-issued opaque token이다. readable namespace,
|
||
account/tenant/user ID 또는 domain key를 token에 인코딩하지 않는다.
|
||
- physical DB명은 오직 세 opaque token으로
|
||
`ca-idb-v1:<authority>.<namespace>.<partition>` 형태로 파생한다. caller가 주는
|
||
database name은 생성 입력이 아니라 exact assertion으로만 허용한다.
|
||
- account scope와 storage policy의 `accountScope`가 일치해야 하며, 모든
|
||
query/index는 해당 dataset scope 안에서만 실행한다.
|
||
- offset pagination 대신 deterministic compound key와 tie-breaker가 있는
|
||
keyset cursor를 사용한다.
|
||
- 실제 query가 없는 index와 민감 원문 index는 만들지 않는다.
|
||
|
||
runtime은 composition 때 scope와 전체 `BrowserStoragePolicy`를 검증하고 깊은
|
||
snapshot/freeze한다. governance store의 immutable `dataset-binding`은 scope와
|
||
정책 전체를 보존한다. 최초 versionchange transaction에서 생성·검증하고,
|
||
일반 open과 maintenance 진입 때도 다시 검증한다. 기존 DB에 binding이 없거나 한
|
||
필드라도 달라지면 임의 보정하거나 다른 dataset으로 열지 않고
|
||
`POLICY_REJECTED`로 fail-closed한다.
|
||
|
||
### 6.2 open, versionchange, blocked 상태
|
||
|
||
connection manager는 다음 상태를 명시적으로 갖는다.
|
||
|
||
```text
|
||
opening -> ready -> draining -> closed
|
||
\-> upgrade-blocked -> ready | online-only
|
||
\-> future-schema -> read-only | online-only
|
||
```
|
||
|
||
- connection을 만든 즉시 `versionchange`와 forced `close` handler를 등록한다.
|
||
- `versionchange`에서는 신규 operation 접수를 중지하고 connection을 즉시
|
||
닫는다. upgrade callback에서 UI state를 저장하려 하지 않는다.
|
||
- upgrader는 versioned prepare hint를 broadcast한 뒤 open한다.
|
||
- `blocked`면 다른 탭을 닫거나 reload하도록 접근 가능한 UI를 보여 준다.
|
||
원격 탭 강제 종료, 무한 reload, timeout 후 자동 DB 삭제는 금지한다.
|
||
- `BroadcastChannel`은 upgrade/invalidation hint다. message loss와 partial
|
||
ordering을 전제로 항상 DB를 다시 읽는다.
|
||
- old bundle이 future schema를 만나면 write를 시도하거나 version을 내리지 않고
|
||
read-only/online-only fallback으로 기동한다.
|
||
- native open request는 일반 AbortSignal로 실제 취소할 수 없다. caller가 취소한
|
||
뒤 늦게 열린 connection은 즉시 close하고 stale 결과를 폐기한다.
|
||
|
||
### 6.3 schema와 data migration
|
||
|
||
`onupgradeneeded`에서는 deterministic structural change와 작은 metadata request를
|
||
동기적으로 queue한다. 다음은 금지한다.
|
||
|
||
- network/fetch
|
||
- timer
|
||
- crypto/hash
|
||
- user callback
|
||
- unrelated Promise/`await`
|
||
- analytics와 외부 side effect
|
||
- 대량 row 변환
|
||
|
||
큰 migration은 expand/migrate/contract로 나눈다.
|
||
|
||
1. **expand:** additive store/index와 migration state만 추가한다.
|
||
2. **fence:** rollout/session authority가 N-1 old-codec writer를 drain하고
|
||
migration/contract window 동안 다시 쓰지 못함을 보장한다. 단순
|
||
`BroadcastChannel` 알림이나 탭 열거는 이 fence가 아니다.
|
||
3. **migrate:** post-open maintenance invocation 또는 worker가 bounded batch를
|
||
변환한다.
|
||
4. 각 batch는 row update와
|
||
`{migrationId, targetCodecVersion, lastKey, state}` checkpoint를 같은
|
||
transaction에 commit하고 aggregate progress count만 caller에 반환한다.
|
||
5. row별 codec version으로 retry가 idempotent해야 한다.
|
||
6. 모든 active release와 rollback window가 지난 뒤 별도 schema version에서
|
||
old store/index 제거를 검토한다. 공통 reference planner는 제품 owner 없는
|
||
destructive DDL을 허용하지 않으므로, 제거는 별도 승인된 product migration
|
||
구현과 rollback evidence가 있어야 한다.
|
||
|
||
old writer drain은 keyset checkpoint의 correctness 전제다. drain되지 않은 N-1이
|
||
현재 `lastKey`보다 앞선 key로 old-codec row를 새로 쓰면 이후 batch가 그 row를 다시
|
||
보지 못한다. 따라서 reference maintenance는 주입된
|
||
`isOldWriterDrainConfirmed`가 참이 아니면 `BLOCKED`로 중단하며, drain 보장은
|
||
migration 완료 후 contract/rollback window까지 유지한다. 새 runtime의 정상 write는
|
||
항상 target codec version을 기록한다.
|
||
|
||
각 invocation은 caller가 주는 `maxRows`와 `maxDurationMs` 중 더 작은 budget을
|
||
지키며 reference runtime의 절대 상한은 500 rows, 30,000ms다. async storage
|
||
operation 사이마다 monotonic deadline을 검사하고 소진 시
|
||
`state: "MORE", budgetExhausted: true`와 keyset checkpoint를 반환한다. migration
|
||
transaction은 revision fence, migrated row, retention sidecar, dataset budget와
|
||
checkpoint를 함께 commit한다.
|
||
|
||
지원하는 모든 historical schema snapshot을 fixture로 보존하고 fresh upgrade,
|
||
중간 crash, resume, 재실행과 N-1 rollback을 실제 browser에서 검증한다.
|
||
|
||
### 6.4 transaction 정확성
|
||
|
||
- raw transaction callback을 port 밖으로 내보내지 않는다.
|
||
- 한 repository method가 하나의 업무 transaction scope다.
|
||
- transaction 안에서 network, timer, UI 또는 unrelated `await`를 수행하지 않는다.
|
||
- request `success`가 아니라 transaction `complete` 이후에만 mutation 성공을
|
||
반환한다.
|
||
- signal이 시작 전에 abort됐으면 transaction을 만들지 않는다.
|
||
- 진행 중 취소는 안전한 경우 `tx.abort()`로 업무 단위 전체를 rollback한다.
|
||
- complete와 abort race는 settle latch 하나로 결정하며 committed write를
|
||
`ABORTED`로 거짓 보고하지 않는다.
|
||
- read-modify-write는 하나의 readwrite transaction과 `RevisionGuard` CAS를 쓴다.
|
||
- command attempt는 unique idempotency key로 중복을 막는다.
|
||
- codec의 `measureStoredBytes()`와 보수적인 envelope reservation으로 logical
|
||
stored bytes를 계산한다. record retention sidecar의 `measuredBytes`와
|
||
`dataset-budget.usedBytes`는 CAS, remove, lifecycle deletion, codec migration과
|
||
같은 transaction에서 원자적으로 갱신한다. 이는 native physical byte 측정치가
|
||
아니며, configured `hardBudgetBytes` 초과는 `LIMIT_EXCEEDED`로 rollback한다.
|
||
- idempotency receipt에는 canonical wire value의 lowercase SHA-256 hex만
|
||
fingerprint로 저장한다. label/ID/PII와 reversible encoding은 거부한다.
|
||
- receipt replay window는 주입된 clock/retention으로 고정하고 expiry index 기반
|
||
bounded maintenance로만 정리한다. 아직 replay 가능한 receipt는 삭제하지 않으며
|
||
prune 성공도 transaction `complete` 이후에만 반환한다. reference runtime의
|
||
receipt retention 절대 상한은 31일이다. configured
|
||
`maxIdempotencyReceipts`와 `dataset-budget.receiptCount`도 write/prune/purge
|
||
transaction에서 원자적으로 강제하며 구현 절대 상한은 1,000,000개다. 제품은
|
||
이 두 상한 안에서 더 짧고 작은 retry window를 선택한다.
|
||
- write 때 retention sidecar의 `eligibleAtEpochMs`를 계산한다. TTL record는 sweep
|
||
전에도 read에서 `EXPIRED_RESOURCE`, query에서 skip 처리한다.
|
||
`UNTIL_SYNCED`는 caller가 `CONFIRMED`를 명시한 record만 lifecycle deletion
|
||
대상이 된다.
|
||
- `SESSION_END`, `LOGOUT`, `ACCOUNT_DELETION`, `RETENTION_SWEEP` batch는 최대
|
||
500 rows/30,000ms의 bounded work다. 매 deleting invocation은 composition이
|
||
필수 주입한 `authorizeLifecycle({action, scope, storagePolicy})`의 opaque
|
||
short-lived proof를 받아 형식 검증한 뒤 즉시 폐기한다. proof를 저장하거나
|
||
telemetry에 내보내거나 caller가 직접 제공하게 하지 않는다.
|
||
- full partition purge는 record와 retention sidecar/idempotency receipt뿐 아니라
|
||
등록된 모든 `lifecycleMetadataStores`를 같은 bounded transaction에서 비우고
|
||
dataset budget counter도 원자적으로 맞춘다. governance binding 자체는 dataset
|
||
identity 검증을 위해 유지한다.
|
||
- 외부 cursor/range key는 native 변환 전에 depth, total element와 total byte
|
||
절대 상한을 검사하고 초과·순환·비정상 key를 `INVALID_INPUT`으로 거부한다.
|
||
- network sync는 `TX1 snapshot/lease -> network outside TX -> TX2 revision와
|
||
fencing token 재검증` 순서다.
|
||
- Web Locks는 maintenance/leader 최적화일 뿐 correctness를 대체하지 않는다.
|
||
|
||
### 6.5 corruption과 recovery
|
||
|
||
runtime codec이 실패한 record와 `NotReadableError`/forced close 같은 database
|
||
failure를 분리한다.
|
||
|
||
- reconstructable record: safe issue bucket만 기록하고 purge/re-fetch 가능
|
||
- synced copy: revision 확인 후 server rehydrate
|
||
- unsynced/user-authored: 자동 삭제 금지, read-only 격리, export/sync/runbook
|
||
- migration/open failure: bounded reopen 한 번 후 online-only
|
||
- full DB deletion: classification owner와 recovery/export 확인 후 명시적 runbook
|
||
action만 허용
|
||
|
||
raw key, value, account ID, exception message/stack을 telemetry로 보내지 않는다.
|
||
|
||
## 7. OPFS
|
||
|
||
### 7.1 역할과 layout
|
||
|
||
OPFS는 사용자가 탐색하는 파일 시스템이 아니라 origin-private storage다.
|
||
사용자 filename, email, tenant/user ID를 path에 넣지 않는다.
|
||
|
||
default physical layout은 구현과 동일하게 다음과 같다.
|
||
|
||
```text
|
||
/ca-frontend-opfs-v1/
|
||
authorities/<authorityToken>/<namespaceToken>/<partitionToken>/
|
||
objects/<object-id-prefix>/<opaque-object-id>/<generation>/manifest.json
|
||
chunks/sha256/<digest-prefix>/<digest>.bin
|
||
staging/<transaction-id>/receipt.json
|
||
```
|
||
|
||
구조화 metadata, query, revision, refcount와 operation journal은 IndexedDB가
|
||
소유한다. OPFS에는 immutable chunk와 bounded runtime-schema-validated manifest만
|
||
둔다. readable `scope.namespace`는 경로에 쓰지 않는다.
|
||
|
||
IndexedDB journal DB는 authority token으로 격리되고, 첫 mutation에서 두 binding을
|
||
같은 transaction에 기록한다. physical
|
||
`authorityToken|namespaceToken|partitionToken -> readable namespace + full policy
|
||
fingerprint`와 logical
|
||
`authorityToken|namespace|partitionToken -> physical scope key`를 양방향으로
|
||
검증한다. 둘 중 하나만 없거나 기존 값이 다르면 `CORRUPT_DATA` 또는
|
||
`POLICY_REJECTED`로 중단한다. runtime과 byte store도 composition 시 scope/policy를
|
||
snapshot하고 namespace 일치를 확인하므로 caller의 사후 mutation으로 다른
|
||
dataset 경계를 열 수 없다.
|
||
|
||
sync access handle은 DedicatedWorker 안에서만 소유하고 `write/truncate -> verify
|
||
-> flush -> close`를 `finally`까지 보장한다. main thread에서 사용하거나
|
||
committed file을 in-place overwrite하지 않는다. portable exclusive mode를
|
||
기본으로 하고 `readwrite-unsafe`는 금지한다.
|
||
|
||
### 7.2 IndexedDB journal saga
|
||
|
||
IndexedDB와 OPFS 사이에는 cross-API atomic transaction이 없다. 다음 journal이
|
||
“committed logical row가 partial file을 가리키지 않음”을 보장한다.
|
||
|
||
```text
|
||
PREPARING
|
||
IDB: operation ID, object ID, expected/target generation, size, expiry
|
||
|
|
||
| OPFS immutable chunks + physical manifest write/flush/verify
|
||
v
|
||
FILES_READY
|
||
|
|
||
| one IDB transaction: generation CAS + logical object row + journal
|
||
v
|
||
COMMITTED <- 사용자에게 보이는 유일한 commit point
|
||
|
|
||
| staging/old physical object bounded cleanup
|
||
v
|
||
CLEANED -> journal 제거
|
||
```
|
||
|
||
- `PREPARING` crash: partial staging을 검증 후 resume하거나 purge한다.
|
||
- `FILES_READY` crash: expected generation과 digest가 맞으면 idempotent logical
|
||
commit, 아니면 quarantine한다.
|
||
- `COMMITTED`인데 file이 없거나 digest가 다름: `CORRUPT_DATA`/
|
||
`STORAGE_EVICTED`; reconstructable만 rehydrate한다.
|
||
- delete는 logical row tombstone을 먼저 commit해 신규 open을 막고 physical bytes를
|
||
나중에 정리한다.
|
||
- operation ID로 replay를 deduplicate한다.
|
||
|
||
`Web Locks`를 쓰면 고정된 origin mutation lock과 AbortSignal/timeout을 사용한다.
|
||
nested lock을 금지하고 lock ordering을 문서화한다. lock은 multi-file transaction
|
||
또는 crash recovery를 대신하지 않는다.
|
||
|
||
### 7.3 민감한 policy maintenance authority
|
||
|
||
`DurableObjectMaintenancePort.enforcePolicies()` caller는 reason, 더 작은
|
||
time/object budget과 signal만 전달한다. application caller가 proof를 만들거나
|
||
전달하는 필드는 없다.
|
||
|
||
`LOGOUT`, `UNTIL_SYNCED`, `ACCOUNT_DELETION`처럼 제품 authority가 필요한 삭제를
|
||
composition하려면 `requestMaintenanceAuthority` provider와
|
||
`consumeMaintenanceAuthority` consumer를 **둘 다** 주입한다. 하나라도 없으면
|
||
runtime은 삭제 전 `POLICY_REJECTED`와 `READ_ONLY` recovery로 fail-closed한다.
|
||
|
||
- provider는 매 invocation의 exact reason, frozen scope, frozen
|
||
`BrowserStoragePolicy`에 묶인 새 opaque proof와 expiry를 발급한다.
|
||
- runtime은 proof 형식과 현재 시각 기준 양수 expiry를 검증하고 수명을 최대
|
||
5분으로 제한한다.
|
||
- consumer는 같은 reason/scope/policy/expiry를 다시 확인하고 proof를 원자적으로
|
||
consume한다. replay, scope/action/policy mismatch와 expiry를 거절해야 한다.
|
||
- proof는 provider에서 consumer로만 전달한다. application port로 반환하거나
|
||
저장·telemetry·diagnostic에 기록하지 않는다.
|
||
|
||
`TTL`, `SESSION_END`, `PRESSURE` maintenance에는 이 proof protocol을 적용하지
|
||
않지만 frozen policy eligibility와 bounded budget은 그대로 강제한다.
|
||
|
||
### 7.4 integrity, GC와 fallback
|
||
|
||
큰 object에 Web Crypto whole-file digest를 사용하지 않는다. 고정된 bounded
|
||
chunk마다 SHA-256을 계산하고 아래처럼 algorithm ID가 포함된 tree root를 쓴다.
|
||
|
||
```text
|
||
SHA-256-TREE-V1(
|
||
canonical(totalBytes, chunkSize, ordered chunk byteLength + digest)
|
||
)
|
||
```
|
||
|
||
server whole-file SHA-256과 같은 값이라고 주장하지 않는다. server protocol이
|
||
whole-file hash를 요구하면 vetted incremental implementation을 별도 공급망/
|
||
bundle review로 선택한다.
|
||
|
||
GC는 exclusive mutation lock 아래 mark/sweep을 bounded batch로 수행한다.
|
||
|
||
1. stale staging
|
||
2. expired reconstructable object
|
||
3. unreferenced chunk에 grace period
|
||
4. tombstoned physical object
|
||
5. synced copy
|
||
|
||
committed user-authored object는 자동 quota GC 대상이 아니다. max entries/bytes/time,
|
||
cursor, 다음 실행 시점과 visibility/battery 정책을 둔다.
|
||
|
||
제품 composition의 목표 capability detection은 API property 확인만으로 끝내지
|
||
않는다. secure context, DedicatedWorker round-trip, small write/read/delete probe,
|
||
IndexedDB journal과 필요한 lock을 확인한다. 현재
|
||
`inspectBrowserOpfsSupport()`는 property/primitive 확인까지만 수행하고 실제
|
||
small-operation preflight는 구현하지 않았다. VD-15가 probe scope, deadline,
|
||
cleanup과 readiness mapping을 정의한다. 목표 fallback 순서는 다음과 같다.
|
||
|
||
```text
|
||
OPFS + IndexedDB journal
|
||
-> product-approved, size-capped IndexedDB Blob
|
||
-> online-only/unavailable
|
||
```
|
||
|
||
silent in-memory “durable” fallback은 금지한다.
|
||
|
||
## 8. Cache Storage
|
||
|
||
### 8.1 의미
|
||
|
||
Cache Storage는 browser HTTP cache와 분리된 script-managed
|
||
`Request`/`Response` map이다.
|
||
|
||
- 자동 TTL, freshness, revalidation, eviction ordering을 제공하지 않는다.
|
||
- Service Worker script update나 unregister가 cache를 자동 삭제하지 않는다.
|
||
- Window/Worker에서도 사용할 수 있어 Service Worker 설치가 필수는 아니다.
|
||
- offline fetch interception을 한다면 한 Service Worker runtime만 owner다.
|
||
- TanStack Query entity cache나 application repository로 사용하지 않는다.
|
||
|
||
raw `Cache`, `Request`, `Response`는 platform-local adapter 안에 둔다. application
|
||
port에서 generic `cache.get/set`을 제공하지 않는다.
|
||
|
||
### 8.2 허용·금지 정책
|
||
|
||
기본 허용:
|
||
|
||
- build manifest에 등록된 same-origin content-hashed JS/CSS/font/image
|
||
- HTTP 200
|
||
- expected media type, byte length, integrity가 모두 일치
|
||
- request credentials `omit`
|
||
- public data classification
|
||
- 선택형 runtime cache는 anonymous public GET, explicit URL class, TTL/entry/byte
|
||
limit과 owner가 있을 때만
|
||
|
||
기본 금지:
|
||
|
||
- `/config.json`, `/release-manifest.json`
|
||
- bootstrap HTML; 별도 static offline document만 예외 review
|
||
- auth/API/user/tenant URL
|
||
- Authorization, cookie-dependent request, credentials include
|
||
- private, personal, confidential representation
|
||
- `Cache-Control: no-store`, `private`, 보수적으로 `no-cache`
|
||
- `Set-Cookie`, `Vary: Cookie`, `Vary: Authorization`, `Vary: *`
|
||
- opaque/opaqueredirect/error/redirect response
|
||
- 206 partial content, non-GET
|
||
- 검증할 수 없는 content type/size/integrity
|
||
|
||
표준 Cache API가 위 정책을 자동 강제한다고 가정하지 않는다. adapter가 put 전에
|
||
검증한다. opaque response는 body/header/integrity를 검사할 수 없으므로 기본
|
||
거절한다.
|
||
|
||
`PublicCacheAsset.expectedContentType`은 선택 metadata가 아니라 manifest binding의
|
||
일부다. adapter는 media type을 정규화하고 canonical manifest의
|
||
`URL + request headers + expectedByteLength + expectedContentType + SHA-256`에
|
||
포함해 `manifestDigestHex`를 검증한다. fetch response의 정규화된
|
||
`Content-Type`이 이 값과 정확히 다르면 body digest가 맞아도
|
||
`INTEGRITY_FAILED`이고 candidate를 활성화하지 않는다.
|
||
|
||
default match의 query와 `Vary` 의미를 보존한다. fragment는 cache key에서
|
||
제외하지만 query는 유지한다. `ignoreSearch`, `ignoreMethod`, `ignoreVary`는
|
||
production policy에서 금지한다. 전체 `CacheStorage.match()` 대신 정확한 owned
|
||
cache name을 열어 match한다.
|
||
|
||
response를 network consumer와 cache 양쪽에 전달하면 body를 소비하기 전에
|
||
`clone()`한다. clone fan-out과 backpressure 때문에 cache entry byte upper bound를
|
||
강제한다.
|
||
|
||
### 8.3 release candidate와 activation
|
||
|
||
cache에는 rename API가 없으므로 name만 바꿔 atomic release라고 주장하지 않는다.
|
||
|
||
```text
|
||
build-produced manifest
|
||
-> candidate cache 생성
|
||
-> exact URLs fetch(credentials omit)
|
||
-> type/size/integrity 검증 후 put
|
||
-> verified marker + manifest digest
|
||
-> composition owner의 explicit activation
|
||
-> active release pointer
|
||
-> current + previous grace retain
|
||
-> incomplete candidate와 오래된 owned cache cleanup
|
||
```
|
||
|
||
한 entry라도 실패하면 candidate는 incomplete이며 active로 전환하지 않는다.
|
||
static Cache-only 조합은 waiting worker, `skipWaiting()`, `clients.claim()` 또는
|
||
controlled-client drain을 activation 전제나 성공 증거로 사용하지 않는다.
|
||
composition owner가 exact release ID와 manifest digest로
|
||
`activateRelease(releaseRegistryId, manifestDigestHex)`를 호출하고, current +
|
||
previous 또는 승인된 grace window를 유지한다. rollback은 verified previous
|
||
release만 대상으로 한다.
|
||
|
||
Service Worker를 별도 선택한 조합만 VD-15 section 9.2의 waiting worker/page update
|
||
controller protocol을 추가한다. 이 branch에서 page controller가 dirty form,
|
||
active transfer와 compatibility를 확인하고 versioned activation을 승인하며, old
|
||
controlled client가 drain되기 전에는 그 client가 사용할 release를 cleanup
|
||
eligible로 만들지 않는다.
|
||
|
||
cleanup caller는 retain cache name, release registry ID 또는 retain list를
|
||
제출하지 않는다. adapter가 검증된 active pointer와 composition의
|
||
`retainedPreviousReleaseCount`에서 보존 집합을 계산하고 owned-prefix 안의
|
||
나머지만 삭제한다. active pointer와 release marker 같은 control JSON도
|
||
`Response.json()`/`text()`로 무제한 materialize하지 않는다. content-length
|
||
preflight와 정확히 2 MiB(2,097,152 bytes) cap의 stream reader로 완전히 읽은 뒤
|
||
strict UTF-8와 runtime schema를 검증하며, cap을 넘으면 reader를 cancel하고
|
||
`CORRUPT_DATA`로 fail-closed한다.
|
||
|
||
현재 `cleanupOwned()`와 `inspect()`는 위 ownership/integrity 경계를 지키지만
|
||
cache count에 대한 cursor, per-invocation max count와 deadline은 제공하지 않는다.
|
||
따라서 cache 수에 비례한 unbounded maintenance를 production boot path에
|
||
연결하지 않는다. VD-15의 목표 runtime은 active-pointer epoch와 policy에 묶인
|
||
opaque cursor, 기본 100 caches/5초와 절대 500 caches/30초 상한을 적용한다.
|
||
|
||
현재 static release runtime의 previous retain과 기존
|
||
`activateRelease(releaseRegistryId, manifestDigestHex)`는 검증된 이전 release를
|
||
다시 활성화할 수 있는 primitive다. 다만 Service Worker waiting/activation,
|
||
old controlled-client drain, `skipWaiting`/`clients.claim` 승인과 실제 rollback
|
||
controller는 구현·조립하지 않았다. Cache Storage runtime을 사용한다는 이유만으로
|
||
Service Worker lifecycle이 설치됐다고 표현하지 않는다.
|
||
|
||
다음 fetch 전략은 Service Worker 또는 명시적인 fetch owner를 별도 선택·조합한
|
||
경우의 목표다. static Cache-only runtime은 navigation이나 global fetch를
|
||
intercept하지 않고 verified release의 explicit lookup/activation primitive만
|
||
제공한다. 특히 navigation 전략에는 Service Worker 선택이 필요하다.
|
||
|
||
fetch 전략은 route class별로 고정한다.
|
||
|
||
| request class | 전략 |
|
||
| --- | --- |
|
||
| immutable hashed asset | exact active cache-first |
|
||
| navigation | network-first + 명시적 static offline fallback |
|
||
| runtime config/release manifest/auth/API | network-only |
|
||
| approved public media | bounded TTL metadata가 있는 경우에만 stale-while-revalidate |
|
||
|
||
Cache API 자체는 TTL을 계산하지 않으므로 runtime-public entry metadata와 prune
|
||
owner가 따로 있어야 한다.
|
||
|
||
Service Worker를 선택한 조합에서 unregister는 cache removal이 아니다. worker
|
||
unregister와 parsed owned-prefix cleanup migration을 함께 배포하고, 기존
|
||
controlled client가 drain될 때까지 old worker가 동작할 수 있음을 고려한다.
|
||
static Cache-only 조합은 worker 절차 없이 verified pointer/retention에서 계산한
|
||
owned-prefix cleanup만 수행한다. 어느 branch도 `caches.keys()` 전체를 삭제하지
|
||
않는다.
|
||
|
||
## 9. 닫힌 failure vocabulary
|
||
|
||
recipe의 공통 code는 UI/telemetry에 raw DOMException을 노출하지 않기 위한
|
||
경계다.
|
||
|
||
| code | 대표 원인 | 기본 복구 |
|
||
| --- | --- | --- |
|
||
| `ABORTED` | pre-commit user/caller cancellation | 조용히 종료 또는 명시적 retry |
|
||
| `PERMISSION_DENIED` | picker/save permission | baseline/manual fallback |
|
||
| `LIMIT_EXCEEDED` | count/bytes/page/buffer budget | 입력 축소 |
|
||
| `POLICY_REJECTED` | type/cache/data policy | 저장·전송 금지 |
|
||
| `BLOCKED` | IndexedDB older context | 다른 탭 close/retry UI |
|
||
| `CONFLICT` | revision/generation/idempotency conflict | authoritative re-read |
|
||
| `MIGRATION_FAILED` | schema/data migration | read-only/online-only |
|
||
| `QUOTA_EXCEEDED` | actual write failure | rollback, reconstructable GC, bounded retry |
|
||
| `CORRUPT_DATA` | codec/manifest invalid | 분류별 quarantine/re-fetch/export |
|
||
| `NOT_READABLE` | I/O/forced close | bounded reopen 후 degrade |
|
||
| `INTEGRITY_FAILED` | byte length/digest/signature mismatch | commit 금지, quarantine |
|
||
| `STORAGE_EVICTED` | origin data loss | sentinel 확인, reconstructable rehydrate |
|
||
| `EXPIRED_RESOURCE` | upload/download session expiry | authorization 후 새 session |
|
||
| `UNAVAILABLE` | browser/worker/storage temporarily unavailable | documented fallback |
|
||
| `UNSUPPORTED` | capability absence | baseline/online-only |
|
||
|
||
user dismissal은 failure가 아니라 outcome이다. browser DOMException name은
|
||
adapter에서 이 vocabulary로 mapping하고 raw message/stack은 local bounded
|
||
diagnostic에도 기본 저장하지 않는다.
|
||
|
||
byte stream도 이 vocabulary 밖의 예외 통로를 만들지 않는다. File/OPFS/Cache와
|
||
download recipe의 stream은 chunk마다 closed Result를 내보내며, failure chunk
|
||
하나를 보낸 뒤 종료한다. raw native error를 throw하거나 실패 뒤 추가 byte를
|
||
내보내는 adapter는 contract 위반이다.
|
||
|
||
## 10. 관측성
|
||
|
||
허용 event 예:
|
||
|
||
- `file.selection.rejected`
|
||
- `file.download.handed_off`
|
||
- `file.download.completed`
|
||
- `file.transfer.failed`
|
||
- `storage.database.opened`
|
||
- `storage.upgrade.blocked`
|
||
- `storage.migration.failed`
|
||
- `storage.quota.pressure`
|
||
- `storage.fallback.entered`
|
||
- `opfs.recovery.completed`
|
||
- `cache.release.staged`
|
||
- `cache.operation.failed`
|
||
- `service_worker.update.state`
|
||
|
||
별도 backend upload example을 설치한 feature만 자기 allowlist에
|
||
`file.upload.completed`와 quarantine/session event를 추가한다.
|
||
|
||
허용 attribute:
|
||
|
||
- operation/phase/outcome/failure kind
|
||
- strategy/backend/data class/cache role
|
||
- size, duration, retry, entry count, pressure ratio의 **bucket**
|
||
- registry-owned database/store/migration/release ID
|
||
- total known 여부, persistence mode
|
||
|
||
금지 attribute:
|
||
|
||
- filename, local path, relative path
|
||
- object URL, download/signed URL, query string
|
||
- resource/object/record key, hash/digest
|
||
- raw MIME와 header/body
|
||
- account, tenant, email 또는 user ID
|
||
- exact usage/quota와 raw DOMException message/stack
|
||
|
||
integrity mismatch, unauthorized/private response cache 시도, committed user data
|
||
corruption은 한 건도 canary promotion을 중지한다. 일반 latency/error rate는
|
||
production-like baseline과 minimum sample size가 정해진 뒤 threshold를 승인한다.
|
||
|
||
## 11. 테스트와 promotion evidence
|
||
|
||
fake contract test만으로 native adapter를 production-ready로 선언하지 않는다.
|
||
|
||
### 11.1 unit/property
|
||
|
||
- filename path/control/bidi/Unicode/reserved-name sanitizer
|
||
- count/size/total/zero/unsafe integer 경계
|
||
- MIME-extension-signature 모든 불일치
|
||
- chunk boundary, closed Result failure termination, checksum, idempotency,
|
||
progress monotonicity
|
||
- codec version encode/decode, hostile/oversized/cyclic input
|
||
- migration planner 연속성, pressure hysteresis
|
||
- cache URL/query/Vary/header/classification policy
|
||
- DOMException mapping과 diagnostic redaction
|
||
|
||
### 11.2 deterministic contract/fault injection
|
||
|
||
- 별도 upload example을 선택했다면 part conflict, complete/abort race, session
|
||
expiry
|
||
- delayed/truncated stream, wrong/missing declared length
|
||
- IndexedDB request success 뒤 transaction commit failure
|
||
- atomic multi-record abort, CAS 경쟁, quota injection
|
||
- upgrade/data migration 각 checkpoint crash와 resume
|
||
- OPFS `PREPARING`, `FILES_READY`, `COMMITTED` 직후 worker termination
|
||
- partial chunk, corrupt/truncated manifest, missing committed file
|
||
- Cache candidate 한 entry 실패와 active release 불변
|
||
- persistence denied, storage clear/eviction sentinel mismatch
|
||
|
||
### 11.3 실제 browser
|
||
|
||
Playwright Chromium, Firefox, WebKit에서 실제 secure-origin API를 검사한다.
|
||
`test:browser-capabilities`의 JUnit을
|
||
`verify:browser-capability-evidence`가 읽어 세 engine의 testcase 집합 동일성,
|
||
양수 실행 수, zero failure/error/skipped와 skipped/failure node 부재를 강제한다.
|
||
현재 checkout의 source suite는 engine마다 정확히 같은 14개 case(File 2,
|
||
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
|
||
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
|
||
promotion artifact는 Chromium/Firefox/WebKit의 14개씩, 총 42개가 모두
|
||
실행되어야 한다. 이 host의
|
||
WebKit은 필수 native libraries(예:
|
||
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
|
||
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았고 현재 보존 artifact도
|
||
Chromium/Firefox 14개씩 총 28개만 통과한 상태다. 따라서 promotion evidence를
|
||
충족하지 않으며 verifier가 실패하는 것이 정상이다. `INSTALLED` 전에는 필요한
|
||
system dependency가 있는 CI/device에서 세 engine 전체 evidence를 새로 생성해야
|
||
한다.
|
||
|
||
- native input keyboard/focus/same-file reselection/multiple/dismissal
|
||
- Chromium conditional picker/save enhancement와 다른 engine fallback
|
||
- download event와 ASCII/UTF-8 filename
|
||
- 큰 synthetic stream의 queued-byte high-water mark
|
||
- IndexedDB fresh/upgrade/versionchange/blocked를 same-origin 두 page로 검증
|
||
- concurrent CAS와 BroadcastChannel loss 후 authoritative re-read
|
||
- native BroadcastChannel delivery와 localStorage-event fallback cleanup
|
||
- 실제 OPFS create/open/read/remove, worker handle close
|
||
- release A active → B complete/incomplete candidate → offline → 승인 activation →
|
||
old/new client → rollback/cleanup
|
||
- fixed BFF capability 발급 → verified streaming download → writable close
|
||
- native IndexedDB/Web Locks와 cross-origin fetch를 통한 3-part resumable upload
|
||
- allowlisted Image CDN response의 static header metadata 검사와 실제 bitmap decode
|
||
- private mode/WebView/unsupported path의 explicit degraded state
|
||
|
||
quota를 실제로 가득 채우는 flaky test는 merge gate의 유일한 근거로 쓰지 않는다.
|
||
deterministic injection을 merge gate로, 실제 pressure/clear를 staging/manual drill로
|
||
유지한다. picker/OS UI와 storage eviction은 실제 device manual evidence도
|
||
필요하다.
|
||
|
||
### 11.4 negative static gate
|
||
|
||
`check:browser-file-storage-boundaries`는 다음 source 위반을 거절한다.
|
||
|
||
- application/presentation의 raw `indexedDB`, `caches`,
|
||
`navigator.storage.getDirectory()`, `FileSystem*`
|
||
- production `download(): Uint8Array`와 unbounded Blob/arrayBuffer/text
|
||
- preview facade 밖 `URL.createObjectURL`
|
||
- `File.type`/`accept`만으로 valid 판정
|
||
- transaction 안의 fetch/timer/unrelated await
|
||
- sync access handle의 main-thread 또는 `readwrite-unsafe` 사용
|
||
- cache의 auth/private/no-store/opaque/ignoreVary 허용
|
||
- raw filename/path/key/URL/hash/value telemetry
|
||
- credential persistence
|
||
- recipe source의 production direct import
|
||
|
||
### 11.5 production bundle과 removability evidence
|
||
|
||
Vite plugin은 emitted chunk마다 실제 source module ID를
|
||
`.vite/module-inventory.json`에 기록하고 release artifact로
|
||
`artifacts/quality/vite-module-inventory.json`을 보존한다.
|
||
`check:optional-recipes`는 `productionComposition: false`인 동안 File/Blob,
|
||
IndexedDB, OPFS, Cache Storage reference runtime source prefix가 inventory에 한
|
||
개라도 있으면 실패한다. minified marker 검색은 보조 방어이고 module inventory가
|
||
권위 있는 bundle 증거다.
|
||
|
||
`test:browser-file-storage-removal`은 runtime source root, 전용 browser test/gate,
|
||
catalog의 세 `referenceRuntime` metadata를 격리 copy에서 제거한 뒤 typecheck,
|
||
lint, architecture, 전체 test, build, optional catalog와 CI contract를 다시
|
||
실행한다. 이 removal gate와 production module inventory가 모두 통과해야
|
||
optional runtime이 skeleton core에 결합되지 않았다고 판단한다.
|
||
|
||
## 12. rollout과 rollback
|
||
|
||
실제 도입은 한 번에 모든 기술을 켜지 않는다.
|
||
|
||
1. 데이터 catalog, owner와 fallback 승인; upload를 선택한 경우에만 backend
|
||
protocol 별도 승인
|
||
2. feature-specific port와 codec/schema 구현
|
||
3. fake contract + historical fixture + real-browser adapter test
|
||
4. runtime default off, capability probe와 online-only fallback
|
||
5. internal cohort에서 read-only/shadow write 검증
|
||
6. 작은 cohort write, quota/recovery/rollback drill
|
||
7. N-1 reader와 migration compatibility 확인 후 확대
|
||
8. product value가 확인된 user action에서만 persistence 설명/요청
|
||
|
||
kill switch는 좁게 분리한다.
|
||
|
||
- enhanced picker/save off → native input/direct authorized download
|
||
- preview off
|
||
- 별도 upload workflow를 설치한 경우 resumable upload off → bounded simple upload
|
||
- offline read-write → read-only → online-only
|
||
- OPFS writes off → approved size-capped fallback 또는 online-only
|
||
- Cache Storage/SW off → network-only
|
||
- new worker activation off → verified current/previous 유지
|
||
|
||
schema version은 내리지 않는다. rollback bundle은 future schema를 감지해
|
||
read-only/online-only로 살아나야 한다. destructive DDL, old store/cache 삭제는
|
||
승인된 rollback window가 지난 뒤 별도 release에서 실행한다.
|
||
|
||
운영 절차는 `docs/operations/browser-file-storage-recovery.md`, decision은
|
||
`docs/architecture/decisions/VD-11-browser-file-and-origin-storage.md`를 따른다.
|
||
|
||
## 13. 공식 기준
|
||
|
||
- [W3C File API](https://w3c.github.io/FileAPI/)
|
||
- [WHATWG HTML file upload state](https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file))
|
||
- [WHATWG HTML downloading resources](https://html.spec.whatwg.org/multipage/links.html#downloading-resources)
|
||
- [WICG File System Access](https://wicg.github.io/file-system-access/)
|
||
- [WHATWG File System](https://fs.spec.whatwg.org/)
|
||
- [WHATWG Streams](https://streams.spec.whatwg.org/)
|
||
- [WHATWG MIME Sniffing](https://mimesniff.spec.whatwg.org/)
|
||
- [RFC 6266 Content-Disposition](https://www.rfc-editor.org/rfc/rfc6266.html)
|
||
- [W3C Indexed Database API 3.0](https://w3c.github.io/IndexedDB/)
|
||
- [WHATWG Storage](https://storage.spec.whatwg.org/)
|
||
- [W3C Service Workers and Cache](https://w3c.github.io/ServiceWorker/)
|
||
- [RFC 9111 HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html)
|
||
- [W3C Web Locks](https://w3c.github.io/web-locks/)
|
||
- [W3C Web Cryptography](https://w3c.github.io/webcrypto/)
|
||
|
||
File System Access와 일부 IndexedDB 3.0 기능은 evolving specification이다.
|
||
cross-browser baseline은 실제 Chromium/Firefox/WebKit evidence로 고정하고, 새 API
|
||
존재만으로 제품 fallback을 제거하지 않는다.
|