mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 14:57:28 +00:00
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>
This commit is contained in:
parent
0ae24719e3
commit
29975b2df7
20 changed files with 1009 additions and 28 deletions
34
packages/ans/src/name.ts
Normal file
34
packages/ans/src/name.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue