@pharoshub/guard
v1.1.0
Published
Seal your published site so a changed page cannot pretend to be yours — plus honeypot traps and honeytokens.
Maintainers
Readme
@pharoshub/guard
Two things, inside the application they protect.
Hallmark seals your published pages so a page altered after it left your build cannot pass as genuine. Deception puts traps in your own app that fire the moment someone probes you.
Neither blocks, redirects, challenges, or slows a visitor. Blocking is your edge provider's job. This is the part that tells you something is happening while it is still happening.
Hallmark — sealing what you publish
Every common web control assumes the attacker is outside. Once a hosting account, build server or CDN is compromised, a firewall sees legitimate requests and a bot filter sees a normal visitor. The realistic attack then is not defacement but one altered line — a phone number replaced, a payment form inserted, an account number changed by two digits.
Add it to your build, after your site is generated:
import { hallmark } from "@pharoshub/guard/hallmark";
await hallmark({ dir: "dist" });That writes two files into your output:
_integrity.json— the signed fingerprint listintegrity.js— the browser verifier
Then load the verifier on your pages:
<script src="/integrity.js" defer></script>Only hashes leave your infrastructure. Your build hashes its own output and sends a list of fingerprints; the files never reach us.
The signing key is not yours to hold, and not ours to leak onto your servers. It is never placed on your origin, your build system, your CDN, or in our database. Someone who takes over all of it can change whatever they like — they cannot produce a signature that verifies. Verification uses a public key that can only check, so anyone can audit a page independently.
It reports; it does not block. A mismatch is usually innocent — a corporate proxy rewriting HTML, antivirus, an ISP banner, or a deploy landing while a visitor had the old page open. What distinguishes an attack is scale: one visitor is their network, thousands across unrelated networks is the file.
It fails open. No keys, no network, our service down, subscription lapsed — the build prints a warning and finishes. A security feature that can stop you deploying is a worse problem than the one it solves.
Known limits
- The verifier is served by the origin it checks, so an attacker with complete control of that origin can remove it. It catches compromises that alter content without auditing every script.
- It checks the HTML document and same-origin scripts and stylesheets. Third- party scripts are not sealed — they are not yours to seal.
- It detects change; it does not prevent it.
Deception — traps in your own app
Install
npm install @pharoshub/guardPHAROS_SITE_KEY=...
PHAROS_SECRET=...Both come from the console when you add a site. The secret signs your reports; treat it like a database password.
Express
import { createGuard } from "@pharoshub/guard";
const guard = createGuard({
siteKey: process.env.PHAROS_SITE_KEY,
secret: process.env.PHAROS_SECRET,
});
app.use(guard.middleware()); // mount first, before your own routesNext.js
// middleware.ts
import { createNextGuard } from "@pharoshub/guard/next";
const guard = createNextGuard();
export async function middleware(request: Request) {
return (await guard(request)) ?? NextResponse.next();
}
export const config = { matcher: "/:path*" };The clone beacon
Put the tag in your document head. It reports the origin the page was actually served from — if that is not your domain, someone has published a copy, and for a hotel that copy is usually taking bookings under your name.
<script src={`https://www.pharoshub.cloud/b/${siteKey}.js`} async defer />If you run a Content-Security-Policy with a nonce, pass it: guard.beaconTag(nonce).
Honeytokens
const token = guard.honeytoken("stripe");Returns a credential that looks real and is never valid. Put it where an attacker will only find it by looking where they should not — a stale config file, an HTML comment, a decoy admin page. Nobody legitimate has it, so its use is not a signal to weigh. It is proof.
What it sends
Only requests that were already suspicious. A normal request never reaches the reporting path.
| Field | Example |
| --- | --- |
| path | /.env |
| method | GET |
| source_ip | 203.0.113.9 |
| user_agent | python-requests/2.31 |
| referer | — |
Request bodies are not captured unless you pass captureBodies: true, and
even then only for trap routes. A body on your real routes contains your
guests' details, and those are yours, not ours.
Failure
Every path fails open, by design:
- missing key or secret → the guard becomes a no-op, and says so once at startup
- collector unreachable → events queue in memory (500 max, newest kept) and retry
- anything throws → the request continues as if the guard were not installed
The worst outcome of a broken guard is missing telemetry. It is never a broken site.
Tuning traps
createGuard({ traps: [...DEFAULT_TRAPS, "/staff-portal", "/backup-2019.zip"] });The best traps are the ones specific to you — a path that looks like it belongs to your stack, that no link points at. Your plan sets how many you may run.
