Skip to content

v0.1.0

Latest

Choose a tag to compare

@github-actions github-actions released this 14 Nov 02:21
· 3 commits to main since this release

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 doctor says which are on and warns about each risky combination. See docs/tools/api.md, events.md, webhooks.md and mcp.md.

    • Events. internal/events is 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.disabled and webhook.test. Each type's data has a JSON Schema embedded in the binary. TestEveryTypeHasASchema fails on a type without a schema or a schema without a type, TestEveryTypeHasAPublisher on a type no production code publishes, and TestRunEventsMatchTheirSchemas validates every event a run emits. The job registry gains publishing hooks; the run page's per-job stream is unchanged.
    • REST API under /api/v1 on the serve listener (api.enabled; beyond loopback also api.allow_remote), 22 endpoints with an OpenAPI 3.1 document that TestOpenAPIMatchesTheRoutes holds to the routes declared in apiRoutes, both ways and scope by scope. Bearer tokens only, with scopes read, sync, config:write and admin: icon-sets-sync api token create|list|revoke shows a token once and keeps only its SHA-256 in api-tokens.json in the state directory (mode 0600), 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-Key on the sync endpoints, an audit line per write in api-audit.jsonl, and dry_run defaulting to true. --read-only registers no write endpoint. GET /api/v1/events streams the bus as Server-Sent Events when events.stream is on. The UI and the API now start runs through one web.StartSync, and save settings through one web.SaveGlobalSetting with the Settings page's checks (web.ParseSettingValue).
    • Webhooks (webhooks.enabled, endpoints in webhooks.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 in webhook-secrets.json (mode 0600), with an optional secret_env override, 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) unless webhooks.allow_private; https only (plain http only to localhost with webhooks.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 rotated webhook-deliveries.jsonl, and auto-disable after 50 consecutive failures with a webhook.disabled event.
    • MCP. icon-sets-sync mcp serves the Model Context Protocol on stdio with the official Go SDK, github.com/modelcontextprotocol/go-sdk v1.8.0 (mcp.enabled or --enable). Eight read tools by default; sync_pack, cancel_job and set_setting only with mcp.allow_writes and never under --read-only, and a real sync needs confirm equal to the slug and is refused while a serve runs 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-only and without mcp.allow_writes; every integration is off on a fresh config; a non-loopback bind defaults to TLS.
  • A Windows launcher that opens the web UI with no console window. make cross builds icon-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 its serve detached with ICHAVA_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 in cmd and PowerShell (owner, option A, 2026-11-11). Like the macOS app and the Linux .desktop entry, it is not a GoReleaser build or a release asset. TestWindowsLauncherIsAGUIProgramAndTheCLIIsNot reads both PE headers (proven red both ways); the build-layout guard names the three launcher files; TestOnlyOneEntrypoint allows exactly this second src/cmd directory.

  • 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_hosts must 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_size setting. Packs, runs, audit results and a run's log show page_size rows (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 a Max bound, used by page_size and carried into global.schema.json.

  • The global config starts initialised, and can be reinitialised. serve and the new config init write it from what the machine has: $schema, packages_dirs (the directory of pack checkouts this tool sits in) and private_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.json in the state directory). The global config page says which it is and has Reinitialise (a confirmed action; config init --force in 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 of icon-sets-extras (20, each recording its version at upstream.sets.<set>.current_version in 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). TestEveryShippedRecipeValidates loads and builds every registered config and fails below 38, or on a config file packs.json does not register.

  • split-variants transform. 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, and flatten_prefix keeps 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.

  • exclude transform. Drops icons by stem (names, Octicons' GitHub marks), by directory (directories, Game Icons' unlicensed badges/) or from an upstream data file (metadata, Simple Icons' non-CC0 logos from data/simple-icons.json, so the list follows each release). An excluded item that cannot be named fails the run.

  • More transform options. slugify takes dirs (slug directory names too: Remix's Game & Sports) and hyphenate_dots (lucene.net -> lucene-net), and now fails when two names slug to one instead of renaming one over the other. flatten-variants takes slug, which slugs each stem before the affix (Health Icons' !.svg is committed as -filled.svg). sanitise takes drop_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. licence now admits Apache-2.0, CC-BY-3.0, CC0-1.0, ISC and MIT beside the two CC-BY ids, each checked against its opening lines (a Markdown # is ignored). The new licence_sha256 pins 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_set names the pack's licence directory, licence_from reads the licence from an upstream sub-directory (Noto Emoji's 2D/svg/LICENSE), and licence_missing brings the set manifest's exception to plain pack configs.

  • max_download_bytes on a github-* 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 detached serve with 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, so make app builds build/macos/icon-sets-sync.app from the universal binary: LSUIElement (no Dock icon), the universal binary itself as Contents/MacOS/icon-sets-sync, and a generated icon. Inside a bundle the binary starts serve detached and quits, so a second double-click hands off to the running server through serve.json. Linux gets build/linux/icon-sets-sync.desktop with Terminal=false (written by make cross and make desktop), which sets the new ICHAVA_DESKTOP_LAUNCH=1: open the browser though there is no terminal. The detached server logs to serve.log beside serve.json, mode 0600. 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. checkbuild accepts build/macos/icon-sets-sync.app when its binary is byte for byte the universal binary checksums.txt verifies, and make cross removes a bundle it would leave stale.

  • The sanitiser drops stray text under <svg> and <g>. OpenMoji 15.1.0's 1F684.svg carries a stray d line 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); the sanitise metric gains stray_text_files and stray_text_nodes. Text in <title>, <desc>, <text> and <tspan> is untouched. More than 25 such files in one sanitise fails the run before anything is written, unless --force; report_only is never refused.

  • Two guards on an openmoji-sets refresh, 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.yml is 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 to contents and pull-requests write, 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).

  • run is an alias of sync, and --non-interactive of --no-input, the names the CI mode uses (P2-C2).

  • sync --json marks a licence change: a pack the licence guard stopped carries licence_changed: true (P2-S3).

  • The per-pack diff. The filesystem sink's metrics gain added, removed and changed counts 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 cross builds the whole release matrix locally into build/dist/ from src/.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.json and the
    openmoji-sets recipe pull the colour and black sets from hfg-gmuend/openmoji, pinned to the
    release tag the checker finds, and the pack is listed in config/packs.json. It shares one
    pipeline definition with the OpenMoji sets inside emoji-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"
    (or CC-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/kinds reads a pack's package.kind, categories.*.kind, items and kinds.rules the 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 all icon.

  • check --suggest-kinds[=default|all] and sync --suggest-kinds[=default|all] (row 7). They scan each pack's checkout and write .cache/kind-suggestions/<pack>.json (--kind-suggestions-dir changes the directory). The heuristic suggests illustration for an item that fires 3 or more of aspect, palette, bytes, elements and canvas, including bytes or elements, and only for items that resolved icon by default (or by package.kind with =all). It never writes a kind: a person copies a record's accept block into config.json.

  • A golden test pins that a sync preserves package.kind, categories.*.kind, items and kinds.rules byte 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. A composer.json and a small PHP launcher at bin/icon-sets-sync, so a PHP project can composer require --dev ichava/icon-sets-sync and run vendor/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, or ICHAVA_ICON_SETS_SYNC_VERSION), checks it against the release's checksums.txt, caches it under the package's .cache/bin/, and runs it with the given arguments. When cosign is on PATH the Sigstore signature over checksums.txt is verified too; ICHAVA_ICON_SETS_SYNC_REQUIRE_SIGNATURE=1 makes that mandatory. The Go binary and Docker image are unchanged. Tests now include a Composer job running composer validate --strict and the launcher's Pest suite on PHP 8.4 and 8.5. Not on Packagist yet.

Changed

  • docs/configuration.md and 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's resources/js since 2026-11-12. CHANGELOG history and the guard in syncscript_test.go that names the retired path are unchanged.

  • release.yml calls simtabi/.github's shared go-release.yml at its v1.0.0 release (767e0c0), pinned as @<commit> # v1.0.0 so Dependabot can advance the commit and the tag comment together. The release adds optional inputs only (node-version among them) and changes nothing a caller passes.

  • A release builds on a pushed version tag, or on request (owner, 2026-11-13). release.yml now holds only those two triggers (push of v[0-9]+.[0-9]+.[0-9]+[-*], and workflow_dispatch -f tag=vX.Y.Z, replacing the -f confirm=release-go gate) and calls simtabi/.github's shared go-release.yml, which every Go project uses; the app and launcher step moves to .scripts/release-launchers.sh, run as its post-release, and now reads the tag from the shared workflow instead of GITHUB_REF_NAME, which is a branch on a dispatched run. TestReleaseRunsOnAVersionTagOrOnRequest replaces TestReleaseRunsOnlyOnAConfirmedDispatch and 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-go for 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 own checksums-launchers.txt and build-provenance attestation. The README and docs/installation.md offer the download first and the source build second; there is still no go install path, tap, bucket or image, and TestGoReleaserPublishesNowhereElse fails if src/.goreleaser.yaml gains one (proven red with a brews block). 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 way make cross and make app do it: build/{os}/ with the 19 binaries, the Linux .desktop entry, the Windows launchers and icon-sets-sync.app. check-build checks it, GoReleaser's scratch directory is gone afterwards, and build/ is the run's icon-sets-sync-build artifact (14 days). Nothing is published; release.yml is unchanged. The same sequence run locally gave 37 entries.

  • .scripts/sync-svg-policy.mjs checks 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 reads icon-browser/resources/js/core/svg-policy.json instead of the retired react-icon-browser/src/core/svg-policy.json. Against core, icon-browser and this repository at main it exits 0. syncscript_test.go pins 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-sync to build/icon-sets-sync (owner, 2026-11-13: one folder, and it is build/). composer.json registers the new path, so Composer still installs it as vendor/bin/icon-sets-sync; the script is unchanged, since build/ sits at the same depth. make check-build allows it at the root of build/ only while it starts with the php shebang, so a compiled binary there still fails (TestTheComposerLauncherPasses, proven red). make clean now empties build/ but keeps its tracked files.

  • serve --allow-remote serves TLS, not plain HTTP (behaviour change; owner, 2026-11-12: "support both, but always default to the secure one"). Bound beyond loopback, serve terminates TLS with api.tls_cert and api.tls_key when both are set, and otherwise with a self-signed certificate it generates once, keeps in the state directory (tls-cert.pem, tls-key.pem, mode 0600) 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 is https://. Plain HTTP beyond loopback, the old behaviour, now needs api.tls=off-behind-proxy, and both serve and doctor warn 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-innerHTML rule 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-extras set configs are renamed to match (breaking). icon-sets-extras.<set> (for example icon-sets-extras.ant-design) is now icon-sets-extras-<set>, in the config file names, their name, packs.json and the embedded copy. A name is letters a-z and digits in words joined by single hyphens (config.IsSlug, and the same pattern on name in pack.schema.json). The old names are not kept: a dotted, capitalised, underscore- or space-joined name in --pack, a registry or a config's name is refused with the rule and the slug to use, and /packs/<old name> answers 404. TestEveryShippedNameIsASlug reads the raw files and fails on any shipped name that is not a slug; TestSchemaSlugPatternMatchesIsSlug keeps 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 the twemoji-sets recipe (owner, 2026-11-05: forward only, no aliases). The pack was renamed on GitHub and ships Twemoji only, so its config is config/icon-sets-twemoji.json (and the embedded copy), registered in packs.json under that name, with pack ichava/icon-sets-twemoji and pack_root /work/icon-sets-twemoji. The recipe emoji-sets is now twemoji-sets (RecipeTwemojiSets, the schema enum), and its pipelines are twemoji-sets:twemoji@<version> and twemoji-sets:commit; the OpenMoji pipelines, which borrowed the old prefix, are named for their own recipe, openmoji-sets:<set>@<version>. make build-emoji is make 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 under icon-sets-twemoji/, OpenMoji under icon-sets-openmoji/, same bytes), so the differential compares them again: 726 of 1,238 present. The emoji.unicode_version setting and ICHAVA_EMOJI_UNICODE_VERSION keep 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 without ICHAVA_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, and internal/tabler is internal/webvendor (G-03). orDash, orDefault and itoa are written once (G-04).

  • The build-layout guard checks file names, not only folders (G-01). An OS folder under build/ holds icon-sets-sync_{os}_{arch}[.exe], the host icon-sets-sync[.exe], and three named launcher exceptions with their reasons (icon-sets-sync.app in macos/, icon-sets-sync.desktop and icon-sets-sync.png in linux/); any other file fails checkbuild.

  • Running the binary with no arguments opens the web UI. A double-click, or icon-sets-sync on its own in a terminal, starts serve and opens the default browser at the tokenised address. It never opens one with CI set, with ICHAVA_NO_BROWSER=1, when stdin or stdout is not a terminal, or on Linux and the BSDs without DISPLAY or WAYLAND_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 is icon-sets-sync menu and make menu, unchanged. serve follows the same rules, and --no-open or ICHAVA_NO_BROWSER=1 turns the browser off.

  • One server at a time, and a way to stop it. A running server records its address in serve.json (mode 0600) 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 needs ICHAVA_WORKSPACE. make embed-config regenerates the embedded copy; a test fails while it differs from config/.

  • make cross leaves only the binaries. It used to leave 86 items (1.0 GB) in build/dist: bare binaries, archives, deb/rpm/apk packages and GoReleaser's per-target folders. It now leaves the bare {binary}_{os}_{arch} binaries and checksums.txt: 38 (568 MB) at the time, 19 since the imt alias was retired (see Removed). The archives and Linux packages moved to the new make dist, which writes them to build/dist/archives/. The release workflow and src/.goreleaser.yaml are unchanged.

  • build/ holds one folder per OS and nothing else. make build-local writes build/<os>/icon-sets-sync (build/macos/ on a Mac, .exe on Windows) instead of build/icon-sets-sync; make cross writes build/{os}/icon-sets-sync_{os}_{arch}[.exe] into build/macos/, build/linux/, build/windows/ and build/freebsd/, with build/checksums.txt over them, instead of build/dist/; make dist adds build/archives/. GoReleaser now builds into the scratch directory .cache/goreleaser/ (src/.goreleaser.yaml dist:), 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 in build/<os>/ still finds the checkout, and TestBuildOSFoldersReachThisCheckout holds the walk-up depth. The new checkbuild guard (make check-build, run by all three targets) replaces the find build/dist -type d check and fails on any other name in build/ or any folder inside an OS folder or archives/. It passes over what a file browser writes into any folder it opens (.DS_Store, Thumbs.db, desktop.ini). build/checksums.txt names each binary by its path inside build/ (macos/icon-sets-sync_macos_arm64), so cd build && shasum -a 256 -c checksums.txt (or sha256sum -c) verifies as-is, and the guard fails unless it lists exactly the cross binaries present and every hash matches. The release checksums.txt that CI uploads keeps bare names for install scripts and self-update. Behaviour change: the default --kind-suggestions-dir moved from build/kind-suggestions to .cache/kind-suggestions, so build/ 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 to opensource.simtabi.com/documentation/ichava/... and the opensource.simtabi.com/products/ichava/... homepage, which answer 404 for every ichava page, now go to the matching docs/*.md on GitHub, or to the repository itself. Each target was checked to exist on that repository's main. The manifest's homepage and support.docs follow.

  • The author and contact address is ichava@simtabi.com (W-11, Sim 2026-11-03), replacing the community address opensource@simtabi.com wherever this repository named it. Vulnerability reports still go to security@simtabi.com. The test that guards the address expects ichava@ now.

  • The SVG policy's _always_blocked_comment describes 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.yml is 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) build build/icon-sets-sync and run it against config/; build is
    build-local; pull downloads and verifies the Go modules; shell opens a host shell with
    build/ on PATH; ui serves on 127.0.0.1:7878; nuke is clean plus Go's test cache;
    test, test-go, lint and format use the host's Go, and lint adds golangci-lint, for issues
    new since main, when it is installed. The shipped configs still name each pack /work/<pack>;
    see docs/installation.md for the /work link 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.yml and release.yml that runs Go sets GOPRIVATE and GONOSUMDB to github.com/ichava/*, so no build of ours feeds a tag to proxy.golang.org or sum.golang.org.

  • The docs offer the signed release binaries and the Composer package, not go install. The README, docs/release.md and the go.mod comment dropped the go install path, because the Go module proxy serves its cached pre-publication copies of src/v0.1.0 to src/v0.1.2. Building from source is git clone plus go -C src build. TestDocsOfferNoGoInstall reads the README, CONTRIBUTING.md and docs/, fails on a go install of 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.author now defaults to ichava-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, .rpm and .apk packages GoReleaser builds carried a personal name and address as maintainer; it is now Simtabi 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, the Dockerfile and docs/, fails on a personal name or address, and fails if it read fewer than 11 files.

  • The ErrRecipePending comments say who reads it now: nobody outside this module. They described a cross-repository contract with icon-sets-bundled's sync-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.mjs and policy.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.md licence marker. The licence-text sink recognises a hand-supplied licence directory by a lowercase source.md only, 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 only SOURCE.md is 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 neither SOURCE.md nor Source.md counts where the Linux runners would not count it. sinks.LegacyHandSuppliedMarker goes with it; a test case shows the old code kept a SOURCE.md folder and this one replaces it.

  • The ICHAVA_DEV_CONFIG_DIR and ICHAVA_DEV_LOG_LEVEL environment variables, the Python toolkit's names for ICHAVA_CONFIG_DIR and ICHAVA_LOG_LEVEL. Nothing in the ichava packages sets either. The settings registry's LegacyEnv field goes with them. log_level still accepts warning, because ichava/core's .env.example sets ICHAVA_LOG_LEVEL=warning under the same name.

  • Choosing emoji-sets and openmoji-sets by pack name. A pack config names its recipe in recipe; auto picks generic for an npm or github-* source and nothing otherwise. config/icon-sets-emoji.json (and its embedded copy) now says "recipe": "emoji-sets"; icon-sets-openmoji already said so. Without the field, the emoji pack's url source fails with "no recipe wired" instead of running a recipe picked by its name.

  • config.ResolveConfigDir, the earlier subset of ResolveConfigSource kept for its callers. None was left outside its own tests, which now drive ResolveConfigSource.

  • Reading items, kinds and categories from the top level of a pack's config.json. The kind reader takes them from metadata.data only, and an empty metadata.data section no longer falls through to the top level. ichava/core's PackConfigData::section() dropped the same fallback, and this reader mirrors it so a suggestion resolves a pack exactly as core does. Measured over the 24 resources/assets/svg/config.json files in the ichava packages: none uses the top level, and 21 carry metadata.data sections.

  • (*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, call RunContext instead.

  • OpenMoji in the emoji-sets recipe, and the emoji.openmoji_version setting (ICHAVA_EMOJI_OPENMOJI_VERSION). icon-sets-emoji ships Twemoji only since its OpenMoji copies were removed; OpenMoji is icon-sets-openmoji's, built by openmoji-sets, which is unchanged. emoji-sets now builds the Twemoji pipeline and its commit pipeline, so the weekly emoji sync can no longer write openmoji-color and openmoji-black back into that pack. The setting, its schema property (a pack config that still sets settings.emoji.openmoji_version fails to load), settings.DefaultOpenMojiVersion and recipes.DefaultOpenMojiVersion go with it; no config in the ichava packages set it. TestEmojiSetsBuildsTwemojiOnly was red against the old recipe.

  • The inline 27-row presentation-value table (TestPresentationAttributeValueGuards). Every row, compared by name, value and keep, is in the shared testdata/presentation-vectors.json, which TestSharedVectorCases runs.

  • The imt alias binary is retired; use icon-sets-sync, same commands. imt was the same program (cli.Execute) under a short name, and building it doubled build/dist: make cross now leaves 19 binaries and checksums.txt instead of 38, and each release archive and Linux package ships icon-sets-sync alone. src/cmd/imt and its GoReleaser build, universal-binary and archive entries are gone, and TestOnlyOneEntrypoint and TestGoReleaserBuildsOneBinary fail if a second entrypoint or binary comes back. Anyone with imt on PATH can switch to icon-sets-sync or add alias imt=icon-sets-sync.

  • The Docker surface. No pack has run the image since sync-pack.yml began building the binary
    from source, so it is gone:

    • Dockerfile, .dockerignore and docker-compose.yml;
    • .github/workflows/docker.yml, which published ghcr.io/ichava/icon-sets-sync on every push to
      main. Images already in GHCR are untouched;
    • the Tests workflow's Docker image job;
    • the docker ecosystem in .github/dependabot.yml;
    • the Makefile's docker compose and golang container invocations (the targets stay, see
      Changed).
  • The Python leftovers. The .gitignore Python 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 --commit flag's help no longer cites the Python.

  • The three Python scripts CI ran. The Changelog order job and release-please's changelog close
    are now src/internal/tools/changelog check and close, with the same messages, exit codes and
    GITHUB_OUTPUT lines; on this CHANGELOG the closed file is byte-identical to the Python's. The web UI
    accessibility job serves its fixture upstream with src/internal/tools/staticserve instead of
    python3 -m http.server. TestNoWorkflowOrScriptInvokesPython fails if a workflow or script
    under .github/ calls Python again.

  • TestNoDockerOrPythonSurface and TestNoWorkflowBuildsOrPublishesAnImage fail if a Dockerfile,
    a compose file, a .dockerignore, a .py file, a Docker workflow, a docker/ action,
    docker build/run/push, ghcr.io, a docker Dependabot ecosystem or a docker call in the
    Makefile comes back. TestReleaseRunsOnlyOnAConfirmedDispatch holds release.yml to
    workflow_dispatch with a required confirm.

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.json and the embedded one) drop it from the recipe enum, so a config that still says bundled fails to load with /recipe: value must be one of 'auto', 'generic', 'emoji-sets', 'manifest-pack'. TestTheRemovedBundledRecipeIsRejected pins it, and fails with bundled put back in the enum.
  • Choosing manifest-pack by pack name. A config for icon-sets-bundled with no recipe (or "auto") no longer reaches manifest-pack; it has to say "recipe": "manifest-pack". TestManifestPackIsNotReachedByItsFormerNameOrByPackName fails if the name dispatch returns. The deprecation notice this path and the old recipe name logged on every run, and the warning config validate printed for them, go with them (recipes.Deprecation, recipes.RemovalRelease, recipes.RecipeBundled).
  • The hidden bundled audit command. Use imt manifest-pack audit; bundled is now an unknown command.
  • The /bundled/audit web routes (the page, the POST that starts an audit, and /{id}/manifest.json). Use /manifest-pack/audit; the old address answers 404, which TestRemovedAuditRouteIsNotFound pins.
  • The ichava-maintainer-toolkit binary name. The image no longer ships the symlink and the binary no longer prints a notice under that name; run icon-sets-sync or imt. 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 (use app.ManifestPack), app.AuditBundled (use app.AuditManifestPack), (*web.AppEngine).AuditBundled (use AuditManifestPack), version.LegacyName, and the unused cli.resolveAll (use app.ResolveSets). All are in internal packages, 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.json state 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 new StatedFacts step, run before the version stamp in every commit chain, compares the SVGs written with those at HEAD (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.ListTree reads 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). gitx gains RevParse, CommitsNotIn, CherryPick and PushBranchLease; PushBranch is deprecated in favour of PushBranchLease and still works.

  • A namespace declaration nothing uses is dropped. The policy strips sodipodi:docname but left the xmlns:sodipodi that 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, every xmlns:<prefix> whose namespace no remaining element or attribute uses is removed; the default xmlns stays, and so does any prefix in use (xmlns:xlink while an xlink:href survives). 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 (unused xmlns:xlink), icon-sets-openmoji 2 (the Cornwall flag), and two private packs 3,037 between them (unused xmlns:xlink, and 151 files with Inkscape's sodipodi, inkscape, cc, dc and rdf). The other 18 packs: 0. namespace_test.go covers the drop and the survivors.

  • The US and UM flags keep their stars. Both draw the fifty stars as one <marker> repeated by marker-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, orient and markerUnits, beside id, viewBox, preserveAspectRatio and the presentation attributes it already allowed. Everything else holds: a script or on* handler inside or on a marker is stripped, an external href is stripped, and marker-start/-mid/-end still take only none or url(#fragment). The shared vector table, testdata/presentation-vectors.json, gains 8 attribute rows and a documents section of 6 whole-document rows, and is byte-identical in core, react-icon-browser and js-icon-components; TestSharedVectorCases and TestSharedVectorDocuments run it. The vendored svg-policy.json is ahead of core until core takes the same bytes.

  • keycap: * is no longer lost to keycap: #. Both names slug to keycap. Without Preserve (the deprecated emoji-sets path) the later file was renamed over the earlier, and with it the later took the name and the earlier went to uncategorised/, while the shipped packs carry keycap: # as keycap.svg. Slugs are now resolved once per taxonomy (ResolveNameSlugs), shared by Categorise and Indexer: on any collision the first record in emoji-test.txt order keeps the base slug, and a later one takes DisambiguatedSlug of its own name, punctuation spelled out from a small named-symbol map and anything else as u plus its hex code point, with the codepoint key as the last resort. So keycap: # stays keycap and keycap: * becomes keycap-asterisk, in both recipes, in codepoints.json and in names.json. In Unicode 16.0 and 17.0 this is the only collision (measured over emoji-test.txt). One emoji shipped twice still goes to uncategorised/, the exact match keeping the name; between two equal matches the first now keeps it. TestCategoriseKeepsBothSidesOfASlugCollision replaces the test that pinned the later one winning.

  • A real emoji-sets sync keeps Twemoji's licence. FindLicences recorded the licence files inside the fetched tree, and Categorise wipes that tree's root and refills it with SVGs only. For Twemoji the root is the whole npm package, so the LICENSE was deleted before LicenceTexts read it, and every non-dry icon-sets-emoji sync since the first release failed at LicenceTexts twemoji: open …/package/license. A dry run never read the file, so it never showed. FindLicences now 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 of icon-sets-emoji against 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. TestFindLicencesSurvivesARebuiltTree fails 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 explicit emoji.unicode_version still wins. TestTheDerivedTaxonomyNeverFallsBelowTheDefault covers 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 as 1F3F3.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 to uncategorised/. With Preserve, an exact match now beats an FE0F-less one in either order. The deprecated emoji-sets path, which has no Preserve, 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 --port had been given. It now moves to the next free port and says so, as serve does; an explicit --port still fails.

  • A dry run writes nothing into the pack checkout. A sync --dry-run of icon-sets-openmoji wrote 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, and would_write) without writing; the fetched tree stays in the run's temporary directory. This covers sync, 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, so git diff there 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-openmoji from 15.1.0 to 17.0.0 logged categorise 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, against 2600 FE0F in emoji-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-sets now matches a name regardless of FE0F, keeps every other icon as uncategorised/<upstream-name>.svg with a warning and an uncategorised count, and keeps both icons when two emoji slug to one name (keycap: # and keycap: * overwrote each other). Measured on 17.0.0: 3,944 categorised, 551 uncategorised, 0 dropped. The deprecated emoji-sets recipe is unchanged and a test pins it.

  • The taxonomy follows the OpenMoji release. openmoji-sets derives emoji.unicode_version from the release (17.0.0 uses 17.0) unless it is set explicitly. Unicode 17.0 publishes emoji-test.txt under Public/17.0.0/emoji/, where the old Public/emoji/17.0/ path answers 404, so the URL follows that layout from 17.0; 16.0 and earlier are unchanged.

  • serve and the other commands find the registry when run from src/. The README's go -C src run ./cmd/icon-sets-sync serve runs in src/, 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, then ICHAVA_CONFIG_DIR, then config/ beside the binary, then ./config if it holds packs.json, then the config/ of this repository found by climbing at most 5 parents from the working directory (stopping at .git, next to src/go.mod), logged at info level. A missing registry now names ICHAVA_CONFIG_DIR and --config-dir, which the UI banner shows. make serve runs the documented command.

  • Every form POST in the web UI was refused with 403 "Cross-origin request refused". Referrer-Policy: no-referrer makes browsers send Origin: null on a same-origin form POST, and the origin check rejected null outright. The policy is now same-origin, and Origin: null is accepted only with Sec-Fetch-Site: same-origin; a bare or cross-site null is 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: the workspace setting when <workspace>/<pack> exists, then /work/<pack>, then the sibling checkout beside this repository, else the configured value with a message naming ICHAVA_WORKSPACE. The pack page and config print --pack ... (pack_root_origin) show where the root came from; sync-pack.yml is unchanged. The config_dir and workspace help text now matches.

  • A licence declared by a pack on the generic recipe is enforced. Only the openmoji-sets recipe passed the pack's licence to 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.MkdirAll and archive extraction without a bound; ../ in the audit's set name surveyed and fetched against a directory outside the pack. The new internal/core/pathguard.Join is 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 with filepath.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' own safeJoin is folded in. Behaviour change: an archive member with an absolute name is refused rather than clamped inside the destination, and ResolveFile refuses 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). setStringAtPath sized its buffer as len(data)-(end-start)+len(encoded) unchecked. spliceSize bounds 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 pass isLocalURL (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(, @import and a quote mismatch such as url("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 covers fill, stroke, filter, clip-path, mask and marker-*, 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) and Policy.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). GetJSON reads at most 32 MiB and Download at 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 https to http, to a scheme other than http(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). GetJSON and Download refuse 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.com and unicode.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-wide allowed_hosts setting (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.com and release-assets.githubusercontent.com are not on the default list, because no shipped recipe downloads a release asset. Behaviour change: a private pack that fetches from another host now needs allowed_hosts.
  • Upstream URLs must be https (P2-G6). A pack config with a plain-http version_check_url or source.archive_url fails 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://localhost and http://[::1] are exempt on the first request so tests can use a local server. Every shipped config was already https.
  • The SVG policy keeps display, color and marker-start/-mid/-end, each behind a value guard. A dry run of OpenMoji 17 stripped display="none" from 13 subdivision flags, so their hidden construction shapes would render, plus color from 3 files and marker-end from 1. display survives only as bare keywords (none, inline, block, inline flow-root): no parens, no url(), no escape. color goes through the CSS value guard. marker-* survive only as none or url(#fragment). So display="url(x)" and marker-end="url(https://e)" are still stripped, and so is Inkscape's sodipodi:docname. The vendored svg-policy.json gained the five names and is ahead of ichava/core's canonical copy until core and react-icon-browser take the same change.
  • npm pack runs with --ignore-scripts (S7), in both places it is called, so a git or local spec cannot run a prepare script 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=4495 is explained in this entry, not changed: would_change counts every file because the serialiser drops the trailing newline; violations counts files with something removed, and all 1,284 lose the root version="1.1" attribute. 13 files also lose display="none" (hidden construction shapes in subdivision flags would render), 3 color, and 1 each docname and marker-end. The first three no longer do (see Security); docname is Inkscape metadata and stays stripped.

Full changelog: https://github.com/ichava/icon-sets-sync/blob/main/CHANGELOG.md