Taqvim is a Persian calendar for Chrome. It makes no network requests, and a calendar has no reason to. The holidays, the fonts and the conversion between calendars all fit inside the package.
The first public versions did not work that way. Versions 0.1.2 and 0.1.3, released in May 2026, loaded the Persian font, Vazirmatn, from Google Fonts. Both stylesheets pulled it in with an @import of a Google Fonts URL, and the manifest's content security policy allowed fonts.googleapis.com and fonts.gstatic.com so that the import would work. Opening the popup could send a request to Google, while the README said the extension used local data only and no external APIs. Nothing about it looked wrong, because the font loaded and the popup rendered.
The fix, and the test that holds it
Version 0.1.4, in July, bundled the font inside the extension with its open font licence, about 110 KB, and cut the policy back to the extension's own files:
script-src 'self'; object-src 'self'; style-src 'self'; font-src 'self';
The same commit added a unit test that reads the two stylesheets and the manifest and fails if any of them points at a server:
assert.doesNotMatch(popupCss, /@import|https?:\/\//i);
assert.doesNotMatch(optionsCss, /@import|https?:\/\//i);
assert.doesNotMatch(manifest.content_security_policy.extension_pages, /https?:\/\//i);
assert.match(popupCss, /Vazirmatn\.woff2/);
assert.match(optionsCss, /Vazirmatn\.woff2/);
The last two lines matter as much as the first three. Removing the remote font is only half the fix if nothing checks that the local one is still in use.
Three places, three moments
The rule is checked in three places, because each one runs at a different moment.
- The unit test runs in GitHub Actions on every push and pull request.
- The smoke test, which also checks the holiday data, the icons and the manifest, runs its own check on the policy, so a bad policy fails locally before anything is pushed.
- The packaging script refuses to build the release zip if a stylesheet or the policy contains a remote URL. Even a release built with the tests skipped cannot ship the old problem.
The packaging script also builds the zip from an explicit allowlist of files, so tests, scripts and repository files never end up in what the Chrome Web Store serves.
The gap the first fix left
The 0.1.4 policy limited scripts, styles and fonts and set no default for anything else, so a fetch() or an image pointing at a server would still have gone out. Every fetch() in the extension loads one of its own data files through chrome.runtime.getURL, and the pages load no outside images, so nothing used the gap, but only reading the code showed that.
In October the policy gained two more directives. connect-src 'self' covers fetch() and similar requests, and default-src 'self' covers the other kinds of resource a page loads, such as images, media and frames. The test now fails if either is missing. I checked both in Chrome with the extension loaded. Under the old policy a fetch() to an outside server was sent and an outside image loaded. Under the new one Chrome refuses both, and the popup and the options page render as before. Neither directive covers navigation, such as following a link or submitting a form. The extension's pages have no outside links, and neither of their two forms points at a server. The changes reach users with the next release.
Why bother for a calendar
A privacy promise in a README or a store listing is easy to write and easy to break by accident. The May versions broke it through a font, a change nobody would have noticed by using the extension. A promise like that needs something that fails the build when it stops being true, because a reviewer reading a diff will not always spot one @import line.
The code is public. The test is short, and the case study covers the rest of the extension.