0008. Requirement conflicts and resolutions
- Status: Accepted (living document: append, never rewrite)
- Date: 2026-10-18
Context
The project's requirements sometimes pull against each other. The priority order is fixed: security > correctness > user privacy > confidentiality > tests and coverage > performance > code style. Every conflict and how we resolved it is recorded here.
Log
| # | Date | Conflict | Resolution | Winning priority |
|---|---|---|---|---|
| C1 | 2026-10-18 | "Latest stable everything" vs. a working toolchain: TypeScript 7.0.2 is outside the peer ranges of typescript-eslint 8.71.2 (<6.1.0) and typedoc 0.28.20 (6.0.x). | Pin TypeScript 6.0.3, the newest version every peer accepts. Adopt 7.x when the peers do (Dependabot will propose it). | Correctness |
| C2 | 2026-10-18 | Packaged builds must be smoke-tested against a fake WhatsApp page, but a configurable WhatsApp origin in a release build is a redirection risk. | The override YAPYAP_TEST_WHATSAPP_ORIGIN is honored only for loopback http://127.0.0.1 / http://localhost origins and is logged when used. | Security |
| C3 | 2026-10-18 | 100% coverage vs. process entry files that only run inside Electron. | Entry files contain no branches (enforced by lint) and are listed with justification in ADR 0005. E2E exercises them. | Tests |
| C4 | 2026-10-18 | "Required status checks" vs. release-please PRs opened with GITHUB_TOKEN, which don't trigger workflows. | Use an AUTOMATION_TOKEN (GitHub App or fine-grained token) when present. Until then, the maintainer re-runs checks. Tracked as a needs-maintainer issue. | Correctness |
| C5 | 2026-10-19 | The repository's default branch is master, but the workflow text says main. | The maintainer chose to keep master. Every rule written for main applies to master. | Maintainer decision |
| C6 | 2026-10-19 | Deny-by-default display capture vs. users' need to share their screen in WhatsApp calls. | Capture is granted only to a WhatsApp frame that is visible and focused (an account view or its call window), and only for a source the user picks in an OS or YapYap picker. The picker choice is the consent. We do not also require a page gesture, because WhatsApp may call getDisplayMedia asynchronously from inside a call (ADR 0001). | Security, with a consent-gated exception for correctness |
| C7 | 2026-10-18 | Extensions need network access for AI providers, but direct renderer fetch to local servers often fails CORS, which pushes authors toward unsafe workarounds. | The SDK offers a host-mediated fetch that runs on the extension's own session, so the same webRequest allowlist still applies. Direct requests remain allowlist-limited. | Security |
| C8 | 2026-10-18 | macOS and Windows self-update require paid signing identities that may not exist. | Without the signing secrets, macOS builds fall back to the notify update policy and Windows builds still self-update with SHA-512 verification. Tracked as needs-maintainer issues. | Security |
| C9 | 2026-10-18 | Custom JS injection is required, but WhatsApp's main world must not run user code that could exfiltrate across accounts. | User JS runs only in a dedicated isolated world, per account, off by default, and labelled Advanced. | Security |
| C10 | 2026-10-18 | The repository variable EXTENSION_SIGNING_PUBLIC_KEY is an SPKI PEM (BEGIN PUBLIC KEY), while the spec calls the keys "PKCS#8 PEM". | PKCS#8 applies to the private key (the extension-sign CLI accepts PKCS#8 PEM). Public trust anchors are SPKI PEM, the standard form for Ed25519 public keys. | Correctness |
| C11 | 2026-10-19 | Automation must open PRs whose commits satisfy the signed-commits rule, but CI cannot hold a signing key and a plain git push from Actions is unsigned. | Automation commits through GitHub's createCommitOnBranch GraphQL mutation, which GitHub signs. The same AUTOMATION_TOKEN serves release-please and the pinned-versions workflow. | Security |
| C12 | 2026-10-19 | Mutation testing must use Stryker, but @stryker-mutator/vitest-runner@10.0.0 does not activate mutants under Vitest 5: every non-static mutant ran zero tests or survived, which produced a meaningless score. | Use Stryker's command runner with a flat Vitest config, which activates mutants through the environment. It is slower but correct. PRs mutate only changed in-scope files, and nightly runs the full scope. Revisit when the runner extension supports Vitest 5. | Correctness |
| C13 | 2026-10-19 | OpenSSF Scorecard's Code-Review and Branch-Protection checks want approving reviews (with stale-review dismissal, code-owner review and last-push approval) on every change, but YapYap has a single maintainer, and GitHub does not let authors approve their own PRs. | The master ruleset requires PRs, signed commits, resolved review threads, up-to-date branches and every CI check, but zero approvals. CI, CodeQL, mutation testing and package smoke tests act as the reviewer. The two Scorecard alerts are dismissed with a link to this row. Turn on required reviews when a second maintainer joins. | Correctness (merges stay possible), with security kept by required checks |
| C14 | 2026-10-20 | ADR 0001 asked for will-frame-navigate to keep every frame on the view's home origin, but WhatsApp Web plays some link previews inline in third-party iframes, which that rule would break. | Only main-frame navigations are policed. Subframes stay confined by WhatsApp's own CSP, the renderer sandbox and site isolation, they cannot open windows, since setWindowOpenHandler and the link policy apply to every frame, and the permission handler refuses external-app launches from any frame (#29). | Correctness, with security kept by the sandbox and the link policy |
| C15 | 2026-10-20 | Screen sharing must work, and the user must always pick the source. On macOS 15+ the system picker guards getDisplayMedia, but the legacy desktop getUserMedia reaches Electron as the same device-less media request and captures without any picker once it is granted. | On macOS 15+ the device-less request is granted, because denying it would break every call's screen sharing. The legacy path there still needs the macOS Screen Recording permission and shows the OS recording indicator, and it is open only to WhatsApp's own origin in a visible, focused window. Everywhere else the user picks before anything is granted (ADR 0001, #146). | Correctness (calls work), with security kept by the OS permission and indicator |
| C16 | 2026-10-21 | Spell checking is expected in a messaging app, but Chromium fetches a dictionary for the UI language from a Google CDN whenever a window opens, which leaks the language and the IP address and breaks the no-telemetry promise. | Every session gets an empty spellchecker language list, the only setting that stops the fetch, so spell checking is off. It returns in #73 with languages the user picks and dictionaries that are bundled or fetched only with consent. | Privacy over a convenience feature |
| C17 | 2026-10-21 | The dependency review denies (L)GPL licenses, but the official SonarQube Cloud scan action, needed for the analysis in ADR 0010, is LGPL-3.0. | The deny list exists to keep YapYap's distributed code MIT-compatible. A GitHub Action runs only in CI and is never linked into or shipped with YapYap, so the review allows exactly pkg:githubactions/SonarSource/sonarqube-scan-action, with a comment pointing here. Everything that ships keeps the full deny list. The optional LGPL-3.0 image libraries (@img/sharp-libvips-*) that wrangler brings to the packages.guita.org deploy (#404) are allowed the same way: they run only in CI, and a Pages upload never even loads them. | Correctness, with the license policy kept for everything distributed |
| C18 | 2026-10-21 | C4 and C11 relied on a long-lived AUTOMATION_TOKEN. A personal token acts as the maintainer for up to a year, which is more access for longer than the automation needs. | Release-please and the pinned-versions job mint an hour-long token from a dedicated GitHub App, limited to this repository and to contents, pull requests and issues. GitHub signs the app's commits. Without the app configured, both fall back to the workflow token, as before. | Security |
| C19 | 2026-10-22 | The memory watchdog (#37) should keep the composer draft across a reload, but saving it means the adapter reading message text out of the page, which ADR 0001 forbids (probes return flags, never page text). | The draft is never read. The watchdog waits while the user is focused and active in the window, so it never reloads mid-sentence, and WhatsApp keeps per-chat drafts itself. Only a renderer past one and a half times the limit reloads regardless. | Privacy, with correctness kept by deferring the reload |
| C20 | 2026-10-23 | Do not disturb and per-account mute must silence WhatsApp's in-page sounds, but those sounds share the page's audio with the voice messages, videos and calls the user plays, and Chromium can only mute a page's audio as a whole. | A silenced account's page is muted only while it is not on screen (hidden, minimized or another account shown). The account on screen keeps its sound, so whatever the user plays still works; its notifications stay off. Calls open in their own window, which keeps its audio. | Correctness, with the quiet kept wherever the user is not looking |
| C21 | 2026-10-23 | ADR 0001 asked setWindowOpenHandler to deny every new window, but WhatsApp Web's calls run in popups that must share the account's session; opening them in the browser or destroying them breaks calls. | Only popups that stay on WhatsApp's own origin become windows, on the opener's session, with the hardened preferences and the opener's zone, so every guard and permission rule applies to them as to the account. Everything else still goes through the link policy, and the windows close with their account (#67). | Correctness, with security kept by the same guards and session isolation |
| C22 | 2026-10-23 | A reply typed in a notification should be sent, but YapYap cannot confirm which chat WhatsApp opened without reading page text, which ADR 0001 forbids, so sending it could deliver a message to the wrong chat. | The reply goes into the opened chat's message box as a draft and is never sent by YapYap; the user sees the chat and presses Send. Sending stays the user's explicit action in WhatsApp's own composer, as #60 requires. | Correctness and privacy over a one-step reply |
| C23 | 2026-10-24 | A manual proxy must carry all of WhatsApp's traffic, but WebRTC calls normally reach the other side over direct UDP, which bypasses the proxy and reveals the real address to the other party and the relays. | With a manual proxy, account views use WebRTC's disable_non_proxied_udp policy: calls go only through the proxy, or through WhatsApp's relays over it, and may connect more slowly or fail when the proxy cannot carry them. Without a manual proxy, views use default_public_interface_only, which keeps local addresses private. | Privacy over call quality |
| C24 | 2026-10-24 | #73 asks for the system language to be selected on first run, but on Windows and Linux selecting a language makes Chromium download its dictionary from Google, which C16 allows only with consent. | Spell checking is off until the user turns it on in Settings, whose description says a dictionary is downloaded from Google. The system language is used once it is on, unless the user chose others. The context menu's Check Spelling… opens Settings instead of turning spelling on, so the user always sees that notice first; turning it off from the menu applies at once. | Privacy over a feature on by default |
| C25 | 2026-10-24 | #98 asks for extension panels inside a YapYap frame showing the extension's name and icon, but a frame YapYap draws beside WhatsApp in the main window puts extension pixels next to WhatsApp's, where a hostile panel could imitate its UI, and the manifest has no icon. | A panel opens in a window of its own, on the extension's session and under its CSP, with no preload, so it gets no port. Its title is <extension name>: <panel title>, set by YapYap, and the page cannot change it. The OS frame is the frame. Icons wait for a manifest field that YapYap can validate. | Security |
| C26 | 2026-10-24 | Extensions with chat:read-active must read the open chat's messages (#100), but ADR 0001 has the adapter's probes return flags, never page text, so chats stay out of YapYap's own code paths. | A separate reader returns the open chat's name, whether it is a group, and its last messages' text, as plain data. It runs only on an extension's call, only for an extension holding the grant for the account on screen, never on a timer or observer. It returns at most the number of messages the user set in Settings (20 by default; never imported from a file), checked against a schema in the main process, and it is never logged; the audit log records only that the call happened. | Correctness (the capability works), with privacy kept by the grant, the cap and no logging |
| C27 | 2026-10-24 | The AI assistant must call providers with the user's own key from its extension page (#103), but the official SDKs refuse to run in a browser unless dangerouslyAllowBrowser is set, since a web page usually exposes its key to visitors. | Set it. The key is the user's, kept encrypted by the OS through host.secrets, and used only inside the extension's sandboxed page, which nobody else can load or script; the extension CSP and network allowlist still bound where it is sent. The maintainer chose the official SDKs over hand-written clients (2026-10-24). | Correctness (vendor-maintained streaming and errors), with security kept by the sandbox |
| C28 | 2026-10-25 | Rewriting a draft (#105) needs the extension to read what the user is typing, but the draft is unsent message content, and composer:insert-draft only lets an extension write. | A capability of its own, composer:read-draft, granted per account like the chat readers, so an extension can write drafts without reading them. The adapter's reader returns the composer's plain text (at most 64 KiB) only on such a call, the main process checks it, and nothing logs it. Rewriting replaces the draft by selecting the composer's text and typing over it, as the user would; nothing is sent. | Privacy, with correctness kept by a separate grant |
| C29 | 2026-10-25 | The AI assistant's local-only mode (#103) must reach a model server on the user's computer, such as one on http://localhost:11434, but extensions could reach only declared https:// hosts, and the manifest schema accepts no localhost. | A capability, local-network, shown at install as connecting to servers on this computer. With it, the extension CSP and network filter allow HTTP or HTTPS to loopback only (localhost, 127.0.0.1, [::1]), any port, without credentials; no other address on the local network. Loopback can also reach other local services, which is why it is a grant the user sees and can take back, never implied by a host. | Correctness (local models work), with security kept by the grant and loopback only |
| C30 | 2026-10-25 | Calls need the microphone and camera only after the user agrees (ADR 0001), but Electron reports a refused permission check to the page as denied, never as "not asked yet". WhatsApp checks before a call and, seeing denied, never asks, so v0.1.0 users could not call at all. | The check answers yes for what YapYap asks about (media, WhatsApp's own origin only); the request still asks the user, once per account and device, and refusing it fails the call. A page that checks as allowed can also read the names of the cameras and microphones before the user agreed, which WhatsApp, the user's own messenger, may now see. | Correctness (calls work), with device access still behind the user's answer |
| C31 | 2026-10-26 | The maintainer wants AI suggestions inside the chat and a one-click way to send them (#407), but ADR 0004 keeps extensions out of WhatsApp's page and gives them no way to send a message, so a hostile extension can neither imitate WhatsApp nor message people. | YapYap, not the extension, draws an assistant bar beneath WhatsApp's view, and shows an extension's suggestions in it as plain text under the extension's name (ADR 0013). Only YapYap's own Send button sends, once per click; extensions still only offer text. | Correctness (the feature the maintainer asked for), with security kept by host-drawn UI and sending only on a click |
| C32 | 2026-10-27 | People want to record WhatsApp calls (#392), but a recording holds other people's voices, may be illegal without everyone's consent, and an extension able to record would be a wiretap; ADR 0001 also keeps YapYap out of WhatsApp's page. | YapYap records, never the extension: a hidden recorder captures only the call window's own audio and the microphone, never the system's sound, and only after the user clicks Record in a bar YapYap draws in the call window. The user confirms they know consent laws before the first recording; a red mark with Stop recording shows in the call window, the sidebar and the tray; recording stops with the call; files stay local, named by date only, and extensions never get audio or paths (ADR 0015). | Security and user privacy, then correctness (the feature exists, under these limits) |
| C33 | 2026-10-29 | Start at login must work in the Flatpak (#234), which only the Background portal can grant, over D-Bus; but YapYap opens no local endpoint (ADR 0004, #94), and a test forbade importing node:net at all. | YapYap connects out to the session bus with createConnection from node:net, a client of the bus as every desktop app is, for the duration of one portal request. It listens on nothing: the test still rejects any server API, and allows exactly that one client import. | Correctness (autostart works in the Flatpak), with security kept: nothing can connect to YapYap |
| C34 | 2026-10-29 | The AI assistant must reach a model server on the user's computer (#103), but Ollama refuses any request whose Origin it does not list (#430): its defaults name only localhost-style web origins and a few app schemes, never extension://, and apps such as Alpaca let users set none. Local servers keep that check against web pages, and lifting it for every loopback service would let an extension use them all. | Each server on this computer (host and port) must be approved by the user for the extension, through the same prompt as a new host, on top of local-network. Requests to an approved server leave without an Origin, and YapYap answers CORS for them itself; every other local service stays unreachable. The maintainer chose this over documenting OLLAMA_ORIGINS and over lifting the check for all loopback servers (2026-10-29). | Correctness (local models work out of the box), with security kept by the grant, a per-server approval and loopback only |
Consequences
Each new conflict gets a row here in the same PR that resolves it.