Skip to content

chore(security): remove brand-owned DNS domains from source and shipped artifacts (1.4.1) - #335

Merged
andrei-hasna merged 5 commits into
mainfrom
scrub/owned-domains-1.4.1
Jul 27, 2026
Merged

chore(security): remove brand-owned DNS domains from source and shipped artifacts (1.4.1)#335
andrei-hasna merged 5 commits into
mainfrom
scrub/owned-domains-1.4.1

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

@hasna/connectors@1.4.0 shipped to public npm carrying brand-owned apex
hostnames. This removes every occurrence from source and from everything that
ships, and bumps to 1.4.1. Ship-forward was chosen deliberately over unpublishing
1.4.0; 1.4.0 will be deprecated on npm pointing at 1.4.1.

Positions only below — no owned value appears in this description, the commit
message, or the diff.

How each site was handled

File Treatment
connectors/zendesk/src/utils/config.ts config-driven. The remote API URL had a hardcoded deployment host as its fallback default. There is now no shipped default. getRemoteApiUrl() throws an actionable error naming ZENDESK_REMOTE_API_URL and config set-remote-url; a new findRemoteApiUrl() returns undefined for callers that must not throw.
connectors/zendesk/src/cli/index.ts config show switched to findRemoteApiUrl() so it renders not set instead of throwing; option description and a section comment now name the env var.
connectors/zendesk/nginx.conf, nginx-connector.conf placeholder host (reserved example domain), consistent with the placeholder already used across ~870 files in this repo.
connectors/zendesk/Makefile config-driven. New overridable REMOTE_API_URL ?=; the deploy banner prints it or an explicit not-set marker.
connectors/zendesk/README.md, CLAUDE.md, SCAFFOLD.md prose now describes the configuration mechanism rather than naming a host.
connectors/googlephotos/src/cli/index.ts illustrative comment value replaced with a reserved example value.
connectors/yousearch/README.md usage example replaced with a reserved example value.
SECURITY.md vulnerability reports route through GitHub Security Advisories instead of an email address at an owned domain — strictly better than a placeholder, since it keeps disclosure working.
package.json @hasna/events marked external in the CLI bundle (see below), version → 1.4.1.

The one that source review could not have caught

One owned name reached bin/index.js only as a literal inside a bundled
runtime dependency. It has zero occurrences in this repo's tracked source, so
every source-level grep passes while the shipped artifact carries it. This is the
same failure mode as the @hasna/tenants incident: the compiled artifact carried
what the source never did.

Fix: mark @hasna/events external in the CLI build. It is a declared runtime
dependency, so this matches the existing ink / react / chalk / conf
externals and npm resolves it at install time. Verified the built CLI still runs
and still registers its commands.

The literal remains present in that dependency's own published package, which is
a separate remediation, tracked separately.

Verification — packed tarball, not the working tree

The check that matters was run against npm pack output, extracted, with every
member scanned against the full owned-domain portfolio taken from the domains CLI
(1043 apexes after excluding IANA-reserved names):

published 1.4.0 tarball : 10 distinct owned hostnames, 30 occurrences, 12 files
1.4.1 candidate tarball : 0 distinct owned hostnames, 0 occurrences, 0 files
                          (20,411 members scanned)

Independent sweeps over the 1.4.1 tarball, all files including binaries: zero
hits for escaped-dot, percent-encoded, [dot]-obfuscated and base64 renderings
of the owned names.

Also confirmed the tarball did not lose shipped content. Diff of member lists
1.4.0 → 1.4.1 shows three removals, all of them fixes:

  • two stale .d.ts files whose sources were deleted in bd03877 — 1.4.0 shipped
    declarations for code that no longer exists, because dist/ was not clean at
    release time;
  • one bun install-cache blob from a connector's gitignored .test-home/, swept
    into 1.4.0 by files: ["connectors/"].

Tests

bun test        3969 pass, 0 fail, 35192 expect() calls, 449 files
bun typecheck   clean

connectors/zendesk is excluded from the root typecheck and carries 31
pre-existing errors. Verified the error set is byte-identical before and after
this change once line numbers are normalised — no regression introduced, none of
them touch the edited code.

Gate: contracts artifact-scan (hasna/contracts PR #31)

Run as a backstop, not as the check. Result is worth recording:

1.4.0 (leaky)    FAIL exit 1 — 177 domain entries, 288 host entries
1.4.1 (scrubbed) FAIL exit 1 — 176 domain entries, 288 host entries

It fails both, identically, on this package's legitimate connector catalog —
not on the owned names. Host count is unchanged by the scrub; domain count moves
by one. So for this repo the gate currently cannot distinguish leaky from clean
in either direction, and would have to be waived to be usable — at which point it
reports nothing. This is consistent with CONTRACT.md's own statement that
clause B is a prohibition rather than a count; it is a limitation, not a bug.

Follow-ups (not in this PR)

  • connectors/zendesk docs also name internal AWS EC2 / RDS / S3 resource
    identifiers in a public package. Not domains, so out of this PR's mandate, but
    the same class of exposure and worth a decision.
  • connectors/ shipping gitignored .test-home/ cache blobs is a packaging
    hygiene gap that files: ["connectors/"] will keep re-introducing.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

… artifacts

The published 1.4.0 tarball carried brand-owned apex hostnames. This replaces
every occurrence with neutral placeholders, or makes the value configuration-
driven where it is a real runtime endpoint.

- connectors/zendesk: the remote API URL had a hardcoded deployment host as its
  fallback default. There is now no shipped default; the value comes from
  ZENDESK_REMOTE_API_URL or `config set-remote-url`, and getRemoteApiUrl()
  throws an actionable error when unset. `config show` uses a new
  findRemoteApiUrl() so it still renders "not set" instead of throwing.
- connectors/zendesk nginx templates, Makefile, README, CLAUDE.md, SCAFFOLD.md:
  placeholders and env-var references instead of a real deployment host. The
  Makefile deploy banner reads an overridable REMOTE_API_URL.
- connectors/googlephotos, connectors/yousearch: illustrative comment/example
  values replaced with reserved example values.
- SECURITY.md: vulnerability reports now route through GitHub Security
  Advisories rather than an email address at an owned domain.
- package.json: mark @hasna/events external in the CLI bundle. One owned name
  reached bin/index.js only as a bundled dependency literal with no occurrence
  in this repo's source, so every source-level grep passed while the shipped
  artifact carried it. It is a declared runtime dependency, so this matches the
  existing ink/react/chalk/conf externals and keeps the events commands working.

Verified against the packed tarball rather than the working tree: npm pack,
extract, then scan every extracted member against the full owned-domain
portfolio. 1.4.0 tarball: 10 distinct owned hostnames in 12 files. 1.4.1
candidate tarball: zero.

Bumps to 1.4.1.
… resolution

Addresses two reviewer findings on the owned-hostname scrub.

SECURITY.md pointed at the GitHub Security Advisories form while private
vulnerability reporting was disabled on the repo, so an external reporter
following the link got a 404 and the removed security@ address left no
fallback. Private vulnerability reporting is now enabled on hasna/connectors,
which makes the linked URL resolve and renders the public report button; the
policy now states that any signed-in GitHub user can file, and adds a
no-details-in-public fallback for the case where the form is unreachable.

getRemoteApiUrl() changed from returning a hardcoded default host to throwing
when unconfigured, but no test referenced it, so the suite was byte-identically
green with the change reverted. Adds four tests that import the real config
module rather than re-implementing the priority logic, isolated through the
module's own profile override: unconfigured throws and findRemoteApiUrl() is
undefined, env var resolves, stored config resolves, env beats stored config.

Verified the unconfigured test fails on regression: reinstating a hardcoded
default in getRemoteApiUrl() turns it red (16 pass / 1 fail), and reverting
config.ts wholesale fails the file at import.

Suite: 3973 pass, 0 fail, 35200 expect() calls (bun test, after bun run build
as CI does). Typecheck clean.
The afterEach resolved the throwaway profile directory through getConfigDir(),
which only returns the test profile while the override is still set. Reordering
those two statements would have pointed the rmSync at the developer's real
default profile. Derive the path by name from getBaseConfigDir() instead, so
cleanup targets the test profile regardless of sequencing.

Suite: 3973 pass, 0 fail, 35200 expect() calls (bun test). Typecheck clean.
An adversarial review returned BLOCK-PUBLISH on the scrub commit. The scrub
itself was confirmed clean by an independent, more sensitive scanner; these are
the three defects it found alongside it, two of which the scrub introduced.

Three commands crashed instead of reporting an unset value. Removing the
remote API URL default left `remote url`, `remote status` and `remote health`
calling the throwing accessor outside their try blocks, so they exited with a
raw stack trace. `remote url` was the worst of them: its whole job is to tell
you whether the URL is set. They now go through a helper that reports the
missing configuration using the connector's own error convention, and `remote
url` reports "not set" like `config show` already did. Added a test that runs
the real CLI against a throwaway HOME; it fails on the pre-fix code (3 of 7)
and passes after, so it is a regression guard rather than decoration.

The new disclosure link was dead. SECURITY.md now routes reports to GitHub
Security Advisories, but private vulnerability reporting was disabled on the
repository, so the link 404s for anyone who is not a collaborator — the change
had removed the only working disclosure channel. Reporting is now enabled.
Also corrected two stale claims in the same file: the supported-version table
still said 0.2.x, and the credential path did not match what the code uses.

The scanned artifact was not the artifact that would ship. `prepublishOnly`
runs the test suite before npm packs, and the suite creates per-connector
`.test-home` sandboxes and lockfiles that `files: ["connectors/"]` then sweeps
in — 1.4.0 shipped a Bun install-cache blob exactly this way. `files` now
carries explicit negations, verified by packing after a full test run.

`scripts/check-package-secrets.ts` scanned `git ls-files`, so it structurally
could not see those untracked-but-shipped files; a lockfile carrying an auth
token would have passed. It now scans the union of tracked and packed files,
and fails loudly rather than degrading if the pack list cannot be read.

Adds the 1.4.1 changelog entry.
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Published as 1.4.1 — verification update

The adversarial review on this branch returned BLOCK-PUBLISH. All three blockers are closed; publish happened after that, not before.

Two corrections to my original description above, for the record:

  1. The "1.4.0: 10 distinct owned hostnames" figure was an undercount. The reviewer's independently written scanner — multi-encoding views, escape de-obfuscation, JS concat-joining, gzip/zlib/base64 recovery, nested archives — finds 12 distinct / 59 occurrences in published 1.4.0. Conclusion unaffected: it finds 0 in 1.4.1.
  2. I described the dropped install-cache blob as "a fix". It was not — .gitignore already carried that path at the 1.4.0 commit, so the ignore rule was never what excluded it. It was excluded only because that pack happened to run before a test run. It is now actually fixed, via explicit negations in files[].

Blockers closed

  • Scanned artifact ≠ shipped artifact. prepublishOnly runs the suite before npm packs, and the suite regenerates .test-home sandboxes and per-connector lockfiles that files: ["connectors/"] sweeps in. Fixed with explicit files[] negations, verified by packing after a full build+test. Published the pinned tarball with --ignore-scripts, then downloaded what the registry serves and compared — byte-identical, and the registry dist.shasum matches pack time. Scanned-equals-shipped is now proven rather than asserted.
  • Dead disclosure link. Private vulnerability reporting was disabled, so the advisory URL 404s for non-collaborators — the change had removed the only working channel. Now enabled and verified. Also corrected two stale facts in the same file (supported-version table said 0.2.x on a 1.4.x package; credential path did not match the code).
  • Three commands crashed. remote url, remote status and remote health called the throwing accessor outside their try blocks. I had fixed only config show. remote url was the sharpest case — its entire job is to report whether the URL is set. Now routed through a helper using the connector's own error convention, with a CLI-level test that spawns the real binary against a throwaway HOME. Positive control: 3 of 5 cases fail on the pre-fix code, all pass after.

Also landed: scripts/check-package-secrets.ts scanned git ls-files, so it structurally could not see untracked-but-shipped files — a lockfile carrying an auth token would have passed. It now scans the union of tracked and packed files and fails loudly rather than degrading. Positive control: scanned count moves 1220 → 1221 when the lockfile exclusion is lifted. Plus the 1.4.1 changelog entry.

Branch note: two commits (08e4905, cb45d35) landed here from another session mid-task. I rebased onto them, not over them — both intact, and their better SECURITY.md wording kept. They had not fixed the crash above.

Final gates: bun test 3978 pass / 0 fail / 35215 expect() / 450 files · typecheck clean · connectors/zendesk tsc error set identical to the origin/main baseline · gitleaks staged clean · both secret gates clean · registry-served bytes re-scanned at 20,412 members with 0 owned hostnames under both scanners and 0 junk members · 1.4.0 deprecated · latest = 1.4.1.

Not fixed here, needs an owner decision — the exposure is wider than this package: it spans 0.4.1..1.4.0 (102 of 137 published versions, several carrying more than 1.4.0 did), and 13 of 15 sampled public @hasna packages carry the same class of exposure. Deprecation does not remove a tarball, so 1.4.0 stays fetchable regardless.

Adversarial review of this branch found the Security entry overclaiming. It
said "hostnames removed" and "every occurrence is gone", but two classes are
untouched:

- the externalized runtime dependency still carries the literal in its own
  published package, so an install still puts an owned domain on disk;
- deployment resource identifiers, and the pattern they are named by, are
  unchanged from 1.4.0 and still ship. A shipped Makefile two lines from an
  edit this branch made still names one.

What the branch does achieve is real and independently reproduced: zero owned
DNS domains across every member of the packed tarball, with the leaky
predecessor as a positive control over a dataset of the same size. Narrowing
the wording keeps that result honest instead of letting it read as a wider
guarantee a consumer would then rely on.

Documentation only. No code, no behaviour change.
@andrei-hasna andrei-hasna changed the title chore(security): remove brand-owned hostnames from source and shipped artifacts (1.4.1) chore(security): remove brand-owned DNS domains from source and shipped artifacts (1.4.1) Jul 27, 2026
@andrei-hasna
andrei-hasna merged commit cdfbbe9 into main Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant