Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .dev/tools/check-config-drift.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
// Canonical: ichava/js-ui-kernel config/check-config-drift.mjs. Copies must be
// byte-identical, and this script checks its own copy too: list it as one of the pairs.
//
// Usage: node scripts/check-config-drift.mjs <local>=<canonical> [<local>=<canonical> ...]
//
// <local> path in this repository, relative to the working directory
// <canonical> path inside ichava/js-ui-kernel, e.g. config/tsconfig.base.json
//
// Each canonical file is fetched from raw.githubusercontent.com and compared byte for byte.
// It fails on a mismatch, on a non-200 response, on an empty body and on an empty pair
// list, so it cannot pass by comparing nothing.
//
// ICHAVA_KERNEL_REF git ref to compare against (default: main)
// ICHAVA_KERNEL_RAW raw base URL (default: https://raw.githubusercontent.com/ichava/js-ui-kernel)
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';

export const DEFAULT_RAW = 'https://raw.githubusercontent.com/ichava/js-ui-kernel';

/** Parses `local=canonical` arguments. Throws on a malformed one. */
export function parsePairs(args) {
return args.map((arg) => {
const eq = arg.indexOf('=');
if (eq <= 0 || eq === arg.length - 1) {
throw new Error(`expected <local>=<canonical>, got "${arg}"`);
}
return { local: arg.slice(0, eq), canonical: arg.slice(eq + 1) };
});
}

/**
* Compares each pair. Returns { checked, failures }, where failures is a list of
* human-readable reasons. `fetchImpl` and `readImpl` are injectable for tests.
*/
export async function checkDrift({
pairs,
ref = 'main',
raw = DEFAULT_RAW,
fetchImpl = globalThis.fetch,
readImpl = (path) => readFile(path),
}) {
const failures = [];
if (pairs.length === 0) {
failures.push('no files to compare: pass at least one <local>=<canonical> pair');
return { checked: 0, failures };
}

let checked = 0;
for (const { local, canonical } of pairs) {
const url = `${raw}/${ref}/${canonical}`;
let localBytes;
try {
localBytes = Buffer.from(await readImpl(local));
} catch (error) {
failures.push(`${local}: cannot read the local copy (${error.message})`);
continue;
}

let response;
try {
response = await fetchImpl(url);
} catch (error) {
failures.push(`${local}: cannot fetch ${url} (${error.message})`);
continue;
}
if (response.status !== 200) {
failures.push(`${local}: ${url} answered ${response.status}, expected 200`);
continue;
}
const remoteBytes = Buffer.from(await response.arrayBuffer());
if (remoteBytes.length === 0) {
failures.push(`${local}: ${url} returned an empty body`);
continue;
}

checked += 1;
if (!localBytes.equals(remoteBytes)) {
failures.push(
`${local}: differs from ${canonical} at ${ref} ` +
`(${localBytes.length} bytes here, ${remoteBytes.length} canonical). ` +
`Copy the canonical file over it; change the canonical in ichava/js-ui-kernel first.`,
);
}
}
return { checked, failures };
}

async function main() {
let pairs;
try {
pairs = parsePairs(process.argv.slice(2));
} catch (error) {
console.error(`check-config-drift: ${error.message}`);
return 2;
}
const ref = process.env.ICHAVA_KERNEL_REF || 'main';
const raw = process.env.ICHAVA_KERNEL_RAW || DEFAULT_RAW;
const { checked, failures } = await checkDrift({ pairs, ref, raw });

for (const failure of failures) console.error(`check-config-drift: ${failure}`);
if (failures.length > 0 || checked !== pairs.length) {
console.error(`check-config-drift: FAILED, ${checked} of ${pairs.length} file(s) compared`);
return 1;
}
console.log(`check-config-drift: ${checked} file(s) identical to ichava/js-ui-kernel@${ref}`);
return 0;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await main();
}
5 changes: 5 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ jobs:
- name: Vitest
run: npm run test:js

# eslint.base.js and .dev/tools/{sync-vendored-library,check-config-drift}.mjs are copies of
# ichava/js-ui-kernel's config/; this fails when one stops matching (re-audit F-18).
- name: Shared config matches the kernel
run: npm run check:config-drift

# The committed bundle is what hosts receive, so it must at least build from this tree.
# The vendored react-icon-browser build is checked against its manifest by Vitest above.
- name: Build the browser bundle
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k

### Removed

- `CreateCollection::rules()`: its two callers now use `StoreCollectionRequest`, which owns the rules (forward only).
- **The vendored-library test reads only `vendored.json`.** The fallback to the old `VENDORED.json` name is removed (forward only, owner 2026-11-05); every consumer writes the lowercase name since #124.
- **Payload schema 1.8.0: the `android-vd` (Android VectorDrawable XML) asset export is dropped**, by owner rule (Sim, 2026-11-04): the ichava icon ecosystem is strictly `*.svg`. `$defs/format` and `x-ichava-formats` lose `android-vd` (25 formats), `PayloadSchema::VERSION` is `1.8.0`, and `FormatRegistryTest` now fails if the id returns or if any asset export other than the SVG-carrying `css-mask`, `data-uri`, `flutter` and `swiftui` appears. 1.7.0 was never released, so no published payload named it.
- **Payload schema 1.7.0: the `png-zip` asset export is dropped**, by owner decision (Sim, 2026-11-04). `$defs/format` and `x-ichava-formats` lose `png-zip` (26 formats), `PayloadSchema::VERSION` is `1.7.0`, and `FormatRegistryTest` now fails if the id comes back. 1.6.0 was never released, so no published payload named it.
Expand All @@ -31,6 +32,7 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k

### Fixed

- The terms hierarchy endpoint counted icons under a hard-coded morph alias `icon`, so every count read 0 once core moved the alias to `ichava-icon`; it reads `Icon::getMorphClass()`.
- **The icon SVG rules in `ichava.scss` are valid CSS.** 26 selectors were written with `:deep(...)`, Vue single-file-component syntax a browser does not know, so it dropped every one of those rules from the compiled stylesheet. They are now the plain descendant selectors they meant (`.icon-svg svg`, `.icon-svg.icon-colored svg path`, ...), and `public/assets/css/inertia-app.css` is rebuilt. No element in the React browser or the Blade views carries `.icon-svg`, `.icon-svg-modal` or `.icon-colored` (0 matches in `resources/js`, `resources/views`, `src` and the vendored bundle), so nothing renders differently yet. `noDeepSelectors.test.ts` fails on any `:deep(` in a stylesheet outside a `.vue` file; it failed on both files before the fix.
- **Re-vendored `ichava/react-icon-browser` at `e60b526`** (react-icon-browser#207): the US and UM flags keep their stars. Their fifty stars are one `<marker>` repeated by `marker-mid`, and the vendored SVG policy stripped `<marker>`, so the browser drew an empty canton. The policy (sha256 `49ee43ce`, ichava/core#166) now allows `<marker>` with `refX`, `refY`, `markerWidth`, `markerHeight`, `orient` and `markerUnits`; scripts, handlers and external references stay stripped. `vendored.json` names the commit, and the committed bundle under `public/assets` is rebuilt from it.
- **Per-route rate limits now apply (S3).** `getRateLimit()` read `rate_limiting.<bucket>`, which the shipped config always sets, so the limit each route declared (10, 30, 60, 120, 300) was never used, and Laravel's string `throttle:` shares one counter per caller across routes. The new `ichava.throttle` middleware takes the route's declared limit, lets `rate_limiting.<bucket>` only lower it, reads that per request, and counts per route. Turning `rate_limiting.enabled` on also crashed route loading (a nested array in the `cache` group's middleware, "Array to string conversion"); fixed. Behaviour change for hosts that enable the switch: `icons/filters`, `tree` and `statistics` now answer 429 after 60 requests a minute, not 300.
Expand All @@ -46,6 +48,16 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k

### Changed

- **Blade components carry this package's prefix** (forward only): `<x-ichava-icon-browser::prepaint />`, `::sri-asset`, `::ui-icons` and `::test-icons` replace the `ichava::` aliases, so a host that turns core's short prefix off (`ichava.ichava-core.components.short_prefix` null) still renders them. Docblocks show `<x-ichava-core::icon>`, core's canonical tag. `tests/ShortPrefixOff` boots a host with the short prefix off.
- **The demo test-icons component follows core's `components.test_icons`**; `ichava.icon-browser.test_component_enabled` is gone (forward only). Core's default is on in `local` only (it was on everywhere here); `ICHAVA_TEST_COMPONENT_ENABLED` still sets it.
- **Core's contracts, not its classes**: `UpdateStatusApiController` and `InertiaBrowserController` type-hint `Contracts\ChecksPackUpdates` and `Contracts\DatabaseOperations` (core#172); `UpdateStatusApiTest` binds the contract.
- **Run `php artisan migrate` after updating** (`docs/installation.md`): core's `2026_11_05_000001_namespace_icon_morph_alias` moves the icon morph alias to `ichava-icon`.
- **Middleware aliases and groups carry the package name** (re-audit F-11, forward only): `ichava-icon-browser.session`, `.security`, `.json`, `.log`, `.validate`, `.guard`, `.cache-admin`, `.admin`, `.throttle`, `.handle-inertia`, and the groups `ichava-icon-browser.api`, `.web` and `.inertia`. The bare `ichava.*` names are gone, with no forwarding alias. A host route guarded with `->middleware('ichava.admin')` changes to `ichava-icon-browser.admin`. The `ichava.admin` *ability* (a Gate) keeps its name. `HttpLayerConventionsTest` reads the router's live map.
- **One query service for package detail** (F-08): `Support\PackageDetails` returns a `Data\PackageDetailsData` that the API's `package()` and the Inertia `PackageController::show()` both render; neither controller queries. Table names come from `Support\IconTables` (`ICONS` is core's `Icon::TABLE`; the term table and pivot are held to `IconTerm` by a test).
- **FormRequests with `toData()` at the edge** (F-09): `RecordCommandRequest`, `RecordHistoryRequest`, `StoreCollectionRequest` and `UpdateCollectionRequest` replace the six inline rule sets, each with a `final readonly` DTO in `src/Data/`. `IchavaFormRequest` keeps the API's validation body (`success: false`, `error`, `errors`) for JSON callers and Laravel's redirect for pages.
- **Collaborators are injected, not reached for** (F-10): `IchavaApiSecurity` takes core's `SecurityNonce` and `AuditLogger`; `CacheAdminGate` is an injectable service taking `HostUser` (its static `refusal()`/`allows()` are instance methods now); `IconResource`/`IconCollection` take `IconSnippets` through `withSnippets()`; `BrowserBootstrap` reads the locale from the `Translator`.
- **The browser opens on every pack the host has installed** (Q42): the Inertia page's `packages` (what core registered) becomes the library's `defaults.packages`, and the re-vendored react-icon-browser names no pack.
- **Each Inertia page is its own chunk** (F-12): `import.meta.glob` is no longer eager; chunks are content-addressed (`inertia-app-[name]-[hash].js`) since only the entry carries `?v=`, and `npm run clean` removes them.
- **The payload schema's `$id` resolves to the file**: `https://raw.githubusercontent.com/ichava/icon-browser/main/resources/schema/icon-browser.schema.json` (it named a github.com path that answers 404). No field changes, so the version stays 1.8.0. `docs/tools/payload-schema.md` says how react-icon-browser and js-icon-components pin their copies.
- **The vendored manifest is `resources/vendor/react-icon-browser/vendored.json`, no longer `VENDORED.json`** (owner rule: file names are lower-case slugs). `.dev/tools/sync-vendored-library.mjs` writes only the new name, and `vendoredLibrary.test.ts` reads it first, falls back to the old one until after v0.1.0, and fails if the old name returns. Re-synced from react-icon-browser `7188f57`, the commit already vendored: the manifest's bytes and every bundle file are unchanged, and `npm run build:prod` leaves `public/assets` as committed.
- **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.
Expand Down
14 changes: 4 additions & 10 deletions config/icon-browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,9 @@
'resolver' => null,
],

/*
|--------------------------------------------------------------------------
| Demo / test components
|--------------------------------------------------------------------------
| The `<x-ichava::ichava-test-icons>` and `<x-ichava::ichava-ui-icons>`
| shorthand components are registered by this package. Toggle off in
| production if you don't want them rendered.
*/
'test_component_enabled' => env('ICHAVA_TEST_COMPONENT_ENABLED', true),
// The demo `<x-ichava-icon-browser::test-icons>` component follows core's
// `ichava.ichava-core.components.test_icons` (ICHAVA_TEST_COMPONENT_ENABLED).
// This package keeps no key of its own for it.

/*
|--------------------------------------------------------------------------
Expand Down Expand Up @@ -269,7 +263,7 @@
'enabled' => env('ICHAVA_RATE_LIMITING_ENABLED', false),
'browser' => env('ICHAVA_BROWSER_RATE_LIMIT', 300),
'api' => env('ICHAVA_API_RATE_LIMIT', 600),
// Global floor applied to the `ichava.api` middleware group on top of
// Global floor applied to the `ichava-icon-browser.api` middleware group on top of
// per-endpoint limits. Acts as a safety net regardless of `enabled`.
'api_floor' => env('ICHAVA_API_FLOOR', 300),
],
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Relocated from the README, which is now a slim pointer.
|---|---|
| Visual browser | `@ichava/react-icon-browser`, served as Inertia pages at `/{prefix}/icons`. Search, filter by pack/variant/category, copy and export, collections, theme and appearance. See *The browser UI* below. |
| REST API | Endpoints for icons, packages, terms, preferences, command history, cache and statistics. |
| Middleware | The `ichava.api` and `ichava.web` groups, plus per-middleware aliases: `ichava.guard`, `ichava.security`, `ichava.json`, `ichava.log`, `ichava.session`, `ichava.validate`. |
| Middleware | The `ichava-icon-browser.api` and `ichava-icon-browser.web` groups, plus per-middleware aliases: `ichava-icon-browser.guard`, `ichava-icon-browser.security`, `ichava-icon-browser.json`, `ichava-icon-browser.log`, `ichava-icon-browser.session`, `ichava-icon-browser.validate`. |
| Hybrid auth | `HostCapabilities` detects Sanctum and sessions, then adapts the stack. |
| Blade views | `<x-ichava::layouts.app>` and `<x-ichava::layouts.browser>`, plus the `<x-ichava::ichava-test-icons>` and `<x-ichava::ichava-ui-icons>` demo components. |
| Blade views | `<x-ichava::layouts.app>` and `<x-ichava::layouts.browser>`, plus the `<x-ichava-icon-browser::test-icons>` and `<x-ichava-icon-browser::ui-icons>` demo components. |
| `ui-icons` pack | The bundled icon set the browser pages use. |
| Asset publish | `vendor:publish --tag=ichava::icon-browser-assets` copies the pre-built bundle into `public/vendor/ichava/`. |
| `inject-npm-scripts` | Adds the `ichava:dev`, `ichava:build` and watch scripts to the host application's `package.json`. |
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ return [
'store' => env('ICHAVA_LIBRARY_STORE', 'database'),
],

// Demo Blade components (<x-ichava::ichava-test-icons>, <x-ichava::ichava-ui-icons>)
'test_component_enabled' => env('ICHAVA_TEST_COMPONENT_ENABLED', true),
// The demo <x-ichava-icon-browser::test-icons> component follows core's
// `ichava.ichava-core.components.test_icons` (ICHAVA_TEST_COMPONENT_ENABLED).

// Multi-domain restriction (empty = every domain)
'domains' => env('ICHAVA_DOMAINS', []),
Expand Down
6 changes: 3 additions & 3 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The guard the user is read from is core's `ICHAVA_AUTH_GUARD` (`ichava.ichava-co

| Key | Default | Effect | Config |
|---|---|---|---|
| `ICHAVA_TEST_COMPONENT_ENABLED` | `true` | Register the bundled `<x-ichava::ichava-test-icons>` and `<x-ichava::ichava-ui-icons>` Blade components | `ichava.icon-browser.test_component_enabled` |
| `ICHAVA_TEST_COMPONENT_ENABLED` | `local` env only | Register the demo `<x-ichava-icon-browser::test-icons>` component (core's switch; `<x-ichava-icon-browser::ui-icons>` is always registered) | `ichava.ichava-core.components.test_icons` |

## Multi-domain support

Expand All @@ -54,7 +54,7 @@ The guard the user is read from is core's `ICHAVA_AUTH_GUARD` (`ichava.ichava-co
| Key | Default | Effect | Config |
|---|---|---|---|
| `ICHAVA_API_PRETTY_PRINT` | `true` | Indented JSON responses | `ichava.icon-browser.api.pretty_print` |
| `ICHAVA_API_CORS_ENABLED` | `true` | Apply CORS headers from the `ichava.api` middleware group | `ichava.icon-browser.api.cors.enabled` |
| `ICHAVA_API_CORS_ENABLED` | `true` | Apply CORS headers from the `ichava-icon-browser.api` middleware group | `ichava.icon-browser.api.cors.enabled` |
| `ICHAVA_API_CORS_ORIGINS` | `APP_URL` | Allowed origins: one, or a comma-separated list. One is sent as is; with several the request's `Origin` is echoed when listed. Never `*` unless you set it | `ichava.icon-browser.api.cors.allowed_origins` |

## HTTP request size guard
Expand All @@ -70,7 +70,7 @@ The guard the user is read from is core's `ICHAVA_AUTH_GUARD` (`ichava.ichava-co
| `ICHAVA_RATE_LIMITING_ENABLED` | `false` | Master switch for per-route throttle middleware | `ichava.icon-browser.rate_limiting.enabled` |
| `ICHAVA_BROWSER_RATE_LIMIT` | `300` | Browser-route limit figure (requests per minute) | `ichava.icon-browser.rate_limiting.browser` |
| `ICHAVA_API_RATE_LIMIT` | `600` | Ceiling for every API route's own limit (requests per minute); it can lower a route's declared limit, never raise it | `ichava.icon-browser.rate_limiting.api` |
| `ICHAVA_API_FLOOR` | `300` | Global throttle floor on the `ichava.api` middleware group, applied regardless of the master switch | `ichava.icon-browser.rate_limiting.api_floor` |
| `ICHAVA_API_FLOOR` | `300` | Global throttle floor on the `ichava-icon-browser.api` middleware group, applied regardless of the master switch | `ichava.icon-browser.rate_limiting.api_floor` |

## See also

Expand Down
Loading
Loading