Skip to content

Keep configured API paths out of PWA runtime caches - #2416

Merged
Chris0Jeky merged 19 commits into
mainfrom
issue-2411/pwa-api-base-cache-boundary
Sep 4, 2026
Merged

Keep configured API paths out of PWA runtime caches#2416
Chris0Jeky merged 19 commits into
mainfrom
issue-2411/pwa-api-base-cache-boundary

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes #2411

Summary

  • Normalize VITE_API_BASE_URL at build time and exclude both the default /api path and the configured API base from locale and static runtime-cache matchers.
  • Fail closed with a match-nothing predicate when the configured base is malformed or ambiguous.
  • Build the Workbox regular expressions from shared policy factories and prove the serialized worker with a real /assets/api build.
  • Version the retirement acknowledgement so an installed [Security][PWA] Remove authenticated API responses from the service-worker cache #2350 worker cannot satisfy the current policy or skip registration.update().
  • Invalidate taskdeck-static-assets during the security-policy activation, preserve the share-target queue, and reject activation when CacheStorage cleanup fails.

This keeps identity-bound API responses out of the shared runtime cache while preserving build-owned locale, image, icon and font caching after a safe worker activates.

Review fix

The first independent review found two HIGH blockers at 99a0acc6554f7a4b2460ac97f9be7179c7800684: the #2350 policy marker was still accepted, and configured-base entries could survive activation. The bounded fix round replaces that marker, clears the whole affected runtime cache, fails activation on cleanup error, and adds old-worker, worker-restart and stale-entry regressions.

Verification

  • PWA source and parity tests: 42/42 passed.
  • npm run test:pwa-generated-worker: 3/3 passed after a nested /assets/api build.
  • npm test -- --maxWorkers=2: 364 files passed; 5,411 tests passed; 3 skipped.
  • node --test --test-concurrency=1 --test-timeout=30000 scripts/ci/dev-up.test.mjs: 85/85 passed on Windows, including the case that timed out in the prior hosted run.
  • npm run typecheck: passed.
  • npm run lint: passed with 0 errors and 8 existing warnings.
  • Docs governance, golden principles, GitHub operations governance, ledger render synchronization and git diff --check passed.

Not verified

  • The real installed-browser [Security][PWA] Remove authenticated API responses from the service-worker cache #2350-worker-to-v2-worker journey did not execute. Both isolated servers became healthy, but the sanitized Playwright invocation idled before creating a browser child and was stopped after the bounded attempt. This is recorded in docs/agentic/failure_ledger.jsonl; this PR remains unmergeable until that transition is demonstrated.
  • Exact-head hosted CI and the final fresh-context review are running.

Docs

  • Updated docs/platform/PWA_OFFLINE_BEHAVIOR.md after PR Map the PWA cache boundary and fix a false-green in the seam map #2409 released its lease.
  • Corrected only the PWA row in autodoc/AGENT_INDEX.md because the shared build-time factories replace its stale inline-copy warning.
  • Added the required failure-ledger record for the unresolved browser-runner blocker.
  • No status, masterplan or testing-guide change.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T19:30:00.765436Z 2b98011 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Chris0Jeky Chris0Jeky moved this from Pending to Review in Taskdeck Execution Sep 3, 2026
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Independent review at 99a0acc6554f7a4b2460ac97f9be7179c7800684 found two confirmed HIGH blockers:

  1. The old [Security][PWA] Remove authenticated API responses from the service-worker cache #2350 worker returns legacy-api-cache-retired, which the page still accepts. That can skip registration.update() and leave the vulnerable installed worker controlling the page under registerType: prompt.
  2. Activation filters taskdeck-static-assets only by the generic asset path. Stale configured-base entries such as /assets/api/...png and /icons/api/...svg survive.

This PR is in its one bounded fix round. The repair will version the policy acknowledgement so #2350 cannot satisfy it, invalidate the affected static runtime cache fail closed, and extend deterministic plus installed-worker A-to-B proof. The head is not mergeable until the fix, current-base reproof, fresh exact-head review and CI complete.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fad3a06aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/taskdeck-web/tests/e2e/pwa-api-cache.spec.ts
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Current-base reconciliation pushed at 12dda19c9f28711f8d2ad571dfe56b373fd2b87b.

The new main delta (4f0f0525f7e789f3a68a77ebb4fdcad55dac5f96..eaa996fa2c70b13335d1628ccd66b33771fa6779) is confined to capture/backfill backend code and docs/platform/CONFIGURATION_REFERENCE.md; it has no overlap with the PWA repair. The merge was conflict-free.

Current-head proof:

  • npx vitest run src/tests/pwa/runtimeCachePolicy.spec.ts src/tests/pwa/apiCacheCleanup.spec.ts src/tests/pwa/legacyApiCacheWorker.spec.ts tests/runtime-cache-policy-parity.spec.ts --maxWorkers=2: 42 passed
  • npm run test:pwa-generated-worker: build passed; 3 generated-worker tests passed
  • node scripts/check-docs-governance.mjs: passed
  • node scripts/check-golden-principles.mjs: passed
  • node scripts/check-github-ops-governance.mjs: passed
  • git diff --check origin/main...HEAD: passed

The real installed-worker-to-new-worker browser transition remains NOT verified because the isolated Playwright run stalled before spawning a browser. This PR remains unmergeable pending that proof.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Final review-fix head: 65378f4e6ec60d81adad12f5e7db51144205bb19.

The late Codex finding was confirmed: the legacy worker replies with legacy-api-cache-retired, so the pre-sign-in control probe must assert true. The assertion and stale comment are corrected.

Focused proof:

  • npm run typecheck: passed
  • npx eslint tests/e2e/pwa-api-cache.spec.ts: passed
  • sanitized npx playwright test tests/e2e/pwa-api-cache.spec.ts --list --project=chromium: 2 tests discovered
  • git diff --check: passed

This does not close the existing verification blocker. The real old-worker-to-v2-controller browser execution remains NOT verified.

@Chris0Jeky Chris0Jeky moved this from Review to Blocked in Taskdeck Execution Sep 3, 2026
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Independent exact-head review receipt

Head: 65378f4e6ec60d81adad12f5e7db51144205bb19
Base: eaa996fa2c70b13335d1628ccd66b33771fa6779

No realistic CRITICAL/HIGH findings. The review confirmed the v2 marker in source and generated worker, full deletion of taskdeck-static-assets while preserving taskdeck-share-target, rejection of activation cleanup failures, replacement-worker tracking, and the corrected pre-sign-in old-marker assertion.

Residual review note: every worker activation flushes legitimate static runtime entries until fetched again. This is the intentional fail-closed security tradeoff for this repair; the app shell remains precached.

Hosted CI at this checkpoint: 28 of 34 checks completed successfully or intentionally skipped, 6 still running, 0 failed. The real installed old-worker-to-v2-controller browser transition is still NOT verified, so this PR is parked and must not merge.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Parked-state amendment for exact head 65378f4e6ec60d81adad12f5e7db51144205bb19: hosted Windows Frontend Unit failed in run 33718954936, job 100534019890.

The failure is the tracked source-launcher substrate defect, not a PWA regression. PowerShell: pipeline cancellation runs transactional cleanup from finally hit the helper's 20,000 ms boundary with spawnSync ... powershell.exe ETIMEDOUT at 20,077.778 ms; the suite finished 84 passed, 1 failed. The same case failed on the prior PR head near the same boundary, unchanged main has shown a different cleanup-state failure in this suite, and an earlier unchanged-main run passed all 85 tests. This PR changes no launcher, workflow, or package-graph files.

#2159 and #2161 plus the open ci/windows-source-launcher-cancellation-timeout failure-ledger record own the substrate repair. The #2416 restart contract now also requires that repair to land, followed by current-base merge, the focused Windows launcher suite, affected PWA checks, and Required CI. No rerun or new fix loop was started here.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Coordination note on the parked state. The conflict here is smaller than CONFLICTING suggests.

The PWA work does not conflict

Probed against current main 98f3fbd14 with git merge-tree --write-tree (no working-tree effect). The complete conflict set is two files:

  • docs/agentic/FAILURE_LEDGER.md
  • docs/agentic/failure_ledger.jsonl

Everything else auto-merges, including autodoc/AGENT_INDEX.md. No PWA source or test file conflicts: src/pwa/runtimeCachePolicy.ts, src/pwa/legacyApiCacheWorker.ts, public/api-cache-cleanup.js, vite.config.ts, the three src/tests/pwa/*.spec.ts, tests/pwa-generated-worker.spec.ts, tests/runtime-cache-policy-parity.spec.ts and tests/e2e/pwa-api-cache.spec.ts all merge clean.

So the restart is a bookkeeping reconciliation on two shared append-only files, not a re-derivation of the reviewed #2411 work. The 87-commit distance from the merge base is not itself evidence against this head.

What actually still blocks it

The Frontend Unit / Frontend Unit (windows-latest) red on run 33718954936 is the #2378 PowerShell launcher timeout, not this branch. Note the current shape of that dependency: #2378's repair is PR #2427, which is itself parked and holds .github/workflows/ci-required.yml, and that path is currently under an exclusive lease held by the active #2333 claim on PR #2412. So #2427 cannot restart until #2412 resolves, and this PR's Windows leg stays flaky until #2427 lands.

Ordering that follows from the above: #2412#2427 → this PR's clean Windows leg. The two-file bookkeeping merge can be done at any point and does not need to wait for that chain.

Nothing here re-opens the reviewed acceptance or asks for new work.

…se-cache-boundary

# Conflicts:
#	docs/agentic/FAILURE_LEDGER.md
#	docs/agentic/failure_ledger.jsonl

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b980111b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread frontend/taskdeck-web/public/api-cache-cleanup.js Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

[Claude lane claim v2 update]
lane: beta-platform-integrity
issue: #2411
PR: #2416
base: 350f595 (origin/main, merged clean)
exact head: 9250201 21a5212263839b4ea33b11057d77353

The runner blocker is gone; the browser proof now runs

The failure-ledger record test/playwright-pwa-preview-bootstrap is a runner
diagnosis that does not reproduce. Two of its three findings were measurement
artifacts:

  • "/api/boards readiness probe misclassified a live API" — /api/boards
    returns 401 when the backend is healthy. 401 is the healthy signal.
  • "the sanitized invocation idled before creating a browser child for more than
    seven minutes" — a playwright ... | tail -N pipe buffers all output until
    the process exits. I reproduced that exact appearance, then checked the process
    table mid-run and found live Chromium children. The run was never idle; only
    its output was withheld. Nothing was wrong with Playwright.

Bypassing the default webServer (self-started backend + vite preview on a
fixed origin, standalone config with no webServer), the gated spec runs:

TASKDECK_E2E_PWA_PREVIEW=1 npx playwright test tests/e2e/pwa-api-cache.spec.ts \
  --config playwright.pwa-proof.config.ts --project=chromium --reporter=line
  2 passed (15.1s)

Browser proof: the worker transition is demonstrated

A strict probe (controller must be non-null and answer v2, so it cannot pass
vacuously) confirms the journey this PR was parked on:

The same proof DISPROVES summary claim 5 — do not merge

Claim 5 ("Invalidate taskdeck-static-assets during the security-policy
activation") does not hold in a real browser. Seeding one entry under the old
worker and driving the real sign-in migration:

PROOF cachesBefore.staticEntries = ["http://localhost:4173/seeded-static-entry.png"]
PROOF cachesAfter.staticEntries  = ["http://localhost:4173/seeded-static-entry.png"]

Reproduced on three consecutive runs. Instrumenting a snapshot at the exact
controllerchange instant plus a 150 ms poll across the whole migration shows
hasStatic: true with the seeded entry present at every sampled instant —
so this is not a delete-then-repopulate race. caches.delete('taskdeck-static-assets')
never takes effect.

One caution for whoever picks this up: an emptied taskdeck-api-cache* is not
evidence that the worker's activate cleanup ran. src/pwa/legacyApiCache.ts
purges that namespace page-side during sign-in, so it clears either way. That
page-side purge is what makes the worker-side failure invisible to the existing
assertions. Correspondingly, "share-target queue preserved" is currently weak
evidence rather than strong: nothing in that activation was deleted at all.

This is the same HIGH the 2026-09-03T04:46Z review raised — stale configured-base
entries such as /assets/api/...png surviving an account switch. The unit tests
mock caches, so they pass while the shipped worker does not do this. That gap is
exactly what an installed-browser proof exists to catch.

Status

Pushed the current-base merge commit only. No ledger record was closed and no
"resolved" claim was made: the test/playwright-pwa-preview-bootstrap record
stays open, and its runner diagnosis should be corrected to the two
measurement artifacts above rather than deleted.

status: PARKED — worker transition demonstrated, but the static-cache
invalidation this PR claims is disproven in a real browser. Needs a fix round on
the activate-time invalidation (and an assertion that cannot be satisfied by the
page-side purge) before CI, review, aging and merge are meaningful.

requalification run this session: npm ci; VITE_API_BASE_URL=... npm run build
(exit 0, generateSW, 142 precache entries); backend dotnet run on :5000 with
an isolated SQLite file and Llm__Provider=Mock; tests/e2e/pwa-api-cache.spec.ts
2 passed; strict probe 3 runs, all reproducing the claim-5 failure.

…ivate

The generated worker loads api-cache-cleanup.js with importScripts() from inside
vite-plugin-pwa's asynchronous AMD define() factory, so the activate listener is
attached after the event has already been dispatched and never fires. Measured in
Chromium: a seeded taskdeck-static-assets entry survived the whole old-worker-to-v2
migration while the same file's message listener answered the policy handshake.

Run the sweep at evaluation time, deduplicated with the activate hook, and record
completion in a taskdeck-pwa-cache-policy-v2 marker cache so it stays a one-time
migration rather than a purge on every worker restart.
…vate event

The src/tests/pwa mocks dispatched activate themselves, so they passed while the
shipped worker never ran the sweep at all. Evaluate the emitted
dist/api-cache-cleanup.js against a fake CacheStorage keyed on the cache names
parsed out of the generated dist/sw.js, dispatching no activate event: reverting
the evaluation-time sweep turns this red. Update the mock-based specs to the
surfaced original error and the marker cache.
… regression

playwright.pwa-proof.config.ts has no webServer: the operator starts the backend
and a vite preview of a production build, which is what the
test/playwright-pwa-preview-bootstrap ledger record asked for. The strict spec
gates on TASKDECK_E2E_PWA_PREVIEW like the existing preview regression and pins
the two assertions pwa-api-cache.spec.ts left loose: a non-null v2 controller, an
emptied taskdeck-static-assets, and a preserved share-target queue.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

[Claude lane claim v2 update]
lane: beta-platform-integrity
issue: #2411
PR: #2416
exact head: a1d2d53

root cause

The confirmed HIGH was real, and the previous PR body's claim was false. Instrumenting the shipped worker in Chromium settled it: I wrote a breadcrumb into a dedicated cache at script-evaluation time and at each step inside the activate handler, and had the policy handshake report an in-worker activateFired flag. The controlling worker returned {"policy":"taskdeck-api-cache-policy-v2","__proofActivateFired":false,"__proofEvalAt":1788483040642} while the breadcrumb cache contained exactly one entry, /step-0-script-evaluated, with the same __proofEvalAt — and never 00-activate-entered. So public/api-cache-cleanup.js evaluated in the controlling worker and its message listener answered normally, but its activate listener never received the event and the sweep never ran on any instance. The mechanism is in the generated dist/sw.js: vite-plugin-pwa wraps the worker in an asynchronous AMD define() whose factory runs in a promise continuation (Promise.all(...).then(...)), and importScripts("api-cache-cleanup.js") sits inside that factory — so the listener is attached after the lifecycle event has already been dispatched. The unit tests dispatched activate themselves against a mocked caches, which is why they passed over a worker that does nothing. An emptied taskdeck-api-cache* was never evidence, exactly as flagged: src/pwa/legacyApiCache.ts purges that namespace page-side at sign-in.

fix

frontend/taskdeck-web/public/api-cache-cleanup.js only. The sweep moves into retireCachesOnce(), called both from the activate listener (kept, so the cleanup stays bound to activation on any build whose worker imports this file synchronously) and at script-evaluation time, sharing one deduplicated promise. Completion is recorded in a taskdeck-pwa-cache-policy-v2 marker cache — written last, and deliberately outside the taskdeck-api-cache prefix the sweep deletes — so this stays a one-time migration rather than a purge on every worker restart; a failed sweep is not memoised, so activation or a later restart retries. The share-target queue is untouched. No change to runtimeCachePolicy.ts or vite.config.ts was needed.

browser proof (strict probe, 3/3 consecutive, webServer-free config against a real backend on :5000 and vite preview on :4173)

  • before, under the old [Security][PWA] Remove authenticated API responses from the service-worker cache #2350 worker: markerBefore = legacy-api-cache-retired; keys ["taskdeck-api-cache-v2","taskdeck-static-assets","taskdeck-share-target"]; staticEntries ["…/seeded-static-entry.png"]; shareEntries ["…/seeded-share-entry"]
  • after, on the same head before the fix: markerAfter = taskdeck-api-cache-policy-v2 (v2 controller confirmed) but staticEntries still ["…/seeded-static-entry.png"] at the controllerchange instant and on every 150 ms poll — the reported defect, reproduced red
  • after, with the fix: keys ["taskdeck-share-target","taskdeck-pwa-cache-policy-v2","workbox-precache-v2-http://localhost:4173/","taskdeck-static-assets"], staticEntries [], shareEntries ["…/seeded-share-entry"], legacyApiCacheStillPresent [], and every latched snapshot (poll and controllerchange) shows staticEntries: []

requalification (all at the pushed head unless noted)

  • strict browser probe, 3 consecutive runs: 3/3 passed
  • tests/e2e/pwa-api-cache.spec.ts + strict probe, gated TASKDECK_E2E_PWA_PREVIEW=1, webServer-free config: 3 passed
  • npx vitest --run --maxWorkers=2 src/tests/pwa/: 5 files, 51 tests passed
  • npm run test:pwa-generated-worker: 5 passed. Red-check performed: removing only the evaluation-time sweep fails the new test with expected [ 'taskdeck-locale-chunks', …(3) ] to not include 'taskdeck-static-assets'
  • npm run typecheck: clean · scoped npx eslint over the changed files: 0 errors, 0 warnings
  • git diff --check: clean · node scripts/check-docs-governance.mjs: passed
  • py -3 -B scripts/agent_hooks/render_failure_ledger.py then py -3 -B -m unittest discover -s scripts/agent_hooks -p "test_render_failure_ledger.py": 11 tests OK

NOT verified: the full backend suite and the broad frontend suite (untouched by this change); real-device/other-browser behaviour — all evidence is Chromium via Playwright.

adopted into the repo: frontend/taskdeck-web/playwright.pwa-proof.config.ts (no webServer; operator starts the backend and a vite preview of a production build) and frontend/taskdeck-web/tests/e2e/pwa-proof-strict.spec.ts, gated on TASKDECK_E2E_PWA_PREVIEW=1 like the existing preview regression. These resolve the test/playwright-pwa-preview-bootstrap ledger record's stated future_fix; that record is now status: resolved — the runner blocker was an output-buffering artifact, not a stalled browser, and the real defect it was masking was the invalidation.

docs: docs/platform/PWA_OFFLINE_BEHAVIOR.md now documents the evaluation-time sweep, why the activate-only shape does not fire in the generated worker, and the marker cache.

status: fresh repair cycle round 1 complete; ready for exact-head CI, one fresh-context review, aging window, and coordinator merge

The evaluation-time sweep runs during install, while the old vulnerable worker is
still the controller and can still store an identity-bound response in
taskdeck-static-assets. Activation previously awaited the memoised sweep, which
had already resolved and whose marker short-circuited every later pass, so an
entry cached in that install-to-activation window survived the migration.

Activation now calls retireCaches({ force: true }), ignoring both the memo and the
marker, and still fails activation on error. The evaluation-time call keeps its
one-time marker semantics for builds where activate never reaches the listener.

Covered by two new cases that dispatch activate after a completed evaluation-time
sweep; reverting the forced sweep turns both red.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

[Claude lane claim v2 update — review round 1 dispositions]
lane: beta-platform-integrity
issue: #2411
PR: #2416
exact head: 1c5e9b3

HIGH — install-time sweep + memo + marker lets a poisoned entry survive: CONFIRMED and FIXED.

The finding was correct. I reproduced it in a real Chromium before touching the code: seeding taskdeck-static-assets from an init-script poller gated on "the install-time marker cache already exists AND a replacement worker is installing/waiting" left race-static-entry.png present after the migration (PROOF raceStaticEntries = ["http://localhost:4173/race-static-entry.png"]). Fixed in public/api-cache-cleanup.js: the sweep is now retireCaches({ force: true }) inside event.waitUntil on activate — it ignores both the memoised evaluation-time promise and the marker cache, still fails activation on error, still preserves the share-target queue and the Workbox precache, and still writes the marker last. The evaluation-time call keeps its one-time marker semantics for the build shape where activate never reaches the listener.

HIGH — proof: the e2e half is NOT delivered; the unit half is. Being straight about this rather than shipping something that looks like proof. I made three genuinely different attempts at the browser-level race case and could not place the seed deterministically inside the install-to-activation window from page JavaScript:

  1. updatefound + statechange listener in page context — never resolved; the sign-in navigation destroys the page context that holds the promise.
  2. init-script poller gated on a replacement appearing — fired too early, before the install-time sweep, so the sweep legitimately removed the entry and the test passed on the broken build (a false green).
  3. gated additionally on "marker exists" — this is the run that went red above, but the diagnostic I added showed it was seeding at {"installing":"installing","active":"activated","controlled":true}, i.e. during a later update cycle after the v2 worker had already activated. Entries written there are cached by the safe v2 worker and legitimately survive, so that red was not the vulnerability. Adding a controllerchange gate to close that hole made the seed never fire at all: the real window is shorter than a 10 ms page-JS poll.

So I removed the e2e race case rather than commit a test whose red/green does not track the defect. The HIGH is instead pinned deterministically in tests/pwa-generated-worker.spec.ts, exactly as you also asked: re-sweeps at activation even after the evaluation-time sweep wrote the marker evaluates the real emitted dist/api-cache-cleanup.js, lets the evaluation-time sweep complete and write the marker, re-seeds taskdeck-static-assets plus taskdeck-api-cache-v2, then dispatches activate and asserts both are gone and the share queue survives; fails activation when the forced sweep cannot complete asserts activation still rejects. Red-check performed: reverting only retireCaches({ force: true }) to retireCachesOnce() turns both red (expected [ Array(4) ] to not include 'taskdeck-static-assets' and promise resolved "undefined" instead of rejecting). A browser-level regression for this specific window remains not verified — flag it if you want it tracked as an issue.

MEDIUM — marker name uncoupled from the policy constant: FIXED (~10 lines). New test keeps the migration marker versioned with the policy handshake constant reads the taskdeck-api-cache-policy-vN constant from src/pwa/legacyApiCacheWorker.ts and the taskdeck-pwa-cache-policy-vN marker from the emitted dist/api-cache-cleanup.js and asserts the version suffixes match, so a v3 bump fails loudly.

LOW — runner scope: FIXED. playwright.pwa-proof.config.ts now sets testMatch: ['pwa-api-cache.spec.ts', 'pwa-proof-strict.spec.ts'].

LOW — docs sentence: FIXED. docs/platform/PWA_OFFLINE_BEHAVIOR.md now describes the real two-part mechanism: a one-time evaluation-time (install) sweep guarded by the marker, plus an unconditional forced re-sweep at activation that ignores the memo and marker and rejects activation on failure, with the reason (the old worker still controls the page during install).

requalification at this head

  • strict probe, 3 consecutive runs: 3/3 passed
  • full gated preview suite (pwa-api-cache.spec.ts + strict, TASKDECK_E2E_PWA_PREVIEW=1, webServer-free config): 3 passed
  • npx vitest --run --maxWorkers=2 src/tests/pwa/: 5 files, 51 tests passed
  • npm run test:pwa-generated-worker: 8 passed, with the red-check above
  • npm run typecheck: clean · scoped npx eslint over the changed files: 0 errors, 0 warnings
  • git diff --check: clean · node scripts/check-docs-governance.mjs: passed

Two src/tests/pwa assertions were updated because the sweep now legitimately runs twice (evaluation + forced activation); they now pin that explicitly — the unique deleted set is unchanged, taskdeck-static-assets is deleted exactly twice, taskdeck-share-target is never deleted, and the only cache ever opened is the marker.

Operator note worth having: npm run test:pwa-generated-worker rebuilds dist with VITE_API_BASE_URL=/assets/api. Running it before the preview e2e leaves a build whose sign-in POSTs 404, which looks like a product failure and is not. Always npm run build again before pointing vite preview at dist.

NOT verified: full backend suite and broad frontend suite (untouched); non-Chromium browsers; a browser-level regression for the install-to-activation window specifically.

status: review round 1 addressed; ready for exact-head CI, the aging window, and coordinator merge. Not merged, CI not polled.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Fresh repair-cycle review receipt (round 1 at a1d2d53, fix round verified at 1c5e9b3).

Verdict after the fix round: no CRITICAL or HIGH finding. Merge-blocking: none. Round count in this repair cycle: one review round, one fix round. Earlier cycles: the original two rounds under the Codex lane, then the park.

Repair-cycle findings and disposition:

  • HIGH, fixed: the install-time sweep ran while the old worker still controlled the page and the activate path reused the memoised result, so an identity-bound response could be re-cached into taskdeck-static-assets between the sweep and activation and never re-swept. The activate listener now calls a forced sweep inside event.waitUntil that bypasses both the memo and the marker short-circuit, awaited before clients.claim(), and still rejects activation on error. Reproduced red in a real Chromium with a lifecycle-gated seed before the fix; pinned by two generated-worker tests that run the emitted dist/api-cache-cleanup.js against a fake CacheStorage with the real cache names and fail when force is ignored.
  • MEDIUM, fixed: the marker version suffix is now coupled by test to the taskdeck-api-cache-policy-vN constant in src/pwa/legacyApiCacheWorker.ts, so a future policy bump fails loudly.
  • LOW, fixed: the webServer-free Playwright config is restricted by testMatch to the two PWA specs; the offline-behavior doc describes the real mechanism (one-time install sweep plus unconditional forced activation re-sweep).

Fix-diff verification (by trace against the emitted dist/sw.js): the sweep deletes only the taskdeck-api-cache prefix and the exact taskdeck-static-assets; taskdeck-locale-chunks, taskdeck-share-target, and the workbox precache match neither predicate; the two sweeps are idempotent deletes over the same names with independent rejection paths, so no re-poisoning or swallowed-failure race exists between them. Worker-run proof at 1c5e9b3: strict probe 3/3, gated preview suite 3 passed, src/tests/pwa/ 51/51, generated-worker suite 8/8 with red-check, typecheck, scoped lint, diff check and docs governance clean.

Tracked, not fixed here: a browser-level Playwright regression for the install-to-activation window could not be made reliable in three attempts and is tracked in #2475. LOW, declined: a stale return-type annotation and an any widening in the frontend-root test file (excluded from typecheck by design), and a duplicated comment.

The original ledger record for the runner blocker was resolved in this PR: the "idle Playwright" symptom was output buffering from a | tail pipe, not a runner defect.

Remaining gate: exact-head hosted ci-required green and the three-minute aging floor, then a merge commit. Closes #2411 on merge.

@Chris0Jeky
Chris0Jeky merged commit bb65732 into main Sep 4, 2026
35 checks passed
@github-project-automation github-project-automation Bot moved this from Blocked to Done in Taskdeck Execution Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Security][PWA] Static-asset runtime cache admits API responses when VITE_API_BASE_URL is nested under /assets/ or /icons/

1 participant