Repository navigation
First release of ichava/icon-sets-sync as a single published commit. The 0.1.x tags cut between 2026-10-23 and 2026-10-26 (v0.1.0 to v0.1.2, with their src/ module aliases) are folded into this release, and their notes are kept below under Archived history.
The Go module proxy and checksum database already hold github.com/ichava/icon-sets-sync/src at v0.1.0, v0.1.1 and v0.1.2, fetched from commits before the single-commit publication, and neither can be cleared. So go install github.com/ichava/icon-sets-sync/src/...@v0.1.0 serves that older code, and @latest the older v0.1.2. The supported installs are the signed release binaries and the Composer package; go install is not offered.
Added
-
Integrations: a REST API, an event bus with an event stream, webhooks and an MCP server, every one off by default (owner, 2026-11-12, one pull request). A fresh install behaves exactly as before; each surface is a setting in a new Integrations card on the Settings page, and
doctorsays which are on and warns about each risky combination. See docs/tools/api.md, events.md, webhooks.md and mcp.md.- Events.
internal/eventsis an in-process bus of CloudEvents 1.0 envelopes (JSON format, ULID ids) with eleven v1 types:job.started,job.progress(stream only),job.finished,pack.synced,pack.behind_upstream,upstream.checked,config.changed(names the key, never the value),server.started,server.stopping,webhook.disabledandwebhook.test. Each type'sdatahas a JSON Schema embedded in the binary.TestEveryTypeHasASchemafails on a type without a schema or a schema without a type,TestEveryTypeHasAPublisheron a type no production code publishes, andTestRunEventsMatchTheirSchemasvalidates every event a run emits. The job registry gains publishing hooks; the run page's per-job stream is unchanged. - REST API under
/api/v1on theservelistener (api.enabled; beyond loopback alsoapi.allow_remote), 22 endpoints with an OpenAPI 3.1 document thatTestOpenAPIMatchesTheRoutesholds to the routes declared inapiRoutes, both ways and scope by scope. Bearer tokens only, with scopesread,sync,config:writeandadmin:icon-sets-sync api token create|list|revokeshows a token once and keeps only its SHA-256 inapi-tokens.jsonin the state directory (mode0600), compared in constant time, with expiry, revocation and a recorded last use. The session cookie does not open/api/and a bearer token opens no page. RFC 9457 problem documents, a 1 MiB body cap, a per-token rate limit (api.rate_limit, 60 a minute),Idempotency-Keyon the sync endpoints, an audit line per write inapi-audit.jsonl, anddry_rundefaulting to true.--read-onlyregisters no write endpoint.GET /api/v1/eventsstreams the bus as Server-Sent Events whenevents.streamis on. The UI and the API now start runs through oneweb.StartSync, and save settings through oneweb.SaveGlobalSettingwith the Settings page's checks (web.ParseSettingValue). - Webhooks (
webhooks.enabled, endpoints inwebhooks.endpoints, each with a required event filter).icon-sets-sync webhooks add|list|remove|rotate|test; secrets are generated (whsec_and 32 random bytes), shown once and kept inwebhook-secrets.json(mode0600), with an optionalsecret_envoverride, and a rotation keeps the old secret signing for 24 hours, so both signatures are sent. Standard Webhooks headers and HMAC-SHA256 signing (checked against the specification's test vector). Every address is checked when it is dialled (RFC 1918, loopback, link-local and metadata, CGNAT, ULA, unspecified, and the IPv6 prefixes that embed IPv4) unlesswebhooks.allow_private; https only (plain http only to localhost withwebhooks.allow_insecure_localhost), no proxy, no redirects, a 10 s timeout and a 256 KiB cap. A bounded queue (1,000), retries after 1 m, 5 m, 30 m, 2 h and 6 h, a rotatedwebhook-deliveries.jsonl, and auto-disable after 50 consecutive failures with awebhook.disabledevent. - MCP.
icon-sets-sync mcpserves the Model Context Protocol on stdio with the official Go SDK,github.com/modelcontextprotocol/go-sdkv1.8.0 (mcp.enabledor--enable). Eight read tools by default;sync_pack,cancel_jobandset_settingonly withmcp.allow_writesand never under--read-only, and a real sync needsconfirmequal to the slug and is refused while aserveruns on the machine. - Security guards, each proven red before it was kept: a token without the scope is refused; an expired or revoked token is refused; a webhook to a private address is refused at connect time; a bad signature does not verify; the write tools are absent under
--read-onlyand withoutmcp.allow_writes; every integration is off on a fresh config; a non-loopback bind defaults to TLS.
- Events.
-
A Windows launcher that opens the web UI with no console window.
make crossbuildsicon-sets-sync-launcher_windows_<arch>.exe(amd64, arm64, 386) beside each Windows binary. It is a separate program (src/cmd/icon-sets-sync-launcher) linked for the GUI subsystem (-H=windowsgui), so Windows opens no console for it: it finds the CLI beside itself, starts itsservedetached withICHAVA_DESKTOP_LAUNCH=1, and exits, and if the CLI is missing it shows an error box naming the paths it tried. A double-clicked CLI still flashes a console for under a second; the CLI itself is unchanged, so it behaves normally incmdand PowerShell (owner, option A, 2026-11-11). Like the macOS app and the Linux.desktopentry, it is not a GoReleaser build or a release asset.TestWindowsLauncherIsAGUIProgramAndTheCLIIsNotreads both PE headers (proven red both ways); the build-layout guard names the three launcher files;TestOnlyOneEntrypointallows exactly this secondsrc/cmddirectory. -
Settings are edited in place on the Settings page. Each effective setting has the control its type calls for (a menu, a switch, a bounded number, a list, a path or a multi-line field), a Save that writes it to the global config and a Reset that removes it from there, and says beside it when an
ICHAVA_*variable or a flag overrides the saved value. Values are checked as they are typed by the same code as the save (POST /settings/value/<name>/check), and the save adds checks the loader does not make: directories and the npm path must exist,allowed_hostsmust be bare host names, branch names must be ones git accepts, and lists may not repeat an entry. A refused save keeps what was typed and says why beside the field. -
Pagination on every list, with a
page_sizesetting. Packs, runs, audit results and a run's log showpage_sizerows (default 25, 5 to 500,ICHAVA_PAGE_SIZE) with a Per page menu and page links that keep any filter in the address. Settings gain aMaxbound, used bypage_sizeand carried intoglobal.schema.json. -
The global config starts initialised, and can be reinitialised.
serveand the newconfig initwrite it from what the machine has:$schema,packages_dirs(the directory of pack checkouts this tool sits in) andprivate_config_dir(when exactly one pack carries one). A missing,$schema-only or still-generated file is (re)written; one a person edited is left alone, told apart by a record of what was last written (global-init.jsonin the state directory). The global config page says which it is and has Reinitialise (a confirmed action;config init --forcein the CLI), which keeps the old copy in the history. Read-only servers and CI runs write nothing. -
Recipes for the 34 v0.1.0 packs and sets.
config/gains Lucide, Heroicons, Bootstrap Icons, Simple Icons, Material Design Icons, Phosphor, Font Awesome Free, Remix Icon (pinned to v4.8.0, the last Apache-2.0 release), Material Symbols, Fluent, Ionicons, Carbon, Octicons and Iconoir, and one config per set oficon-sets-extras(20, each recording its version atupstream.sets.<set>.current_versionin the pack). All 38 registered configs validate, and a dry run of each new one at its pinned version against the local pack checkout reproduced the committed SVGs byte for byte (0 added, removed or changed, 2026-11-04).TestEveryShippedRecipeValidatesloads and builds every registered config and fails below 38, or on a config filepacks.jsondoes not register. -
split-variantstransform. Sorts upstream files into one directory per variant by the directory they come from (from), the suffix they carry (suffix, stripped), or both; unclaimed files are dropped and counted, which is how a pack takes a subset, andflatten_prefixkeeps sub-folders as a name prefix. It covers Heroicons, Phosphor, Material Symbols, Fluent, Ionicons, Carbon and Octicons. A variant that claims nothing, or two names reducing to one, fail the run. -
excludetransform. Drops icons by stem (names, Octicons' GitHub marks), by directory (directories, Game Icons' unlicensedbadges/) or from an upstream data file (metadata, Simple Icons' non-CC0 logos fromdata/simple-icons.json, so the list follows each release). An excluded item that cannot be named fails the run. -
More transform options.
slugifytakesdirs(slug directory names too: Remix'sGame & Sports) andhyphenate_dots(lucene.net->lucene-net), and now fails when two names slug to one instead of renaming one over the other.flatten-variantstakesslug, which slugs each stem before the affix (Health Icons'!.svgis committed as-filled.svg).sanitisetakesdrop_unparsable, which removes a file the policy cannot parse instead of leaving it unchecked (Noto Emoji's 170 SVGs with an Illustrator DTD). -
Licence guards for every licence the packs carry.
licencenow admitsApache-2.0,CC-BY-3.0,CC0-1.0,ISCandMITbeside the two CC-BY ids, each checked against its opening lines (a Markdown#is ignored). The newlicence_sha256pins the upstream licence text to the byte, the pack plan's licence-change guard and the only check for a licence with no standard text; every new config pins one where upstream ships a licence.licence_setnames the pack's licence directory,licence_fromreads the licence from an upstream sub-directory (Noto Emoji's2D/svg/LICENSE), andlicence_missingbrings the set manifest's exception to plain pack configs. -
max_download_byteson agithub-*source, at most 4 GiB, for an archive over the 1 GiB default. Only Noto Emoji sets it (2 GiB): its tagged archive is 1.2 GB, almost all of it PNGs. -
Double-clicking opens the web UI with no console window left behind. On Windows the binary detects an Explorer launch (its console holds only itself,
GetConsoleProcessList= 1, and the parent is not a shell), restarts as a detachedservewith no console and exits, so the window closes within a second. macOS cannot do this for a bare executable, because Finder runs it inside Terminal and the window is Terminal's, somake appbuildsbuild/macos/icon-sets-sync.appfrom the universal binary:LSUIElement(no Dock icon), the universal binary itself asContents/MacOS/icon-sets-sync, and a generated icon. Inside a bundle the binary startsservedetached and quits, so a second double-click hands off to the running server throughserve.json. Linux getsbuild/linux/icon-sets-sync.desktopwithTerminal=false(written bymake crossandmake desktop), which sets the newICHAVA_DESKTOP_LAUNCH=1: open the browser though there is no terminal. The detached server logs toserve.logbesideserve.json, mode0600. With any argument, and in CI, the CLI is unchanged, including the binary inside the bundle. The app is unsigned; docs/installation.md gives the signing and notarisation commands.checkbuildacceptsbuild/macos/icon-sets-sync.appwhen its binary is byte for byte the universal binarychecksums.txtverifies, andmake crossremoves a bundle it would leave stale. -
The sanitiser drops stray text under
<svg>and<g>. OpenMoji 15.1.0's1F684.svgcarries a straydline directly under<g>, which renders; it was removed by hand in icon-sets-openmoji#4, and the next sync would have brought it back. Non-whitespace text and CDATA directly under<svg>or<g>is now dropped, its indentation kept, and each file is logged with its count (sanitise: dropped stray text); thesanitisemetric gainsstray_text_filesandstray_text_nodes. Text in<title>,<desc>,<text>and<tspan>is untouched. More than 25 such files in onesanitisefails the run before anything is written, unless--force;report_onlyis never refused. -
Two guards on an
openmoji-setsrefresh, lifted by--force. The run fails when more than 20% of the icons are uncategorised (a taxonomy of the wrong release looks like that), and the file sink refuses, before the wipe, to remove any icon the pack ships today. Both are opt-in options (Categorise.Preserve/MaxUncategorised,Filesystem.RefuseRemovals), off for every other recipe. -
CI mode (Phase 2, D5).
.github/workflows/sync-pack.ymlis a reusable workflow (on: workflow_call) the pack repositories can call instead of each carrying a copy of the sync logic. It builds the binary from source at a pinned toolkit tag or SHA, opens the update pull request with a Refresh Bot token limited tocontentsandpull-requestswrite, and turns an upstream licence change into an issue instead of a pull request. A Go test holds the file to its triggers, permissions, timeout, concurrency and pinning (P2-C3, P2-C8, P2-C9). -
runis an alias ofsync, and--non-interactiveof--no-input, the names the CI mode uses (P2-C2). -
sync --jsonmarks a licence change: a pack the licence guard stopped carrieslicence_changed: true(P2-S3). -
The per-pack diff. The filesystem sink's metrics gain
added,removedandchangedcounts and up to 50 names of each, hashed before the wipe, so a dry run reports them too. The web UI's run page shows the counts, and names a licence change as one rather than as a generic failure (P2-S2, P2-S3). -
make crossbuilds the whole release matrix locally intobuild/dist/fromsrc/.goreleaser.yaml, with publishing, signing and SBOMs skipped (P2-B2). -
A test proves a GitHub token in the environment reaches neither the run history on disk nor a page the web UI renders (P2-T5).
-
A sync recipe for
ichava/icon-sets-openmoji(Q15).config/icon-sets-openmoji.jsonand the
openmoji-setsrecipe pull the colour and black sets fromhfg-gmuend/openmoji, pinned to the
release tag the checker finds, and the pack is listed inconfig/packs.json. It shares one
pipeline definition with the OpenMoji sets insideemoji-sets, which are deprecated and still
sync unchanged, and a test asserts the two produce identical stages. -
A pack can declare its
licence, and the sync enforces it. With"licence": "CC-BY-SA-4.0"
(orCC-BY-4.0) the licence-text sink fails before touching the pack if the upstream licence text
no longer opens with that licence's title, so a relicensing blocks the sync. openmoji declares it;
a pack that declares nothing behaves as before. -
Kind support for packs (contract D6).
internal/core/kindsreads a pack'spackage.kind,categories.*.kind,itemsandkinds.rulesthe way core does, with the same glob syntax and the same test vectors, and resolves an item through item, rule, category, pack, default. It is additive: a config with no kind keys reads as allicon. -
check --suggest-kinds[=default|all]andsync --suggest-kinds[=default|all](row 7). They scan each pack's checkout and write.cache/kind-suggestions/<pack>.json(--kind-suggestions-dirchanges the directory). The heuristic suggestsillustrationfor an item that fires 3 or more of aspect, palette, bytes, elements and canvas, including bytes or elements, and only for items that resolvediconby default (or bypackage.kindwith=all). It never writes a kind: a person copies a record'sacceptblock intoconfig.json. -
A golden test pins that a sync preserves
package.kind,categories.*.kind,itemsandkinds.rulesbyte for byte. -
ICHAVA_ICON_SETS_SYNC_BIN: the Composer launcher runs a binary you built from source instead of
downloading a release. Without it, and with no release published, the launcher says how to build one. -
Composer package
ichava/icon-sets-sync. Acomposer.jsonand a small PHP launcher atbin/icon-sets-sync, so a PHP project cancomposer require --dev ichava/icon-sets-syncand runvendor/bin/icon-sets-sync. The first run downloads the release binary for the host OS and architecture from the GitHub release matching the installed tag (the latest release for a branch install, orICHAVA_ICON_SETS_SYNC_VERSION), checks it against the release'schecksums.txt, caches it under the package's.cache/bin/, and runs it with the given arguments. Whencosignis onPATHthe Sigstore signature overchecksums.txtis verified too;ICHAVA_ICON_SETS_SYNC_REQUIRE_SIGNATURE=1makes that mandatory. The Go binary and Docker image are unchanged. Tests now include a Composer job runningcomposer validate --strictand the launcher's Pest suite on PHP 8.4 and 8.5. Not on Packagist yet.
Changed
-
docs/configuration.mdand the SVG package's comments name icon-browser where they named react-icon-browser, which is retired; the browser's SVG caps live in icon-browser'sresources/jssince 2026-11-12. CHANGELOG history and the guard insyncscript_test.gothat names the retired path are unchanged. -
release.ymlcalls simtabi/.github's sharedgo-release.ymlat itsv1.0.0release (767e0c0), pinned as@<commit> # v1.0.0so Dependabot can advance the commit and the tag comment together. The release adds optional inputs only (node-versionamong them) and changes nothing a caller passes. -
A release builds on a pushed version tag, or on request (owner, 2026-11-13).
release.ymlnow holds only those two triggers (pushofv[0-9]+.[0-9]+.[0-9]+[-*], andworkflow_dispatch -f tag=vX.Y.Z, replacing the-f confirm=release-gogate) and callssimtabi/.github's sharedgo-release.yml, which every Go project uses; the app and launcher step moves to.scripts/release-launchers.sh, run as itspost-release, and now reads the tag from the shared workflow instead ofGITHUB_REF_NAME, which is a branch on a dispatched run.TestReleaseRunsOnAVersionTagOrOnRequestreplacesTestReleaseRunsOnlyOnAConfirmedDispatchand fails on any other trigger, a branch push, or a pipeline of its own (proven red all three ways). -
Binaries are published on the GitHub release, and nowhere else (owner, 2026-11-13). A release, still dispatched by hand with
-f confirm=release-gofor a version the maintainers choose, now also attaches the macOS app (icon-sets-sync_macos_universal.app.zip) and the three Windows launchers, built from the released universal binary, with their ownchecksums-launchers.txtand build-provenance attestation. The README anddocs/installation.mdoffer the download first and the source build second; there is still nogo installpath, tap, bucket or image, andTestGoReleaserPublishesNowhereElsefails ifsrc/.goreleaser.yamlgains one (proven red with abrewsblock). The launcher step was run locally against a GoReleaser snapshot: four assets, all verified by their checksums file. -
CI builds the binaries and the macOS app into
build/and uploads it. The Tests workflow's release snapshot is now laid out the waymake crossandmake appdo it:build/{os}/with the 19 binaries, the Linux.desktopentry, the Windows launchers andicon-sets-sync.app.check-buildchecks it, GoReleaser's scratch directory is gone afterwards, andbuild/is the run'sicon-sets-sync-buildartifact (14 days). Nothing is published;release.ymlis unchanged. The same sequence run locally gave 37 entries. -
.scripts/sync-svg-policy.mjschecks icon-browser's copy of the SVG policy (react consolidation 3f). The browser UI's source moved into icon-browser at 3d, so the script readsicon-browser/resources/js/core/svg-policy.jsoninstead of the retiredreact-icon-browser/src/core/svg-policy.json. Against core, icon-browser and this repository atmainit exits 0.syncscript_test.gopins the path and runs the script over a temporary workspace, passing on matching copies and failing on a drifted icon-browser copy (both tests proven red against the old path). -
The Composer launcher moved from
bin/icon-sets-synctobuild/icon-sets-sync(owner, 2026-11-13: one folder, and it isbuild/).composer.jsonregisters the new path, so Composer still installs it asvendor/bin/icon-sets-sync; the script is unchanged, sincebuild/sits at the same depth.make check-buildallows it at the root ofbuild/only while it starts with thephpshebang, so a compiled binary there still fails (TestTheComposerLauncherPasses, proven red).make cleannow emptiesbuild/but keeps its tracked files. -
serve --allow-remoteserves TLS, not plain HTTP (behaviour change; owner, 2026-11-12: "support both, but always default to the secure one"). Bound beyond loopback,serveterminates TLS withapi.tls_certandapi.tls_keywhen both are set, and otherwise with a self-signed certificate it generates once, keeps in the state directory (tls-cert.pem,tls-key.pem, mode0600) and reuses until it nears expiry or the host changes, printing its SHA-256 fingerprint and public-key pin at every start. The printed address ishttps://. Plain HTTP beyond loopback, the old behaviour, now needsapi.tls=off-behind-proxy, and bothserveanddoctorwarn about it. Loopback is unchanged: plain HTTP on this machine. -
A run of many packs no longer runs its title off the page. The run page's title names the count ("sync-all · 39 packs") with the first three packs as chips and the rest behind "and 36 more"; the runs list does the same in its Packs column.
-
Icons sit beside their text everywhere. An icon leading text keeps a gap from it outside buttons too (the footer's Source link), an icon beside text that wraps stays on its first line (a long sync branch in the Packs table, an override warning), and the config list puts its icons in their own column. On a phone the Packs and Runs pages no longer scroll sideways: a table's visually hidden label escaped its scroll box.
-
The config editor highlights JSON and YAML, and checks JSON in the browser. Keys, strings, numbers, literals, punctuation and YAML comments are coloured by a small scanner drawn under the text area, built from text nodes only, so the CSP and the no-
innerHTMLrule hold. JSON is checked on every keystroke before the server is asked: a syntax error is underlined at its place (found by the editor's own scanner, since only some browsers report a position), its line is marked in the gutter, the finding links to it, and Save stays off; the server's schema check follows once the text parses. Findings from the server link to their line too. -
The config editor answers in place. It checks the file as it opens, so its status is known at once; Validate and Fix run without leaving the page, with a status badge beside the buttons, the findings right under them and a toast for the result ("Valid", "N problem(s)" with the first one, "Fix made N change(s)", "Nothing to fix"). Fix answers JSON to the page's script and fills the editor with its repair. Without JavaScript both still post the form.
-
Work in flight shows at the top of the window. A thin indeterminate bar is pinned to the top edge, and the button that started the work shows a spinner, instead of a bar appended below the form where a long page hid it. A page restored from the back/forward cache drops a stuck bar.
-
The audit page no longer calls an incomplete run "succeeded". A run that could not read a set carries an "incomplete" badge, and headings keep a gap between their icon and their text.
-
Pack names are lowercase slugs, and the 20
icon-sets-extrasset configs are renamed to match (breaking).icon-sets-extras.<set>(for exampleicon-sets-extras.ant-design) is nowicon-sets-extras-<set>, in the config file names, theirname,packs.jsonand the embedded copy. A name is letters a-z and digits in words joined by single hyphens (config.IsSlug, and the samepatternonnameinpack.schema.json). The old names are not kept: a dotted, capitalised, underscore- or space-joined name in--pack, a registry or a config'snameis refused with the rule and the slug to use, and/packs/<old name>answers 404.TestEveryShippedNameIsASlugreads the raw files and fails on any shipped name that is not a slug;TestSchemaSlugPatternMatchesIsSlugkeeps the schema and the loader on one pattern. The rule is in docs/configuration.md, "Naming packs", and CONTRIBUTING.md. The check table still labels a set config's row with its set. -
The emoji pack is
icon-sets-twemoji, synced by thetwemoji-setsrecipe (owner, 2026-11-05: forward only, no aliases). The pack was renamed on GitHub and ships Twemoji only, so its config isconfig/icon-sets-twemoji.json(and the embedded copy), registered inpacks.jsonunder that name, withpackichava/icon-sets-twemojiandpack_root/work/icon-sets-twemoji. The recipeemoji-setsis nowtwemoji-sets(RecipeTwemojiSets, the schema enum), and its pipelines aretwemoji-sets:twemoji@<version>andtwemoji-sets:commit; the OpenMoji pipelines, which borrowed the old prefix, are named for their own recipe,openmoji-sets:<set>@<version>.make build-emojiismake build-twemoji. Nothing reads the old pack name, recipe name or target. The frozen lxml oracle's sample paths follow the files to where they now live (Twemoji undericon-sets-twemoji/, OpenMoji undericon-sets-openmoji/, same bytes), so the differential compares them again: 726 of 1,238 present. Theemoji.unicode_versionsetting andICHAVA_EMOJI_UNICODE_VERSIONkeep their names: both recipes read them, so they name emoji, not the pack. -
The Composer launcher downloads a release binary only on opt-in (
ICHAVA_ICON_SETS_SYNC_DOWNLOAD=1). Without it, and withoutICHAVA_ICON_SETS_SYNC_BIN, it stops and says how to build from source, so cutting a release can never switch a download channel on by itself (G-06). -
The web-asset vendor fetches through
httpx, under the upstream allowlist, the https-only redirect guard and the size caps, andinternal/tablerisinternal/webvendor(G-03).orDash,orDefaultanditoaare written once (G-04). -
The build-layout guard checks file names, not only folders (G-01). An OS folder under
build/holdsicon-sets-sync_{os}_{arch}[.exe], the hosticon-sets-sync[.exe], and three named launcher exceptions with their reasons (icon-sets-sync.appinmacos/,icon-sets-sync.desktopandicon-sets-sync.pnginlinux/); any other file failscheckbuild. -
Running the binary with no arguments opens the web UI. A double-click, or
icon-sets-syncon its own in a terminal, startsserveand opens the default browser at the tokenised address. It never opens one withCIset, withICHAVA_NO_BROWSER=1, when stdin or stdout is not a terminal, or on Linux and the BSDs withoutDISPLAYorWAYLAND_DISPLAY; there no arguments prints the help and exits 0, as it did without a terminal before. Every command, and any flag, stays headless. The terminal menu that no arguments used to open isicon-sets-sync menuandmake menu, unchanged.servefollows the same rules, and--no-openorICHAVA_NO_BROWSER=1turns the browser off. -
One server at a time, and a way to stop it. A running server records its address in
serve.json(mode0600) in the XDG state directory; a second no-argument launch that finds it answering opens the browser there and exits with one line instead of starting another. A stale record is removed. Every page has a Stop server action (a same-origin POST with the CSRF token), and it, Ctrl-C and closing the Terminal window (SIGHUP) shut down gracefully and remove the record. The window now says "running in your browser; close this window or press Ctrl-C to stop". -
A double-clicked binary finds its configs. Finder starts it with the working directory at
$HOME, so it showed "missing pack registry" and 0 packs. The config directory is now also found walking up from the executable (symlinks resolved), and a binary outside any checkout uses the pack configs embedded in it, read-only in the Config page. The startup line and the Settings page name the source (flag,env,cwd,executable,embedded). Pack checkouts are found beside the toolkit from the executable too, and a pack with none says a sync needsICHAVA_WORKSPACE.make embed-configregenerates the embedded copy; a test fails while it differs fromconfig/. -
make crossleaves only the binaries. It used to leave 86 items (1.0 GB) inbuild/dist: bare binaries, archives, deb/rpm/apk packages and GoReleaser's per-target folders. It now leaves the bare{binary}_{os}_{arch}binaries andchecksums.txt: 38 (568 MB) at the time, 19 since theimtalias was retired (see Removed). The archives and Linux packages moved to the newmake dist, which writes them tobuild/dist/archives/. The release workflow andsrc/.goreleaser.yamlare unchanged. -
build/holds one folder per OS and nothing else.make build-localwritesbuild/<os>/icon-sets-sync(build/macos/on a Mac,.exeon Windows) instead ofbuild/icon-sets-sync;make crosswritesbuild/{os}/icon-sets-sync_{os}_{arch}[.exe]intobuild/macos/,build/linux/,build/windows/andbuild/freebsd/, withbuild/checksums.txtover them, instead ofbuild/dist/;make distaddsbuild/archives/. GoReleaser now builds into the scratch directory.cache/goreleaser/(src/.goreleaser.yamldist:), and only the bare binaries are copied out. The release workflow uploads from that directory, so release assets and their names are unchanged. A binary inbuild/<os>/still finds the checkout, andTestBuildOSFoldersReachThisCheckoutholds the walk-up depth. The newcheckbuildguard (make check-build, run by all three targets) replaces thefind build/dist -type dcheck and fails on any other name inbuild/or any folder inside an OS folder orarchives/. It passes over what a file browser writes into any folder it opens (.DS_Store,Thumbs.db,desktop.ini).build/checksums.txtnames each binary by its path insidebuild/(macos/icon-sets-sync_macos_arm64), socd build && shasum -a 256 -c checksums.txt(orsha256sum -c) verifies as-is, and the guard fails unless it lists exactly the cross binaries present and every hash matches. The releasechecksums.txtthat CI uploads keeps bare names for install scripts and self-update. Behaviour change: the default--kind-suggestions-dirmoved frombuild/kind-suggestionsto.cache/kind-suggestions, sobuild/stays binaries only; pass the flag to keep the old location. -
Documentation links point at each repository's own
docs/(Sim, 2026-11-03: "all ichava repos have docs in their respective repos. thats by design"). Links toopensource.simtabi.com/documentation/ichava/...and theopensource.simtabi.com/products/ichava/...homepage, which answer 404 for every ichava page, now go to the matchingdocs/*.mdon GitHub, or to the repository itself. Each target was checked to exist on that repository'smain. The manifest'shomepageandsupport.docsfollow. -
The author and contact address is
ichava@simtabi.com(W-11, Sim 2026-11-03), replacing the community addressopensource@simtabi.comwherever this repository named it. Vulnerability reports still go tosecurity@simtabi.com. The test that guards the address expectsichava@now. -
The SVG policy's
_always_blocked_commentdescribes the parked brand logos. The five raster-wrapping brand logos it called "render blank" are now parked outside their pack's registered assets (OQ-018), so the comment says so. Only that underscore-prefixed comment changed; the policy is identical. The copies in icon-sets-sync and react-icon-browser are re-synced to the new digest (950214c5828531987ecfa72f1055ab3491c3c664336eaca657cc04907400d390). -
Binary releases are off until the maintainers decide on one. A version tag is git only:
release.ymlis manual and needs-f confirm=release-go. The README and docs install from source
(make build-local). -
Every Makefile target runs on the host, without Docker. Each target keeps its name. The run
targets (menu,check,check-pack,sync,sync-all,sync-all-force,build-emoji,
validate,config-print) buildbuild/icon-sets-syncand run it againstconfig/;buildis
build-local;pulldownloads and verifies the Go modules;shellopens a host shell with
build/onPATH;uiserves on 127.0.0.1:7878;nukeiscleanplus Go's test cache;
test,test-go,lintandformatuse the host's Go, andlintadds golangci-lint, for issues
new sincemain, when it is installed. The shipped configs still name each pack/work/<pack>;
seedocs/installation.mdfor the/worklink on Linux and macOS. -
CI and the release fetch the org's own modules from git, never through the public Go proxy. Every job in
tests.ymlandrelease.ymlthat runs Go setsGOPRIVATEandGONOSUMDBtogithub.com/ichava/*, so no build of ours feeds a tag toproxy.golang.orgorsum.golang.org. -
The docs offer the signed release binaries and the Composer package, not
go install. The README,docs/release.mdand thego.modcomment dropped thego installpath, because the Go module proxy serves its cached pre-publication copies ofsrc/v0.1.0tosrc/v0.1.2. Building from source isgit cloneplusgo -C src build.TestDocsOfferNoGoInstallreads the README,CONTRIBUTING.mdanddocs/, fails on ago installof this module, and fails if it read fewer than 11 files. -
The sync bot commits as the github-actions bot, not a personal GitHub login.
git.authornow defaults toichava-sync-bot <41898282+github-actions[bot]@users.noreply.github.com>(gitx, settings, docs/configuration.md). That matches the address new packs get from the scaffolder. The no-personal-contact guard now also scans the Go sources and matches the login. -
Packages name Simtabi LLC as maintainer, not a person. The
.deb,.rpmand.apkpackages GoReleaser builds carried a personal name and address asmaintainer; it is nowSimtabi LLC <opensource@simtabi.com>, the community role address (decided 2026-10-28, address set for the first release). A Go test (TestNoPersonalContactShips) reads.goreleaser.yaml,README.md, theDockerfileanddocs/, fails on a personal name or address, and fails if it read fewer than 11 files. -
The
ErrRecipePendingcomments say who reads it now: nobody outside this module. They described a cross-repository contract with icon-sets-bundled'ssync-upstream.yml, which grepped the run output for "recipe pending". That wrapper was removed in icon-sets-bundled#95, and no recipe returns the error today. Comments only; the error and the CLI's warning for it are unchanged.
Removed
-
.scripts/wt, the per-session git worktree tool: the estate rule since 2026-11-01 is no worktrees, and nothing called it (G-02). -
.scripts/migration/census.mjsandpolicy.json, referenced by nothing in the Makefile, CI or docs (G-07).
Forward only: each of these was kept for an old caller or an old spelling, and is gone rather than deprecated. Callers were measured across every repository in the ichava packages tree before removal (a positive and a negative control each), and the one pack still using an old form was converted first. -
The uppercase
SOURCE.mdlicence marker. The licence-text sink recognises a hand-supplied licence directory by a lowercasesource.mdonly, the estate's file-name rule, which the sim-standards pre-commit hook enforces on Markdown. It was briefly accepted alongside, with an info line asking for the rename; the one pack that carried it renamed its 13 markers first, so no folder loses its text. Behaviour change: a folder holding onlySOURCE.mdis now an ordinary licence directory, replaced or removed by a sync. Names are matched exactly against the directory's entries, so on a case-insensitive filesystem neitherSOURCE.mdnorSource.mdcounts where the Linux runners would not count it.sinks.LegacyHandSuppliedMarkergoes with it; a test case shows the old code kept aSOURCE.mdfolder and this one replaces it. -
The
ICHAVA_DEV_CONFIG_DIRandICHAVA_DEV_LOG_LEVELenvironment variables, the Python toolkit's names forICHAVA_CONFIG_DIRandICHAVA_LOG_LEVEL. Nothing in the ichava packages sets either. The settings registry'sLegacyEnvfield goes with them.log_levelstill acceptswarning, because ichava/core's.env.examplesetsICHAVA_LOG_LEVEL=warningunder the same name. -
Choosing
emoji-setsandopenmoji-setsby pack name. A pack config names its recipe inrecipe;autopicksgenericfor an npm orgithub-*source and nothing otherwise.config/icon-sets-emoji.json(and its embedded copy) now says"recipe": "emoji-sets";icon-sets-openmojialready said so. Without the field, the emoji pack'surlsource fails with "no recipe wired" instead of running a recipe picked by its name. -
config.ResolveConfigDir, the earlier subset ofResolveConfigSourcekept for its callers. None was left outside its own tests, which now driveResolveConfigSource. -
Reading
items,kindsandcategoriesfrom the top level of a pack'sconfig.json. The kind reader takes them frommetadata.dataonly, and an emptymetadata.datasection no longer falls through to the top level. ichava/core'sPackConfigData::section()dropped the same fallback, and this reader mirrors it so a suggestion resolves a pack exactly as core does. Measured over the 24resources/assets/svg/config.jsonfiles in the ichava packages: none uses the top level, and 21 carrymetadata.datasections. -
(*pipeline.Pipeline).Run,RunContext(context.Background())kept for callers with no context. No production code called it; the 14 test call sites, found by removing it and letting the compiler list them, callRunContextinstead. -
OpenMoji in the
emoji-setsrecipe, and theemoji.openmoji_versionsetting (ICHAVA_EMOJI_OPENMOJI_VERSION). icon-sets-emoji ships Twemoji only since its OpenMoji copies were removed; OpenMoji is icon-sets-openmoji's, built byopenmoji-sets, which is unchanged.emoji-setsnow builds the Twemoji pipeline and its commit pipeline, so the weekly emoji sync can no longer writeopenmoji-colorandopenmoji-blackback into that pack. The setting, its schema property (a pack config that still setssettings.emoji.openmoji_versionfails to load),settings.DefaultOpenMojiVersionandrecipes.DefaultOpenMojiVersiongo with it; no config in the ichava packages set it.TestEmojiSetsBuildsTwemojiOnlywas red against the old recipe. -
The inline 27-row presentation-value table (
TestPresentationAttributeValueGuards). Every row, compared by name, value and keep, is in the sharedtestdata/presentation-vectors.json, whichTestSharedVectorCasesruns. -
The
imtalias binary is retired; useicon-sets-sync, same commands.imtwas the same program (cli.Execute) under a short name, and building it doubledbuild/dist:make crossnow leaves 19 binaries andchecksums.txtinstead of 38, and each release archive and Linux package shipsicon-sets-syncalone.src/cmd/imtand its GoReleaser build, universal-binary and archive entries are gone, andTestOnlyOneEntrypointandTestGoReleaserBuildsOneBinaryfail if a second entrypoint or binary comes back. Anyone withimtonPATHcan switch toicon-sets-syncor addalias imt=icon-sets-sync. -
The Docker surface. No pack has run the image since
sync-pack.ymlbegan building the binary
from source, so it is gone:Dockerfile,.dockerignoreanddocker-compose.yml;.github/workflows/docker.yml, which publishedghcr.io/ichava/icon-sets-syncon every push to
main. Images already in GHCR are untouched;- the Tests workflow's
Docker imagejob; - the
dockerecosystem in.github/dependabot.yml; - the Makefile's
docker composeandgolangcontainer invocations (the targets stay, see
Changed).
-
The Python leftovers. The
.gitignorePython section (virtualenvs, pytest, mypy, ruff,
coverage, eggs), keeping a bytecode rule so an old checkout's stale caches stay out of a commit.
Comments and docs that described the Python, or the image, as part of the current product now
describe the source build. The--commitflag's help no longer cites the Python. -
The three Python scripts CI ran. The Changelog order job and release-please's changelog close
are nowsrc/internal/tools/changelog checkandclose, with the same messages, exit codes and
GITHUB_OUTPUTlines; on this CHANGELOG the closed file is byte-identical to the Python's. The web UI
accessibility job serves its fixture upstream withsrc/internal/tools/staticserveinstead of
python3 -m http.server.TestNoWorkflowOrScriptInvokesPythonfails if a workflow or script
under.github/calls Python again. -
TestNoDockerOrPythonSurfaceandTestNoWorkflowBuildsOrPublishesAnImagefail if a Dockerfile,
a compose file, a.dockerignore, a.pyfile, a Docker workflow, adocker/action,
docker build/run/push,ghcr.io, a docker Dependabot ecosystem or adockercall in the
Makefile comes back.TestReleaseRunsOnlyOnAConfirmedDispatchholdsrelease.ymlto
workflow_dispatchwith a requiredconfirm.
These were deprecated in 0.1.2 with 0.2.0 as the earliest removal, and are removed now on the owner's decision. Each is a breaking change for anyone still using the old name.
- The
"recipe": "bundled"value. Write"recipe": "manifest-pack". Both copies of the pack schema (config/schema/pack.schema.jsonand the embedded one) drop it from therecipeenum, so a config that still saysbundledfails to load with/recipe: value must be one of 'auto', 'generic', 'emoji-sets', 'manifest-pack'.TestTheRemovedBundledRecipeIsRejectedpins it, and fails withbundledput back in the enum. - Choosing
manifest-packby pack name. A config foricon-sets-bundledwith norecipe(or"auto") no longer reachesmanifest-pack; it has to say"recipe": "manifest-pack".TestManifestPackIsNotReachedByItsFormerNameOrByPackNamefails if the name dispatch returns. The deprecation notice this path and the old recipe name logged on every run, and the warningconfig validateprinted for them, go with them (recipes.Deprecation,recipes.RemovalRelease,recipes.RecipeBundled). - The hidden
bundled auditcommand. Useimt manifest-pack audit;bundledis now an unknown command. - The
/bundled/auditweb routes (the page, the POST that starts an audit, and/{id}/manifest.json). Use/manifest-pack/audit; the old address answers 404, whichTestRemovedAuditRouteIsNotFoundpins. - The
ichava-maintainer-toolkitbinary name. The image no longer ships the symlink and the binary no longer prints a notice under that name; runicon-sets-syncorimt. CI now asserts the symlink is absent from the image. The docker compose service keeps its name,ichava-maintainer-toolkit, which is a service name and not a binary. - Go aliases:
app.BundledPack(useapp.ManifestPack),app.AuditBundled(useapp.AuditManifestPack),(*web.AppEngine).AuditBundled(useAuditManifestPack),version.LegacyName, and the unusedcli.resolveAll(useapp.ResolveSets). All are ininternalpackages, so nothing outside this module could import them.
Fixed
-
Update pull requests open green when a count changes (2026-11-11). A pack's tests pin its icon count and version, and its README and
composer.jsonstate them, but a sync moved only the assets, so tabler 3.51.0, carbon 11.92.0, simple-icons 16.37.0 and lucide 1.72.0 each opened red and were fixed by hand. The newStatedFactsstep, run before the version stamp in every commit chain, compares the SVGs written with those atHEAD(total and per set folder) and rewrites each moved figure, and the version, where those files state them. Only whole figures move, counts under ten are left alone, and a figure no file states is reported.gitx.ListTreereads the committed tree. -
A re-run no longer drops commits a person added to the sync's pull request branch. The git-branch sink reset the stable branch to the base and force-pushed with a lease taken from a fetch made a moment earlier, so the lease always held and a hand-made commit was overwritten (icon-sets-openmoji#18 lost its test fix this way on 2026-11-09). It now replays every commit on the old branch not authored by the sync, oldest first and with its author, on top of the new sync commit; pushes with a lease naming the exact tip it inspected; and stops as needs review, pushing nothing, when such a commit no longer applies. The run metric lists the commits kept (
kept_commits).gitxgainsRevParse,CommitsNotIn,CherryPickandPushBranchLease;PushBranchis deprecated in favour ofPushBranchLeaseand still works. -
A namespace declaration nothing uses is dropped. The policy strips
sodipodi:docnamebut left thexmlns:sodipodithat declared it, so OpenMoji's Cornwall flag (1F3F4-E0063-E0061-E0071-E0063-E007F, both sets) shipped a declaration js-icon-components' generator refuses. After filtering, everyxmlns:<prefix>whose namespace no remaining element or attribute uses is removed; the defaultxmlnsstays, and so does any prefix in use (xmlns:xlinkwhile anxlink:hrefsurvives). It is tidying, not a policy violation, so it is not counted as removed. Measured 2026-11-05 over all 22 pack checkouts (253,984 SVGs, old against new sanitiser on the shipped files): 8,002 files change on their next sync, all in four packs: icon-sets-extras 4,963 (unusedxmlns:xlink), icon-sets-openmoji 2 (the Cornwall flag), and two private packs 3,037 between them (unusedxmlns:xlink, and 151 files with Inkscape'ssodipodi,inkscape,cc,dcandrdf). The other 18 packs: 0.namespace_test.gocovers the drop and the survivors. -
The US and UM flags keep their stars. Both draw the fifty stars as one
<marker>repeated bymarker-mid="url(#us-a)", and the SVG policy stripped<marker>, so the reference pointed at nothing and the canton shipped empty (0 white pixels against 11,050 upstream). The policy now allows<marker>with its own geometry,refX,refY,markerWidth,markerHeight,orientandmarkerUnits, besideid,viewBox,preserveAspectRatioand the presentation attributes it already allowed. Everything else holds: a script oron*handler inside or on a marker is stripped, an externalhrefis stripped, andmarker-start/-mid/-endstill take onlynoneorurl(#fragment). The shared vector table,testdata/presentation-vectors.json, gains 8 attribute rows and adocumentssection of 6 whole-document rows, and is byte-identical in core, react-icon-browser and js-icon-components;TestSharedVectorCasesandTestSharedVectorDocumentsrun it. The vendoredsvg-policy.jsonis ahead of core until core takes the same bytes. -
keycap: *is no longer lost tokeycap: #. Both names slug tokeycap. WithoutPreserve(the deprecatedemoji-setspath) the later file was renamed over the earlier, and with it the later took the name and the earlier went touncategorised/, while the shipped packs carrykeycap: #askeycap.svg. Slugs are now resolved once per taxonomy (ResolveNameSlugs), shared byCategoriseandIndexer: on any collision the first record inemoji-test.txtorder keeps the base slug, and a later one takesDisambiguatedSlugof its own name, punctuation spelled out from a small named-symbol map and anything else asuplus its hex code point, with the codepoint key as the last resort. Sokeycap: #stayskeycapandkeycap: *becomeskeycap-asterisk, in both recipes, incodepoints.jsonand innames.json. In Unicode 16.0 and 17.0 this is the only collision (measured overemoji-test.txt). One emoji shipped twice still goes touncategorised/, the exact match keeping the name; between two equal matches the first now keeps it.TestCategoriseKeepsBothSidesOfASlugCollisionreplaces the test that pinned the later one winning. -
A real emoji-sets sync keeps Twemoji's licence.
FindLicencesrecorded the licence files inside the fetched tree, andCategorisewipes that tree's root and refills it with SVGs only. For Twemoji the root is the whole npm package, so theLICENSEwas deleted beforeLicenceTextsread it, and every non-dryicon-sets-emojisync since the first release failed atLicenceTexts twemoji: open …/package/license. A dry run never read the file, so it never showed.FindLicencesnow copies the files it finds into the pipeline's working directory, byte for byte under their upstream names, and records the copies. A real run oficon-sets-emojiagainst a scratch copy of the pack now writes every set, with 0 Twemoji SVGs changed, and adds the three upstream licence texts the pack has never carried.TestFindLicencesSurvivesARebuiltTreefails when the stage records the originals. -
OpenMoji 15.1.0 keeps the taxonomy the pack shipped. Deriving the taxonomy from the release categorised OpenMoji 15.1.0 against Unicode 15.1, which files crab, lobster, oyster, shrimp and squid under food-drink, where Unicode 16.0 had moved them to animals-nature. The removal guard refused the run over those five, in both sets. The derived version now never falls below the default (
16.0), because a newer taxonomy still names every older emoji. OpenMoji 17.0.0 still uses 17.0, and an explicitemoji.unicode_versionstill wins.TestTheDerivedTaxonomyNeverFallsBelowTheDefaultcovers it. -
An exact taxonomy match keeps its name over one without the selector. OpenMoji 15.1.0 ships white flag twice, as
1F3F3-FE0F.svg(the artwork the pack has always shipped) and as1F3F3.svg, which matches only with FE0F left off. The later file kept the slug, so the shipped flag was replaced by the other drawing and moved touncategorised/. WithPreserve, an exact match now beats an FE0F-less one in either order. The deprecatedemoji-setspath, which has noPreserve, is untouched. -
No arguments takes the next free port. A double-click with 7878 busy failed with "omit --port to take the next free one", because the no-argument path behaved as if
--porthad been given. It now moves to the next free port and says so, asservedoes; an explicit--portstill fails. -
A dry run writes nothing into the pack checkout. A
sync --dry-runoficon-sets-openmojiwrote 17 new SVGs into the pack and modified tracked files, because the file and licence sinks ran in full on a dry run. They now run their guards and report the diff they would make (added, removed, changed, andwould_write) without writing; the fetched tree stays in the run's temporary directory. This coverssync,sync-all, the menu's dry run and the web UI's "Sync stale packs (dry run)", which share one path. Behaviour change: a dry run used to leave the refreshed files in the pack's working tree, sogit diffthere no longer shows them; the run's own diff does. -
An OpenMoji refresh no longer drops icons that match no taxonomy entry. A dry run of
icon-sets-openmojifrom 15.1.0 to 17.0.0 loggedcategorise copied=3574 missing=921: those 921 of 4,495 SVGs were deleted from the refreshed tree, not filed anywhere. They were 207 emoji OpenMoji names without the emoji-presentation selector (2600.svg, the sun, against2600 FE0Finemoji-test.txt), 542 OpenMoji extras, 163 Unicode 17 emoji the 16.0 taxonomy does not know, and 9 component glyphs. Only the first group was a pre-existing loss in the shipped pack (it holds neither the sun nor the wastebasket today).openmoji-setsnow matches a name regardless ofFE0F, keeps every other icon asuncategorised/<upstream-name>.svgwith a warning and anuncategorisedcount, and keeps both icons when two emoji slug to one name (keycap: #andkeycap: *overwrote each other). Measured on 17.0.0: 3,944 categorised, 551 uncategorised, 0 dropped. The deprecatedemoji-setsrecipe is unchanged and a test pins it. -
The taxonomy follows the OpenMoji release.
openmoji-setsderivesemoji.unicode_versionfrom the release (17.0.0 uses 17.0) unless it is set explicitly. Unicode 17.0 publishesemoji-test.txtunderPublic/17.0.0/emoji/, where the oldPublic/emoji/17.0/path answers 404, so the URL follows that layout from 17.0; 16.0 and earlier are unchanged. -
serveand the other commands find the registry when run fromsrc/. The README'sgo -C src run ./cmd/icon-sets-sync serveruns insrc/, so the web UI said "missing pack registry: config/packs.json" and listed no packs. One resolver (config.ResolveConfigDir) now serves every command and the UI:--config-dir, thenICHAVA_CONFIG_DIR, thenconfig/beside the binary, then./configif it holdspacks.json, then theconfig/of this repository found by climbing at most 5 parents from the working directory (stopping at.git, next tosrc/go.mod), logged at info level. A missing registry now namesICHAVA_CONFIG_DIRand--config-dir, which the UI banner shows.make serveruns the documented command. -
Every form POST in the web UI was refused with 403 "Cross-origin request refused".
Referrer-Policy: no-referrermakes browsers sendOrigin: nullon a same-origin form POST, and the origin check rejectednulloutright. The policy is nowsame-origin, andOrigin: nullis accepted only withSec-Fetch-Site: same-origin; a bare or cross-sitenullis still refused, and the CSRF token and Host checks are unchanged. The UI-accessibility job sends a browser-shaped POST. -
A pack's root resolves on a developer machine. The shipped configs name
/work/<pack>, the CI runner's layout, so locally every pack page showed a missing directory. For such a root: theworkspacesetting when<workspace>/<pack>exists, then/work/<pack>, then the sibling checkout beside this repository, else the configured value with a message namingICHAVA_WORKSPACE. The pack page andconfig print --pack ...(pack_root_origin) show where the root came from;sync-pack.ymlis unchanged. Theconfig_dirandworkspacehelp text now matches. -
A licence declared by a pack on the
genericrecipe is enforced. Only theopenmoji-setsrecipe passed the pack'slicenceto its licence-text sink, so a declaration in a tabler or flag config was silently unguarded (P2-G10). No shipped generic config declares one, so no sync changes behaviour today.
Security
- Paths built from a request, a config or a manifest are confined to their root (CodeQL go/path-injection). A set name from the audit form, a package name, a pack name from an edited document and a fingerprint key reached
os.Stat,os.ReadFile,os.MkdirAlland archive extraction without a bound;../in the audit's set name surveyed and fetched against a directory outside the pack. The newinternal/core/pathguard.Joinis the one rule: it cleans the path, refuses an absolute element or any..(inside a name too: none of the 260,763 shipped SVGs has one), checks the result withfilepath.Rel, and refuses a symlink anywhere under the root that resolves outside it, a dangling one included.docfile.Find,config.ResolveFile(now returning an error), the manifest-pack resolver and its npm fetch, the audit, and both archive extractors go through it; the extractors' ownsafeJoinis folded in. Behaviour change: an archive member with an absolute name is refused rather than clamped inside the destination, andResolveFilerefuses a name with a separator in the public directory too, not only in the private overlay. - The config splice size cannot overflow (CodeQL go/allocation-size-overflow).
setStringAtPathsized its buffer aslen(data)-(end-start)+len(encoded)unchecked.spliceSizebounds the document and the value at 256 MiB each before the arithmetic and refuses an out-of-range splice. - The launch-token exchange cannot redirect off site (CodeQL go/unvalidated-url-redirection). It redirected to the request's own path with the token stripped, so
//evil.example/x?token=…sent a signed-in browser to another host. The target now has to passisLocalURL(one leading slash, no//or/\, no scheme, host or control character); anything else lands on/. - The SVG policy's CSS
url()guard can no longer be bypassed (S1). It matched the raw text with a regex, so CSS escapes (\75rl(,\000075rl(,u\rl(), comments (u/**/rl(),image-set(,-webkit-image-set(,image(,src(,cross-fade(,element(,expression(,@importand a quote mismatch such asurl("https://e/x'y")all got a remote fetch past it. Values are now normalised (comments stripped, escapes decoded, lowercased) and checked against an allow-list:url(only with a same-document#id, only colour,var, math, transform and easing functions otherwise, and no at-rules. The same check now coversfill,stroke,filter,clip-path,maskandmarker-*, which were trusted on their name.url(#grad)is unchanged. - Documents are capped before and while they are parsed (S2).
Policy.MaxBytes(default 4,500,000 bytes, matching react-icon-browser and core) andPolicy.MaxNodes(default 30,000 elements) refuse an oversized file as an error, like an unparseable one. The largest SVG in the packs is 1,053,328 bytes and 6,741 elements. - Response bodies and archives are bounded (S6).
GetJSONreads at most 32 MiB andDownloadat most 1 GiB. Extraction refuses a file over 256 MiB, more than 2 GiB in total, or more than 200,000 entries, counted on the bytes written and not on a size the header declares, so a decompression bomb stops at the cap. An over-limit body is not retried. - Redirects are policed (S13). The client refuses a redirect from
httpstohttp, to a scheme other thanhttp(s), or to a literal loopback, private or link-local address, and stops after 10. A refused redirect is not retried. - Fetches go only to an upstream allowlist (P2-G5).
GetJSONandDownloadrefuse any host not on the list before sending a request, and every redirect hop is held to it. The default is exactly the hosts the shipped recipes reach:registry.npmjs.org,api.github.com,github.com,codeload.github.comandunicode.org. A test builds every shipped pack's pipelines and fails if any URL they reach is off the list, or if a default host is reached by none. Extend it with the new run-wideallowed_hostssetting (ICHAVA_ALLOWED_HOSTS,--set, or the global config); a pack config cannot set it. A blocked fetch names the host and the setting, and is not retried.objects.githubusercontent.comandrelease-assets.githubusercontent.comare not on the default list, because no shipped recipe downloads a release asset. Behaviour change: a private pack that fetches from another host now needsallowed_hosts. - Upstream URLs must be
https(P2-G6). A pack config with a plain-httpversion_check_urlorsource.archive_urlfails to load, naming the file, the field and the URL, and the client refuses the same URL at fetch time and on a redirect.http://127.0.0.1,http://localhostandhttp://[::1]are exempt on the first request so tests can use a local server. Every shipped config was alreadyhttps. - The SVG policy keeps
display,colorandmarker-start/-mid/-end, each behind a value guard. A dry run of OpenMoji 17 strippeddisplay="none"from 13 subdivision flags, so their hidden construction shapes would render, pluscolorfrom 3 files andmarker-endfrom 1.displaysurvives only as bare keywords (none,inline,block,inline flow-root): no parens, nourl(), no escape.colorgoes through the CSS value guard.marker-*survive only asnoneorurl(#fragment). Sodisplay="url(x)"andmarker-end="url(https://e)"are still stripped, and so is Inkscape'ssodipodi:docname. The vendoredsvg-policy.jsongained the five names and is ahead ofichava/core's canonical copy until core and react-icon-browser take the same change. npm packruns with--ignore-scripts(S7), in both places it is called, so a git or local spec cannot run apreparescript on the runner.- S1, the CSS
url()bypass, was already closed in this section's first entry; no further change was needed here.
Documented
- The SVG sanitiser's
violations=1284 would_change=4495is explained in this entry, not changed:would_changecounts every file because the serialiser drops the trailing newline;violationscounts files with something removed, and all 1,284 lose the rootversion="1.1"attribute. 13 files also losedisplay="none"(hidden construction shapes in subdivision flags would render), 3color, and 1 eachdocnameandmarker-end. The first three no longer do (see Security);docnameis Inkscape metadata and stays stripped.
Full changelog: https://github.com/ichava/icon-sets-sync/blob/main/CHANGELOG.md