Files
hanzo.ai/e2e/gates/trust-copy.spec.ts
zeekay b29f60c059 Answer every URL Google knows at one address
Search Console had 5,812 hanzo.ai URLs as 404, 1,323 as duplicates
without a canonical and 165 as noindex. Three causes: the export shipped
a noindex page that forwarded in the browser at each retired address,
every page also answered at /x/, /x.html and www, and each page's .md
twin was an indexable copy of it.

Retired addresses are now 301s at the edge, rendered into hanzoai/universe
by scripts/edge.mjs from lib/edge.ts: MOVED grows the old product
taxonomy, the first site's routes, pages later dropped and the gateway's
old docs; SHELVES sends the rest of /docs/ to docs.hanzo.ai; and
lib/data/retired.json holds the 256 model pages the catalog has dropped,
read against the catalog each build renders. The shells go, except the
four that are Go import paths, which ?go-get=1 still reads.

The sitemap lists only pages at their own address, each with a lastmod
from its files' last commit, so deploy and gates check out the whole
history.
/privacy, /terms and /cookies stay up for the shared footer but point at
/legal/*, so they leave the sitemap. /pay and /v1 join the Disallow list.

Gates run the edge's rules over the export: every retired address lands
on a page in one hop, every page answers at one address, every sitemap
URL is final and dated, and no page links a redirect. scripts/crawl.mjs
asks production the same questions, and reads a Search Console export.
2026-09-29 21:34:44 -07:00

265 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { answer } from '../../lib/edge'
import { OUT, read, serveExport } from './export'
/**
* What the trust page is allowed to claim.
*
* Copy is not usually testable. This page is the exception, because every
* sentence on it is a statement about a system that either exists or does not,
* and because it is the page a security reviewer reads hardest — a claim that
* drifts here is a claim somebody procured on.
*
* The failure this guards against has already happened once on our own
* surfaces: two sites gave two different answers about the same framework on
* the same day, and neither had an artifact behind either one. The /security
* badge row was the same defect in a quieter form — four framework names, each
* with a green checkmark, and the qualifiers that made them honest sat in a
* `description` field the markup never rendered.
*
* So the assertions below are written as RULES, not as searches for the
* sentence that broke last time. A framework may be named where the sentence is
* about control alignment or about a document we do send; "certified" may not
* appear at all. Neither can be satisfied by implying.
*
* The rule used to be the opposite: a framework could be named ONLY in a
* sentence denying we held it. That produced true copy with a second problem —
* the denial went into this page's meta description, which is what a search
* engine prints for someone searching whether we are certified, so the answer
* opened by listing three absences. A hedge fails for the same reason it always
* did, and now so does a denial: both are answers to a question nobody asked.
*/
const FRAMEWORKS = /\b(SOC ?2|ISO ?27001|FedRAMP|PCI ?DSS|HITRUST)\b/gi
const DENIAL = /\b(no|not|never|without|neither|nor)\b/i
/** The sentence shapes that make naming a framework legitimate. */
const ALIGNED = /\b(compatible with|compliant with|built to|aligned (to|with)|available (up)?on request|on request|internal audits?)\b/i
/** Claims to a certificate. No phrasing rescues one of these. */
const CERTIFICATE = /\b(certified|certification|accredited|attestation)\b/i
/** Hedges. Each reads as a claim to a skimming buyer and dates itself on ship. */
const HEDGE = /\b(in ?progress|in ?audit|assessment planned|planned|readiness|ready|scoped|towards?|underway|scheduled)\b/i
interface Rendered {
text: string
headings: string[]
}
let rendered: Rendered
test.beforeAll(async ({ browser }) => {
const server = await serveExport()
const tab = await browser.newPage({ viewport: { width: 1280, height: 900 } })
try {
await tab.goto(server.url + '/trust', { waitUntil: 'load' })
rendered = await tab.evaluate(() => {
// The kit renders its own <main> inside the layout's, so the innermost —
// last in document order — is the page and not the shell.
const main = [...document.querySelectorAll('main')].at(-1)
if (!main) throw new Error('the trust page rendered no <main>')
return {
text: (main as HTMLElement).innerText,
headings: [...main.querySelectorAll('h1, h2')].map((h) => (h.textContent ?? '').trim()),
}
})
} finally {
await tab.close()
await server.close()
}
})
const words = () => rendered.text
/** The page's sentences, for the rules that must hold within one breath. */
const sentences = () => words().split(/(?<=[.!?])\s+/)
test('the rendered page is the page, and not an empty read of it', () => {
// The floor, and it is load-bearing. Most assertions below are of the form
// "the copy never says Y", and every one of those is true of an empty string
// — so an extraction that quietly returned nothing would switch this file off
// and report green. Silence is the failure mode a copy gate has.
expect(rendered, 'the extraction must have run').toBeTruthy()
expect(words().length, 'the page is a document, not a stub').toBeGreaterThan(3000)
expect(rendered.headings.length, 'it is built from the kit’s sections').toBeGreaterThan(6)
expect(words(), 'innerText, not stripped markup').not.toMatch(/<[a-z/]|class=|style=/i)
})
test('a framework is named only to describe controls or offer a document', () => {
// The RULE, scoped to a sentence rather than to a character distance — a bound
// of "within N characters" is satisfied by rewording, which is not the same as
// being true. Written this way, no paraphrase of a certification claim can
// come back, which a `not.toContain` of last time's sentence could not promise.
const loose = sentences().filter(
(s) => FRAMEWORKS.test(s) && !ALIGNED.test(s) && !DENIAL.test(s),
)
expect(loose, `a framework named as neither alignment nor offer:\n${loose.join('\n')}`).toEqual([])
// And the page must actually take a position, rather than pass by saying nothing.
expect(words(), 'the position must be stated').toMatch(/compatible with SOC 2/i)
expect(words(), 'and the document offered').toMatch(/available upon request/i)
})
test('the position is stated positively, never as a list of absences', () => {
// The regression this replaces: "Hanzo does not hold SOC 2, ISO 27001, or
// FedRAMP" was this page's own meta description for months. It was true, and
// it was the sentence Google printed under our name for that exact search.
const denials = sentences().filter((s) => FRAMEWORKS.test(s) && /\b(do not|does not|don't|no longer|hold no)\b/i.test(s))
expect(denials, `a framework named to say we lack it:\n${denials.join('\n')}`).toEqual([])
})
test('the page never claims a certificate', () => {
const text = words()
expect(text, 'a certificate is a document we do not have').not.toMatch(/\bcertified\b/i)
expect(text, 'nor an accreditation').not.toMatch(/\baccredited\b/i)
expect(text, 'nor a formal attestation').not.toMatch(/\battestation\b/i)
// "compliant with X" used to be banned here as the same claim wearing a
// different coat. It is not the same claim: it describes our controls, where
// "certified" describes a document an auditor issued. The certificate words
// above are the ones no context can make true.
//
// "audit report" is likewise allowed now — we run continual INTERNAL audits
// and send that report. What stays banned is the shape that implies a third
// party performed it, which is the thing we cannot produce.
expect(text, 'no auditor of ours').not.toMatch(/\bour (auditor|assessor)\b/i)
expect(text, 'nor the certificate artifact by name').not.toMatch(/\bType (I|II) report\b/i)
expect(text, "and our audits are ours, not a third party's").not.toMatch(
/\b(independent|third.?party|external)\s+(audit|auditor|assessment|assessor)/i,
)
})
test('no timeline is promised, and no certification is said to be coming', () => {
const text = words()
// "assessment planned" was the hanzo.ai half of the contradiction this page
// replaces. A date is a promise; "scoped per engagement" is a fact.
expect(text).not.toMatch(/\b(assessment|audit|certification|report)\s+(is\s+)?(planned|underway|in progress|scheduled|coming)\b/i)
expect(text).not.toMatch(/\bwe (will|expect to|plan to|aim to)\s+(be|become|obtain|achieve|complete)\b/i)
// And no hedge beside a framework in any spelling. "SOC 2 ready" and "Type II
// readiness" were the two that kept coming back, each reading as a claim to
// anyone skimming and each stale the month after it shipped.
const hedged = sentences().filter((s) => FRAMEWORKS.test(s) && HEDGE.test(s))
expect(hedged, `a framework named beside a hedge:\n${hedged.join('\n')}`).toEqual([])
})
test('no availability figure, because that number is contractual', () => {
const text = words()
expect(text, 'no percentage anywhere — an uptime claim reads as an SLA').not.toMatch(/\d+(\.\d+)?\s?%/)
expect(text, 'and the reason is stated rather than left as a gap').toMatch(
/uptime number is a contractual commitment/i,
)
})
test('FIPS is named only as a denial', () => {
// A FIPS 140-3 claim was removed from /hsm for having no evidence. Naming a
// standard we implement (ML-KEM, ML-DSA) is fine; a validation belongs to a
// module's vendor. The word must not reappear as a claim in any form.
for (const s of sentences().filter((s) => /\bFIPS\b/i.test(s))) {
expect(s, `FIPS named without a denial: ${s}`).toMatch(DENIAL)
}
expect(words(), 'no validation claim in any spelling').not.toMatch(/\bFIPS[- ]?(140|validated)/i)
})
test('the controls it does claim are the ones it can point at', () => {
const text = words()
// Each of these was read in the source before it was written on the page.
// They are asserted so that deleting the evidence deletes the claim with it.
expect(text, 'PKCE, and only the strong method').toMatch(/only S256 is accepted/)
expect(text, 'argon2id, with the parameters in the digest').toMatch(/argon2id at 64 MiB and two passes/)
expect(text, 'refresh rotation with family revocation').toMatch(/every token in that family is deleted/)
expect(text, 'the grant model').toMatch(/A grant covers a path and everything below it/)
expect(text, 'and the segment-wise test that makes it safe').toMatch(
/acme\/prod does not cover acme\/production/,
)
expect(text, 'the edge re-deriving identity').toMatch(/stripped at the gateway/)
expect(text, 'per-tenant file').toMatch(/Two organizations are two files/)
expect(text, 'the audit trail’s reserved rows').toMatch(/refuses to create, correct or delete one/)
expect(text, 'the key derivation that is actually in the build').toMatch(/through HKDF-SHA256/)
expect(text, 'and a signing key that stays in its module').toMatch(/takes a key id and some bytes/)
})
test('the keys section describes live code, not an archived repo', () => {
// hanzoai/kms is archived — its README and DEPRECATED.md both say the
// canonical KMS is luxfi/kms, which is an MPC service for validator keys and
// does not carry the customer-secret chain at all. So the passphrase →
// Argon2id → HKDF → AES-GCM story, and the hybrid-HPKE key sharing beside it,
// describe a repo nobody runs. Both were on the first draft of this page.
//
// What IS in the running build is hanzoai/cek: HKDF-SHA256 per namespace and
// subsystem, pinned in base's go.mod and called on every org Base open. That
// is what the page says, and this keeps the deleted claims from drifting back.
const text = words()
expect(text, 'the archived passphrase KDF').not.toMatch(/Argon2id at 256 MiB/)
expect(text, 'the archived key-sharing story').not.toMatch(/ML-KEM-768 together|hybrid HPKE/i)
expect(text, 'and no claim that a key never reaches us').not.toMatch(/never leaves (the|your) (client|browser|device)/i)
})
test('encryption at rest is described as configured, not as universal', () => {
// base/plugins/org/org_db.go: an EMPTY master key is dev mode and the org Base
// opens plaintext. A wrong-length key is an error — that half is unconditional
// — but "every file is encrypted" would be false of a deployment that set no
// master. The file boundary is the claim that holds either way.
const text = words()
expect(text, 'the unconditional half is the file boundary').toMatch(/Two organizations are two files/)
expect(text, 'and the key is qualified by being configured').toMatch(
/Where a master key is configured/,
)
expect(text, 'no unconditional at-rest claim').not.toMatch(
/\b(all|every) (data|file|database)s? (is|are) encrypted\b/i,
)
})
test('the controls it cannot point at are named as absent', () => {
const text = words()
// internal/webauthn stores passkey credentials and holds no assertion
// ceremony; pkg/schema/passkey.go returns PasskeySignin() == false outright.
// The prompt for this page asked for passkeys. The code says otherwise, and
// the code wins — this asserts the page kept saying so.
expect(text, 'passkey sign-in is not implemented and must be disclaimed').toMatch(
/a passkey cannot complete a sign-in/i,
)
expect(text, 'nor may it be claimed anywhere on the page').not.toMatch(
/\b(sign in|log in|authenticate)\s+with\s+a?\s?passkey/i,
)
// SCIM provisions Users; there is no /Groups resource type.
expect(text, 'SCIM groups are not implemented').toMatch(/Groups are\s+not implemented/)
})
test('a reader is told where to send a vulnerability', () => {
const text = words()
const html = read('trust.html')
expect(text, 'the address').toContain('[email protected]')
expect(html, 'as a mailto, not just as prose').toContain('mailto:[email protected]')
expect(html, 'and the RFC 9116 location a scanner reads instead').toContain('/.well-known/security.txt')
expect(html, 'the mechanisms in more detail').toContain('/security')
expect(html, 'and one link for an enterprise conversation').toContain('/contact-sales')
})
test('the file a scanner asks for is in the export, not the fallback', () => {
// The lesson `scripts/wellknown.mjs` exists for: a static export behind a SPA
// fallback answers EVERY path with index.html, so a missing file returns 200
// with HTML and looks fine to anything checking a status code. The page links
// this address; the bytes have to be at it.
expect(read('.well-known/security.txt')).toMatch(/^\s*Contact:\s*mailto:security@hanzo\.ai/m)
})
test('the banned register never gets in', () => {
// Security copy is the worst offender for abstraction. These are the words
// that describe a feeling instead of a mechanism, and every one of them was
// in the first draft of some page in this repo.
const banned = [
'seamless', 'robust', 'enterprise-grade', 'military-grade', 'bank-grade',
'peace of mind', 'unlock', 'empower', 'leverage', 'best-in-class',
'world-class', 'cutting-edge', 'state-of-the-art', 'battle-tested',
'iron-clad', 'ironclad', 'bulletproof', 'unbreakable', 'uncompromising',
]
const lower = words().toLowerCase()
const found = banned.filter((w) => lower.includes(w))
expect(found, `abstraction in place of a mechanism: ${found.join(', ')}`).toEqual([])
})
test('/soc2 forwards to /trust and asserts nothing on the way', () => {
// People type this. It must not be a page that implies a report exists, and
// the safest answer is no page at all: the edge sends it, in one hop, to the
// single place the position is stated.
expect(existsSync(join(OUT, 'soc2.html')), 'no page ships at /soc2').toBe(false)
expect(answer('https://hanzo.ai/soc2')).toEqual({ location: 'https://hanzo.ai/trust' })
})