logicsrc/packages/ans/src/name.ts
Anthony Ettinger 29975b2df7 feat(ans): implement @logicsrc/ans M1 — resolver + offline verifier
M1 of the ANS SDK (docs/ans-sdk.md):

- name: parse/format ans://v<semver>.<agent>.<domain>
- cbor: minimal RFC 8949 codec for the COSE_Sign1 subset
- verify/merkle: RFC 6962 leaf/node hashing, tree build, inclusion-proof
  generation + verification
- verify/es256 + cose: COSE_Sign1 build/parse, Sig_structure, ES256 (WebCrypto)
- verify/rootkeys: kid -> verifier key (JWKS entries; sumdb-note is M2)
- verify: verifyReceipt() + verifyResolution() (signature + inclusion proof +
  name binding), pure and offline
- client: AnsClient resolve/rootKeys/register/status over injectable fetch,
  with a DnsApplier hook for verify-dns

Tests (24) cover name parsing, CBOR round-trips/vectors, RFC 6962 proofs, a full
ES256+Merkle receipt round-trip with positive/negative cases, and the client
against mocked fetch. Wired into the root build script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:48:24 +00:00

34 lines
1.5 KiB
TypeScript

import type { AnsName } from './types.js';
const LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i;
// ans://v<major.minor.patch[-prerelease]>.<agent>.<domain>
// The version's dotted core (X.Y.Z) is anchored so it doesn't get confused with
// the agent/domain labels. Prerelease is dot-free in M1 to keep the boundary
// unambiguous; build metadata (+...) is M2.
const ANS = /^ans:\/\/v(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+)?)\.([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\.(.+)$/i;
/**
* Parse an `ans://v<semver>.<agent>.<domain>` name. The version is `v<semver>`,
* the agent is a single DNS label, and the domain is one or more labels after
* it. Throws on a malformed name.
*/
export function parseAnsName(raw: string): AnsName {
const match = ANS.exec(raw.trim());
if (!match) throw new Error(`malformed ans name — got ${raw}`);
const [, version, agent, domain] = match;
if (!LABEL.test(agent)) throw new Error(`ans name has invalid agent label — got ${agent}`);
if (!domain || domain.split('.').some((label) => !LABEL.test(label))) {
throw new Error(`ans name has invalid domain — got ${domain}`);
}
return { raw: `ans://v${version}.${agent}.${domain}`, version, agent, domain };
}
export function formatAnsName(parts: { version: string; agent: string; domain: string }): string {
return parseAnsName(`ans://v${parts.version}.${parts.agent}.${parts.domain}`).raw;
}
export function toAnsName(name: string | AnsName): AnsName {
return typeof name === 'string' ? parseAnsName(name) : name;
}