mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
robots.txt states its Content Signals in both groups: search, ai-input and ai-train, which the groups already allow. /auth.md says how an agent gets a credential today (wallet sign-in, device flow, an API key) and that the auth.md registration protocol is not implemented. /.well-known/mcp/server-card.json describes the MCP endpoint at api.hanzo.ai/v1/mcp (SEP-2127, protocol 2026-07-28), and the API catalog names it. skill.mjs writes /skill.md again as an Agent Skill with its index (Agent Skills Discovery 0.2.0), the digest taken from the bytes it writes. wellknown.mjs copies subdirectories and refuses a server card that does not point at the API host.
101 lines
3.9 KiB
JavaScript
101 lines
3.9 KiB
JavaScript
/**
|
|
* Put /.well-known into the export, and refuse to finish the build without it.
|
|
*
|
|
* `public/.well-known/security.txt` is committed, the deploy was current, and
|
|
* the URL still answered with the app shell — because this site is
|
|
* `output: 'export'` and a static export plus a SPA fallback answers EVERY path
|
|
* with index.html. So a missing file does not 404. It returns 200, with HTML,
|
|
* and looks fine to anything that checks a status code.
|
|
*
|
|
* That is the whole reason this script exists rather than a copy step: the
|
|
* failure it guards against is invisible from the outside. RFC 9116 fixes the
|
|
* address at /.well-known/security.txt, so if the file is not there the file
|
|
* does not exist, no matter what the server returns.
|
|
*
|
|
* Loud, never partial, like noindex.mjs beside it: it copies, then reads back
|
|
* what it wrote, and exits non-zero if the bytes are not there. A build that
|
|
* quietly ships without a disclosure address is worse than a build that stops.
|
|
*/
|
|
import { readdirSync, mkdirSync, copyFileSync, readFileSync, existsSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const SRC = "public/.well-known";
|
|
const DST = "out/.well-known";
|
|
|
|
if (!existsSync(SRC)) {
|
|
console.error(`[well-known] ${SRC} does not exist — nothing to publish.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Every file under SRC, by its path below it: a document may sit a directory
|
|
// down (/.well-known/mcp/server-card.json).
|
|
const names = readdirSync(SRC, { recursive: true, withFileTypes: true })
|
|
.filter((e) => e.isFile() && !e.name.startsWith("."))
|
|
.map((e) => join(e.parentPath, e.name).slice(SRC.length + 1))
|
|
.sort();
|
|
if (names.length === 0) {
|
|
console.error(`[well-known] ${SRC} is empty.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const name of names) {
|
|
mkdirSync(dirname(join(DST, name)), { recursive: true });
|
|
copyFileSync(join(SRC, name), join(DST, name));
|
|
}
|
|
|
|
// Read back rather than trust the copy. The point is to prove the bytes are at
|
|
// the address, which is the one thing nobody could confirm from outside.
|
|
let bad = 0;
|
|
for (const name of names) {
|
|
const out = join(DST, name);
|
|
if (!existsSync(out)) {
|
|
console.error(`[well-known] ${out} missing after copy`);
|
|
bad++;
|
|
continue;
|
|
}
|
|
const body = readFileSync(out, "utf8");
|
|
if (name === "security.txt" && !/^\s*(#|Contact:)/m.test(body)) {
|
|
console.error(`[well-known] ${out} does not look like RFC 9116 — no Contact: field`);
|
|
bad++;
|
|
}
|
|
// RFC 9727's API catalog is a linkset document; anything else is not one.
|
|
if (name === "api-catalog") {
|
|
let linkset = null;
|
|
try {
|
|
linkset = JSON.parse(body).linkset;
|
|
} catch {}
|
|
if (!Array.isArray(linkset) || !linkset.length) {
|
|
console.error(`[well-known] ${out} is not an RFC 9727 linkset ({"linkset": [...]})`);
|
|
bad++;
|
|
}
|
|
}
|
|
// An MCP Server Card (SEP-2127): the fields a client needs to connect, and a
|
|
// streamable-http remote on the API host. Anything else is not one.
|
|
if (name === "mcp/server-card.json") {
|
|
let card = null;
|
|
try {
|
|
card = JSON.parse(body);
|
|
} catch {}
|
|
const remote = card?.remotes?.[0];
|
|
if (
|
|
card?.$schema !== "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json" ||
|
|
!/^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/.test(card?.name ?? "") ||
|
|
!card?.version ||
|
|
!(card?.description?.length > 0 && card.description.length <= 100) ||
|
|
remote?.type !== "streamable-http" ||
|
|
!/^https:\/\/api\.hanzo\.ai\//.test(remote?.url ?? "")
|
|
) {
|
|
console.error(`[well-known] ${out} is not an MCP Server Card for the API host`);
|
|
bad++;
|
|
}
|
|
}
|
|
// Apple Pay's domain proof is hex, and Square checks it byte for byte: anything else is not it.
|
|
if (name === "apple-developer-merchantid-domain-association" && !/^[0-9A-Fa-f]+\s*$/.test(body)) {
|
|
console.error(`[well-known] ${out} is not the hex domain association Apple Pay asks for`);
|
|
bad++;
|
|
}
|
|
}
|
|
|
|
if (bad) process.exit(1);
|
|
console.log(`[well-known] published ${names.length} file(s): ${names.join(", ")}`);
|