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>
28 KiB
Adapter Review — Service Worker and Web Push
검토 기준:
develop/4dc033c(2026-08-13)범위:
src/adapters/service-worker/**,src/adapters/web-push/**,src/contracts/service-worker.ts,src/contracts/web-push.ts, 관련 build input·unit test·architecture 문서
결론
서비스 워커는 registration ownership, static asset install의 byte/digest 검증, activation drain handshake, clients.claim() 금지와 staged removal이라는 좋은 기반을 갖고 있다. Web Push도 raw endpoint/key를 durable control record에서 분리하고, push/click 전에 association fence를 두 번 확인하며, notification copy/route를 closed registry로 제한한다. 이 경계들은 유지해야 한다.
현재 코드에는 조합 전에 고쳐야 할 P1 항목이 있다.
- generated manifest는 root-relative URL을 가지지만 fetch 분류는 absolute
Request.url과 비교해 정적 cache path가 사용되지 않을 수 있다 (SW-URL-01). - Cache Storage 전체에서 match하여 현재 release가 아닌 구 cache response를 반환할 수 있다 (
SW-01). - reset가 소유권 parser가 아니라 문자열 prefix만 사용해 유사 이름의 타 cache까지 삭제한다 (
SW-02). unregister()의false를 성공으로 보고하며 removal mode도 실패/ownership mismatch를DISABLED로 숨긴다 (SW-03,SW-04).- build input의 static manifest decoder가 asset row와 set digest를 실제로 검증하지 않는다 (
SW-05). - Push fence CAS adapter가 repository의 다음 revision을 확인하지 않고, deadline 뒤 late mutation effect도 표현하지 못한다 (
WP-01,WP-02). - backend registration response가 request의 전체 authority를 echo/bind하지 않아 client가 잘못 묶인 association을 검출할 수 없다 (
WP-03).
Web Push는 현재 AVAILABLE_NOT_COMPOSED이고 제품 선택도 NOT_SELECTED다. service worker entry에 연결되지 않은 사실 자체는 회귀가 아니다. 아래 P1 계약을 해결하고 product-owned registry/provider/consent가 준비되기 전에는 default composition에 추가하지 않는다.
파일별 판정
| 파일 | 역할 | 판정 | 후속 |
|---|---|---|---|
service-worker-entry.ts |
단일 physical worker entry와 event wiring | KEEP/REFACTOR | SW-06, SW-10; 두 번째 registration 생성 금지 |
service-worker-lifecycle.ts |
install/activate/fetch/activation/reset | VERIFIED_DEFECT | SW-URL-01, SW-01, SW-02, SW-07, SW-08 |
service-worker-page-controller.ts |
registration/update/activation/reset page facade | VERIFIED_DEFECT | SW-04, SW-06 |
service-worker-protocol.ts |
page-worker strict message codec/nonce | CONTRACT_GAP | SW-06, SW-10 |
service-worker-removal.ts |
exact registration/cache ownership cleanup | VERIFIED_DEFECT | SW-03 |
service-worker-static-assets.ts |
static manifest/install/cache policy | KEEP/REFACTOR | SW-01, SW-05, SW-09 |
web-push/index.ts |
public exports | KEEP | product selection 전 surface 확대 금지 |
web-push/notification-registry.ts |
closed copy/route registry | KEEP | arbitrary copy/URL 허용 금지 |
web-push/push-association-fence-store.ts |
durable authority state machine | VERIFIED_DEFECT | WP-01, WP-02 |
web-push/push-codec.ts |
bounded hint/click codec | KEEP | exact keys, expiry, no raw text 유지 |
web-push/push-registration-gateway.ts |
fixed backend commands/decoders | CONTRACT_GAP | WP-03, WP-04 |
web-push/push-subscription-adapter.ts |
window consent/native/backend/local orchestration | VERIFIED_DEFECT/REFACTOR | WP-04, WP-05, WP-06 |
web-push/runtime-support.ts |
deadline/link/observation mechanics | CONTRACT_GAP | WP-02, WP-07 |
web-push/service-worker-runtime.ts |
push/click/subscriptionchange handler composition | REFACTOR | WP-06 |
web-push/service-worker-scope-host.ts |
native scope facade | KEEP | single worker entry 내부에서만 사용 |
web-push/inbound/push-event-adapter.ts |
hint → fence → safe notification | KEEP/CONTRACT_GAP | WP-07 |
web-push/inbound/notification-click-adapter.ts |
click → fence → safe route handoff | KEEP/CONTRACT_GAP | WP-07 |
직접 경계 inventory도 대조했다: src/contracts/service-worker.ts는 protocol/cache ownership identity, src/contracts/web-push.ts는 push protocol/selection을 소유한다. src/bootstrap/register-service-worker.ts는 page composition, scripts/lib/service-worker-build-input.ts와 scripts/generate-service-worker-assets.ts는 build decode/generation, vite.service-worker.config.ts는 worker bundle entry를 소유한다. 이 파일들은 SW-05/SW-10의 shared codec과 rollout scope에 포함한다.
Service Worker 상세
SW-URL-01 — generated root-relative manifest와 absolute fetch URL의 분류 불일치
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거: generator
scripts/generate-service-worker-assets.ts는 asset URL을/assets/...로 생성하고,service-worker-lifecycle.ts는 그 문자열 set을 absoluteRequest.url과 직접 비교한다. - 영향: generator output을 그대로 사용하면 verified static URL이 manifest member로 분류되지 않아 current cache lookup path에 들어가지 않고 network fallback이 된다.
SW-01의 cache 선택을 고쳐도 URL identity를 먼저 맞추지 않으면 cache path는 여전히 작동하지 않는다. - 결정: runtime 생성 시 각 root-relative manifest URL을
new URL(asset.url, scope.registrationScope).href로 canonicalize하고 same-origin을 재확인한 frozen absolute URL set을 만든다. install cache key, fetch classification, lookup/delete validation이 이 canonical URL identity를 공유한다. generator의 persisted manifest shape는 root-relative로 유지한다. - 테스트: generator-shaped
/assets/app.<hash>.jsfixture와 absolutehttps://app.example/assets/app.<hash>.jsrequest를 사용해onFetch()가 current cache로 들어가는지 직접 검증한다. 다른 origin, scope 밖 path, query/hash 변형은 거절한다.
SW-01 — fetch가 current static cache가 아닌 전역 CacheStorage를 조회
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거:
service-worker-lifecycle.ts:166-192; worker facadeservice-worker-entry.ts:38-44 - 현재 동작: verified static URL에
scope.caches.match(request.url)을 호출한다. CacheStorage-wide match는 current, previous 또는 같은 URL을 가진 다른 cache 중 먼저 찾은 response를 반환할 수 있다. - 영향:
- current release manifest에 URL이 포함되어 있어도 구 cache의 동일 URL response가 반환될 수 있다.
- invalid hit를 발견해도 삭제는 current cache에만 수행하므로 실제로 반환된 stale cache entry는 남는다.
결정: onFetch()는 config.manifest.setDigest로 계산한 current cache를 open()하고 그 cache에서만 match()한다. worker scope facade의 CacheStorage-wide match는 제거한다.
테스트 추가 (tests/unit/service-worker-runtime.test.ts):
- current/previous cache에 같은 URL과 다른 bytes가 있을 때 current만 반환
- previous에만 entry가 있으면 network fallback (
null) - current invalid response만 current cache에서 삭제
- unrelated cache의 same URL은 조회/삭제하지 않음
완료 조건: runtime fetch path에 caches.match 호출이 0이고 current cache name이 exact digest에서 파생된다.
SW-02 — cache reset가 exact ownership 대신 prefix를 사용
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거:
service-worker-lifecycle.ts:333-365; exact helpersrc/contracts/service-worker.ts:111-116 - 현재 동작:
name.startsWith("ca-static-v1-")이면 삭제한다.isOwnedStaticCacheName()은 정확히 16자리 lower-hex suffix를 요구하지만 reset path가 이를 사용하지 않는다. - 영향:
ca-static-v1-not-owned, suffix가 더 긴 이름 등 같은 prefix를 가진 타 기능/cache가 삭제될 수 있다. - 수정: import되어 있는
isOwnedStaticCacheName(name)만 사용한다. cache name 상수 literal도 lifecycle에서 제거한다. - 테스트: valid 16-hex 두 개만 삭제하고 short/long/non-hex/upper-hex/unrelated cache를 보존한다.
SW-03 — unregister() === false를 UNREGISTERED로 보고
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거:
service-worker-removal.ts:89-120 - 현재 동작: Promise가 resolve하면 boolean을 무시하고
UNREGISTERED를 반환한다. - 수정:
const unregistered = await registration.unregister()후true만 성공으로 인정한다.false는{ kind: "FAILED", operation: "UNREGISTER" }로 닫는다. 새 outcome을 추가할 필요는 없다. - 테스트: true, false, rejection, absent, ownership mismatch를 각각 고정한다.
SW-04 — explicit removal mode가 cleanup 실패를 DISABLED로 숨김
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거:
service-worker-page-controller.ts:78-121 - 현재 동작:
REMOVE_REGISTRATION과PURGE_OWNED_RESOURCES는 실제 outcome과 무관하게DISABLED를 반환한다.disabledCleanup도OWNERSHIP_MISMATCH를DISABLED로 반환한다.
- 영향: staged removal이 끝난 것으로 판단해 다음 release에서 worker source/handler를 제거할 수 있지만 실제 registration 또는 cache가 남아 있을 수 있다.
결정 매핑:
| cleanup outcome | page start outcome |
|---|---|
ABSENT, UNREGISTERED, PURGED |
DISABLED |
OWNERSHIP_MISMATCH |
INCOMPATIBLE |
FAILED |
FAILED (DISABLE_CLEANUP_FAILED, REMOVE_FAILED, PURGE_FAILED) |
관찰 이벤트만 남기고 success로 바꾸지 않는다. 테스트는 selection 세 종류와 위 outcome matrix를 모두 table-driven으로 작성한다.
SW-05 — build input의 manifest row와 set digest 검증 부재
- 우선순위/분류: P1 / CONTRACT_GAP
- 근거:
scripts/lib/service-worker-build-input.ts:82-94; runtime의 부분 검사service-worker-static-assets.ts:85-112; 생성 canonical hashscripts/generate-service-worker-assets.ts:48-93 - 현재 동작:
- build input은 manifest top-level shape만 보고
assets를 type cast한다. - runtime validator도 build/release identity, exact row keys, unique/canonical URL, content type type/allowlist, set digest 재계산을 확인하지 않는다.
- 잘못된
contentType은storeAsset()의.toLowerCase()에서 typed rejection이 아니라 throw가 될 수 있다.
- build input은 manifest top-level shape만 보고
결정: runtime-neutral shared manifest codec이 exact row keys, content-type/extension allowlist, root-relative canonical URL, length-prefixed canonical byte serialization을 소유한다. generator와 Node build gate는 같은 bytes를 Node SHA-256으로 hash하고 worker는 injected WebCrypto digest로 같은 bytes를 재검증한다. Node crypto 구현을 worker에서 import하지 않는다. 이 작업은 기존 2026-08-01 plan Task 5/SW-10의 선행 build-decoder 단계로 병합하며 canonical digest를 별도 PR에서 두 번 구현하지 않는다.
Build gate 필수 조건:
- top-level/asset row exact keys
- buildId/releaseId exact match
- sorted unique same-origin root-relative hashed asset URL
- 허용 content type/extension pair
- non-negative safe byte length와 전체 bound
- lower-hex SHA-256
- generator와 같은 length-prefixed canonical algorithm으로
setDigest재계산
테스트 (tests/unit/service-worker-build-input.test.ts): 각 row field tamper, duplicate/reorder, cross-origin URL, dot segment, wrong extension/content type, wrong set digest, unknown field. valid generator output을 decoder에 다시 넣는 parity test도 추가한다.
SW-06 — activation/reset command의 source identity와 single-flight 부재
- 우선순위/분류: P2 / CONTRACT_HARDENING
- 근거:
service-worker-page-controller.ts:174-211,:231-305,:308-369 - 현재 동작:
- activation 전용 listener는
event.origin과event.source를 검증하지 않는다. - general listener/reset은 origin 일부만 확인하며 expected waiting/controller source와 correlation하지 않는다.
- 동시에
requestActivation()또는resetOwnedCaches()를 여러 번 호출하면 nonce와 listener가 중복 생성된다.
- activation 전용 listener는
결정:
- activation reply는 request 시 capture한
registration.waiting과event.source가 같아야 한다. - reset reply는 request 시 capture한
container.controller와 같아야 한다. - long-lived
CLIENT_DRAIN_REQUESTlistener도 expectedregistration.waitingsource와 correlation한다. nonce가 없더라도 arbitrary same-origin source가 page admission을 닫게 하지 않는다. - empty origin을 신뢰 근거로 사용하지 않고 source identity + nonce + target identity를 함께 검증한다.
- 각 command를 single-flight Promise로 만들고 concurrent caller는 같은 Promise를 받는다.
- message 수신 직전에
event.source, captured source, 현재registration.waiting/container.controller가 모두 동일한지 확인한다. 교체되었으면 ignore 후 timeout이 아니라 즉시PROTOCOL_MISMATCH로 종료한다.
테스트: wrong source with correct nonce, source swap, concurrent 10 calls가 postMessage 한 번, stop 중 pending 종료, retry after terminal.
SW-07 — zero-client drain 의미가 불필요하게 activation을 막음
- 우선순위/분류: P2 / VERIFIED_BEHAVIOR_CHANGE
- 근거:
service-worker-lifecycle.ts:268-300 - 현재 동작: scope 내 client가 0이면
false를 반환한다. requester가 request 직후 닫힌 경우 dirty client가 없는데도 waiting worker가 거절된다. - 결정: empty set은 vacuously drained이므로
true다. 단,clients.matchAll()실패는 reject/throw로 유지한다. - 테스트: zero clients → skipWaiting, one missing ack → timeout/reject, out-of-scope only → zero in-scope로 처리.
SW-08 — client postMessage() 예외가 activation event 전체를 깨뜨림
- 우선순위/분류: P2 / VERIFIED_DEFECT
- 근거:
service-worker-lifecycle.ts:225-265,:290-299 - 현재 동작: drain request/accepted/reload notification loop에 per-client 예외 격리가 없다.
- 결정: drain request 전달 실패는 해당 expected client를 failed 처리하고 pending state를 즉시 정리한다. drain 완료 뒤
skipWaiting()호출 성공을 activation admission commit으로 기록한다. 그 다음ACTIVATE_ACCEPTED/reload 알림은 client별 best effort로 보내고 실패를 degraded observation으로 남긴다. 현재 코드의 pre-commitACTIVATE_ACCEPTED순서는 바꾸거나 protocol V2에서 그 message를 제거한다.skipWaiting()실패는REJECTED/FAILED이고 accepted 성공으로 관찰하지 않는다. - 테스트: 첫/중간/마지막 client throw, skipWaiting throw, partial delivery, pending map leak 없음.
SW-09 — install deadline 뒤 late candidate 작업
- 우선순위/분류: P2 / LIFECYCLE_HARDENING
- 근거:
service-worker-static-assets.ts:119-153,:156-254,:259-274 - 현재 동작: deadline Promise가 먼저 끝나면 candidate cache를 삭제하고 반환하지만, signal을 무시한 fetch/digest/cache put은 뒤늦게 계속될 수 있다. digest rejection도
storeAsset()에서 직접 typed outcome으로 변환되지 않는다. - 결정: public install result는 overall 60초에 닫고 candidate generation fence를 세워 뒤늦은 worker가 새 fetch/digest/put을 시작하지 못하게 한다. late
Responsebody는 compensator로 취소한다. 이미 시작한cache.put은 취소할 수 없으므로 background settlement를 관찰한 뒤 candidate cache를 다시 exact-delete하는 second cleanup을 등록한다. cleanup을 public completion에 포함하려면 그 budget을 총 60초 안에 미리 예약하며, 60초 뒤 별도 cleanup deadline을 await해 public bound를 늘리지 않는다. 모든 dependency exception은 closedFETCH_FAILED/INTEGRITY_MISMATCH로 mapping한다. - 테스트: non-cooperative late fetch/digest, late cache put, digest rejection, delete rejection, unhandled rejection 없음.
SW-10 — message protocol을 kind별 discriminated schema와 full identity로 승격
- 우선순위/분류: P1 before release hardening / 기존 계획 승계
- 근거:
service-worker-protocol.ts:44-143;SERVICE_WORKER_PROTOCOL_VERSION = 1; 기존docs/superpowers/plans/2026-08-01-http-worker-adapter-remediation.mdTask 5 - 현재 동작: 모든 kind가 하나의 optional field bag을 공유하고 page-worker correlation은 주로 buildId에 의존한다.
service-worker-entry.ts:151-166의 sync message는 codec 대신 V1 literal을 직접 만든다.
결정:
- 기존 계획대로 protocol V2에서 protocol/cache schema/build/release/contract/static set 전체 canonical identity digest를 교환한다.
- kind별 exact required/forbidden field schema를 사용한다. activation/reset kinds에는 nonce와 target identity가 필수다.
- 모든 message, including
SYNC_WAKE_OBSERVED,는createServiceWorkerMessage()만 사용한다. - V1/V2 worker가 같은 scope에서 교차 activation하지 않도록 mismatch는 fail-closed하고 강제 skipWaiting 하지 않는다.
이 항목은 기존 계획을 유지한다. 정확한 sequence는 SW-URL-01, SW-01SW-04 → 기존 plan Task 4 bounded activation-marker reader → SW-05 build decoder와 기존 Task 5/SW-10 통합 → SW-06SW-09다. 같은 canonical digest/codec을 중복 구현하지 않는다.
Web Push 상세
WP-01 — CAS success receipt의 expected next revision 미검증
- 우선순위/분류: P1 / VERIFIED_DEFECT
- 근거:
push-association-fence-store.ts:475-513; remove는:516-546에서 next revision을 검사함 - 현재 동작: compareAndSwap success는 key/revision type/replayed만 확인하고
revision === (expectedRevision ?? 0) + 1을 확인하지 않는다. - 영향: repository가 stale/임의 receipt를 반환하면 adapter가 실제로 확인되지 않은 control을 새 revision으로 포장한다. 이후 CAS authority가 틀어진다.
- 수정: write와 remove 모두 exact next revision, expected key, replay semantics를 같은 validator로 검증한다. replayed receipt도 동일 idempotency command의 exact revision이어야 한다.
- 테스트 (
tests/unit/web-push-fence-store.test.ts): stale/same/skipped/huge revision, wrong key, malformed replay, valid initial/next/replayed receipt.
WP-02 — deadline 뒤 local fence mutation effect가 UNKNOWN일 수 있음
- 우선순위/분류: P1 / CONTRACT_GAP
- 근거:
runtime-support.ts:51-113; fence store:411-423,:493-503 - 현재 동작: deadline은 signal을 abort하고 실패를 반환하지만 generic
PushControlRepository가 signal을 무시하거나 commit 경계 직후 늦게 resolve하면 CAS는 반환 이후 적용될 수 있다. - 영향: security fence adapter가
DEADLINE_EXCEEDED를 반환한 뒤 ACTIVE/REVOKED record가 실제로 바뀔 수 있다.
결정:
- read deadline wrapper와 mutation wrapper를 분리한다. repository는 commit 전 abort 시
NOT_APPLIED, commit 후 success receipt를 반환한다. deadline뿐 아니라 caller abort와 commit/receipt race도 unknown일 수 있다. WebPushFailureCode에MUTATION_OUTCOME_UNKNOWN과 recovery reason을 추가한다. lifecycle은OPEN | RECONCILIATION_REQUIRED | CLOSED이며 unknown 뒤 mutation admission을 닫는다.- 복구는 새 bounded read로 exact revision/authority/state를 확인한 뒤에만 한다.
withAbortableDeadline을 mutation의 correctness authority로 사용하지 않는다. deadline은 caller wait bound이며 effect는 repository receipt/read-back이 결정한다.
테스트: timeout-before-commit, timeout-racing-commit, late success, late rejection, recovery read, dispose 중 late ACTIVE 금지.
WP-03 — backend commit이 전체 request authority에 binding되지 않음
- 우선순위/분류: P1 / CONTRACT_GAP
- 근거: request
push-registration-gateway.ts:73-121; response:170-218; activation checkpush-subscription-adapter.ts:682-711 - 현재 동작: request는
fenceGeneration,sessionBindingEpoch,releaseEpoch을 보낸다. response는associationEpoch과sessionBindingEpoch만 반환하고 adapter도 session epoch만 비교한다. - 영향: provider/server bug 또는 stale response가 다른 fence/release request의 association을 반환해도 local current fence가 unchanged이면 ACTIVE로 commit할 수 있다.
결정: register와 reconcile의 request/response protocol을 V2로 올리고 서로 다른 exact response union을 사용한다.
type WebPushRegisterCommitV2 = Readonly<{
protocol: "WEB_PUSH_REGISTRATION_RECEIPT_V2";
associationEpoch: string;
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
replacedAssociationEpoch: string | null;
}>;
type WebPushReconciliationV2 =
| Readonly<{
protocol: "WEB_PUSH_RECONCILIATION_V2";
state: "ACTIVE";
associationEpoch: string;
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
}>
| Readonly<{
protocol: "WEB_PUSH_RECONCILIATION_V2";
state: "ABSENT";
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
requestBindingSha256: string;
}>;
WEB_PUSH_PROTOCOLS가 V2 literal과 length-prefixed field order를 소유한다. register digest에는 operation, authority tuple, subscription fingerprint, idempotency key, expected previous association epoch를 넣는다. reconcile에는 idempotency key가 없으므로 명시적으로 제외한다. decoded fixed-length digest bytes를 비교한 뒤 fence CAS를 수행한다.
배포: server가 V1 request에는 V1 response, V2 request에는 V2 response를 반환하도록 request protocol negotiation 배포 → V2 client → old client drain → V1 제거. exact decoder를 깨뜨리는 response dual-emit은 하지 않는다. authority field mutation과 reconcile ABSENT fixture를 추가한다.
WP-04 — repeated enable의 backend upsert/rotation 의미가 타입에 없음
- 우선순위/분류: P2 / CONTRACT_GAP
- 근거:
push-subscription-adapter.ts:162-275, fenceprepare():175-215,activate():219-270 - 현재 동작: 같은 authority가 이미 ACTIVE여도
enable()은 새 idempotency key로 backend register를 다시 수행한다. server atomic installation upsert가 같은 association을 반환하거나 old association을 폐기한다는 문서 요구가 gateway receipt에 표현되지 않는다. - 결정: public
enable()이 publicreconcile()을 호출하지 않는다. permission/prepare 뒤 privatereconcilePrepared()flow를 공유해 exclusive guard 내부에서 호출한다. ACTIVE + valid native material이면 먼저 reconcile하고ABSENT일 때만 register한다. request에expectedPreviousAssociationEpoch: string | null을 보내고 receipt의replacedAssociationEpoch과 exact match해야 한다. local activate는 old ACTIVE와 다른 epoch를 무조건 덮어쓰지 않는다. - 테스트: double enable same epoch, reconcile active, server absent then register, replacement receipt, replacement without old epoch rejection, compensation on CAS failure.
WP-05 — pre-aborted operation이 항상 INSPECT로 기록됨
- 우선순위/분류: P3 / VERIFIED_DEFECT
- 근거:
push-subscription-adapter.ts:462-478 - 수정:
webPushFailure("ABORTED", failureOperation)을 사용한다. - 테스트: enable/reconcile/revoke/inspect 각각 pre-aborted operation field.
WP-06 — bounded truncation을 성공으로 관찰
- 우선순위/분류: P2 / EVIDENCE_CORRECTNESS
- 근거: subscriptionchange client handoff
service-worker-runtime.ts:139-172; notification cleanuppush-subscription-adapter.ts:908-945 - 현재 정책: client 32개, notification 64개/2초로 bounded best effort이다. architecture 문서는 notification cleanup을 privacy guarantee로 보지 않고 account-neutral copy를 요구하므로 상한 자체는 결함이 아니다.
- 문제: 목록이 상한을 넘었는데도 success로 관찰해 운영자가 일부 처리만 된 사실을 알 수 없다.
- 수정:
WebPushObservation에countBucket: "0" | "1_8" | "9_32" | "33_64" | "GT_64"와truncated: boolean을 추가한다. subscriptionchange는 32 초과 시LIMIT_EXCEEDED/DEGRADED; notification cleanup은 64 초과 시 revoke authority와 분리된 cleanup observation을DEGRADED로 기록하고{ complete: false }를 반환한다. 무제한 loop나 전체 정리를 주장하지 않는다. - 테스트: 33 clients, 65 notifications, owned item이 cap 밖에 있는 경우, account-neutral copy/click fence가 계속 안전함.
WP-07 — user-visible native effect와 deadline result의 certainty
- 우선순위/분류: P2 / CONTRACT_GAP
- 근거:
runtime-support.ts:51-113,push-event-adapter.ts:144-174,notification-click-adapter.ts:144-176 - 현재 동작: deadline/abort가 먼저 반환된 뒤
showNotification,focus,openWindow가 늦게 성공할 수 있다. 결과는 failure지만 user-visible effect는 발생할 수 있다. - 결정: native 호출 전 terminal=
NOT_APPLIED, native Promise pending 중 terminal=MAYBE_APPLIED, fulfillment=CONFIRMED로 phase를 고정한다. native-effect 전용 observation union에 effect를 두고 wrapper가onLateValue/onLateError로 outer result 종료 뒤에도 safe observation을 한 번 남긴다. 이 observation을 authorization/retry에 사용하지 않는다. account-neutral notification과 click-time fence가 최종 안전 장치다.
유지해야 할 설계
- 한 scope에 physical Service Worker registration은 하나만 둔다.
- static install은 immutable hashed asset만 대상으로 하고 byte/digest 검증 후 all-or-nothing으로 공개한다.
- navigation, runtime config, release manifest, API response는 static cache에 넣지 않는다.
skipWaiting()은 page/client drain handshake 이후에만 호출하고 baseline에서clients.claim()은 사용하지 않는다.- registration과 cache ownership을 exact scope/script/cache parser로 확인한다.
- Web Push endpoint, p256dh, auth, account/user ID, notification content를 durable fence/diagnostics에 저장하지 않는다.
- push와 click 모두 initial/final fence를 확인하고 arbitrary URL 또는 backend raw copy를 사용하지 않는다.
- revoke는 local generation fence를 먼저 commit하고 backend/native cleanup은 bounded best effort로 수행한다.
- notification cleanup 성공을 privacy 보장으로 주장하지 않는다. copy는 항상 account-neutral이어야 한다.
WEB_PUSH가 선택되지 않은 현재 baseline에서 worker import/handler를 억지로 추가하지 않는다.
실행 순서
SW-URL-01,SW-01SW-04,WP-01WP-03을 독립 P1 PR로 처리한다.- 기존 2026-08-01 plan Task 4 bounded activation-marker reader를 완료한다.
SW-05shared build decoder와 기존 Task 5/SW-10protocol V2를 한 sequence로 구현한다.SW-06SW-09,WP-04WP-07을 protocol/lifecycle PR로 나눈다.- 제품이 Web Push를 선택할 때 별도 composition 계획으로 registry/provider/consent/browser evidence를 추가한다.
집중 검증:
corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts \
tests/unit/service-worker-build-input.test.ts \
tests/unit/web-push-codec.test.ts \
tests/unit/web-push-fence-store.test.ts \
tests/unit/web-push-store-port-compatibility.test.ts \
tests/unit/web-push-runtime-support.test.ts \
tests/unit/web-push-subscription-adapter.test.ts \
tests/unit/web-push-worker-runtime.test.ts
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm lint
git diff --check
완료 정의
- generated root-relative asset가 canonical absolute request와 일치하고, current cache 외 response가 반환되지 않으며 exact owned cache만 삭제된다.
- unregister/removal 결과가 실제 browser outcome을 숨기지 않는다.
- build gate가 static manifest row와 canonical set digest tamper를 거절한다.
- every command reply는 expected worker source, nonce, target full identity에 묶인다.
- fence mutation receipt가 exact next revision과 effect certainty를 보장한다.
- backend association receipt가 authority 3-tuple과 request digest에 묶인다.
- bounded truncation과 MAYBE_APPLIED native effect가 성공으로 과장되지 않는다.
- Web Push의 미조합 상태를 구현 완료로 오인하지 않는다.