@hearthforge/repo-guards
v0.1.0
Published
Repository guards every HearthForge repo runs in CI — first: no tracked file names the maintainer's personal namespace
Maintainers
Readme
@hearthforge/repo-guards
Repository guards every HearthForge repository runs in CI, published once so each repository judges its tree with the same code instead of a vendored copy.
Security note: a leak guard, not a secret scanner
The namespace guard stops the maintainer's personal namespace (a registry owner, a package scope, a repository path) from landing where the hearthforge organisation's belongs. It does not look for credentials, tokens or keys. For those, use gitleaks and GitHub secret scanning with push protection.
Use
pnpm add -D @hearthforge/repo-guards{ "scripts": { "test:namespace": "hf-repo-guards namespace" } }hf-repo-guards namespace runs from anywhere inside the repository and judges every tracked file (git ls-files — what CI checks out, never untracked scratch):
- every file path, so a file named after the handle is found even when its content is clean;
- every line of every file the config does not skip, as written and as decoded.
Exit codes: 0 clean, 1 findings (each as file:line: token — excerpt), 2 the guard could not run (not a repository, nothing tracked, a bad config, an unknown argument). A guard that cannot run never reports clean.
What is a hit
The spellings are the HearthForge spec in src/personal-namespace.mjs — the one place that names them. The engine (src/namespace-matcher.mjs) knows no handle of its own:
- a handle spelling is a hit as its own token — separated by any non-alphanumeric character or a camelCase hump — in any case, with any trailing digits;
- a surname spelling is a hit anywhere;
- the maintainer's GitHub login is exempt only as a mention:
@<login>or the profile linkgithub.com/<login>, and only where the login ends. Followed by/,\,_,-, or by.,:,@and a name, it is a path, a host, an image tag or a mailbox, and a hit. A bare login is a hit too:OWNER=<login>is a value some tool builds a path from. - the owner's legal name is not a namespace — the CLA, LICENSE and copyright lines must name the rights holder.
Each line is judged as written and again decoded: percent-encoding, HTML character references and JavaScript escapes undone, NFKC-normalised, invisible characters removed and common look-alike letters folded onto Latin. The exact rules — and what is deliberately out of scope, base64 included — are the header of src/namespace-matcher.mjs; this README does not restate them. A symlink is judged by its target string and never followed; a UTF-16 file (with a byte-order mark) is decoded.
Exemptions: repo-guards.config.json
At the repository root (or --config <path>, relative to the root):
{
"namespace": {
"skip": [{ "file": "fixtures/blob.bin", "reason": "binary fixture; a UTF-8 read of it is noise" }],
"allow": [
{
"file": ".github/workflows/cla.yml",
"line": "allowlist: \"<login>,dependabot[bot]\"",
"reason": "the CLA action matches bare GitHub logins"
}
]
}
}allowexempts that exact line (compared trimmed) in that file only. Every entry needs areason.- An entry that exempts nothing — the line is gone or is no longer a hit — is itself a finding, and so is a
skipnaming an untracked file. A stale exemption is a hole waiting for the next line. skipexcludes a file's content; its path is still judged. Preferallow: a skipped file is a file nobody reads.- The config file is judged too, raw, line by line: the only hit it may carry is a line that is exactly one
"line": "…"member naming an allow entry — so keep it formatted one member per line. Unknown keys, blank reasons and duplicate entries are refused.
API
import { createNamespaceMatcher, findNamespaceFindings, parseNamespaceConfig, personalNamespaceTokens } from "@hearthforge/repo-guards";createNamespaceMatcher({ tokens, anywhere, logins }) compiles any spec into a pure { tokens(line), matches(line) }; findNamespaceFindings({ files, config, tokens }) is the guard's decision with no I/O.
Changing the spec
Edit src/personal-namespace.mjs and its table (test/personal-namespace.test.mjs) here, add a changeset, release. Consuming repositories pick the new version up as an ordinary dependency bump; nothing is copied.
Releasing the first version
npm Trusted Publishing can only be configured on a package that already exists, so the release workflow cannot publish 0.1.0 of a brand-new name over OIDC. Before the "Version Packages" PR that carries it is merged:
- From that PR's head, the owner publishes the one version by hand:
pnpm --filter @hearthforge/repo-guards publish --access public, with a passkey. - On npmjs.com, add the Trusted Publisher: repository
hearthforge/hearthforge-sdk, workflowpublish.yml, environmentnpm. - Verify it anonymously with
npm view @hearthforge/[email protected] dist.tarball, before any repository pins it.
The workflow's pnpm publish -r then skips the already-published version. Check whether the release step still created the @hearthforge/[email protected] tag and GitHub release; if not, create them by hand. Every later version goes through the workflow.
