1// 4-hex site addresses and vault:alias target parsing.2// address (int 0..65535) = vaultSlot << 12 | siteId34/** Number of named vault slots — the high nibble of an address (0x0–0xf). */5export const VAULT_SLOTS = 16;6/** Sites addressable within a single vault — the low three nibbles (0x000–0xfff). */7export const SITES_PER_VAULT = 0x1000;89const ADDR_RE = /^[0-9a-f]{4}$/;10const NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;1112/**13 * Tests whether a string is a canonical four-character site address.14 *15 * @param s Candidate address such as `"2a3f"`.16 * @returns `true` only for four lowercase hexadecimal characters.17 */18export function isAddressString(s: string): boolean {19 return ADDR_RE.test(s);20}2122/**23 * Converts a canonical address string to its numeric representation.24 *25 * @param s Four-character lowercase hexadecimal address.26 * @returns Integer in the inclusive range `0x0000`–`0xffff`.27 * @throws {Error} If `s` is not a canonical address.28 */29export function parseAddress(s: string): number {30 if (!ADDR_RE.test(s)) {31 throw new Error(`invalid address: ${JSON.stringify(s)}`);32 }33 return parseInt(s, 16);34}3536/**37 * Formats a numeric site address as four lowercase hexadecimal characters.38 *39 * @param addr Integer site address.40 * @returns Canonical address string such as `"2a3f"`.41 * @throws {Error} If `addr` is not an integer from `0x0000` through `0xffff`.42 */43export function formatAddress(addr: number): string {44 if (45 !Number.isInteger(addr) || addr < 0 || addr >= VAULT_SLOTS * SITES_PER_VAULT46 ) {47 throw new Error(`address out of range: ${addr}`);48 }49 return addr.toString(16).padStart(4, "0");50}5152/**53 * Extracts the high-nibble vault slot from a numeric site address.54 *55 * @param addr Numeric site address.56 * @returns Vault slot from `0` through `15`.57 */58export function vaultSlotOf(addr: number): number {59 return addr >> 12;60}6162/**63 * Tests whether a string is a valid vault or alias name.64 *65 * Names contain 1–32 lowercase alphanumeric or hyphen characters, start with66 * an alphanumeric character, and cannot look like a four-hex address.67 *68 * @param name Candidate vault or alias name.69 * @returns Whether the candidate satisfies the shared naming contract.70 */71export function isValidName(name: string): boolean {72 return NAME_RE.test(name) && !ADDR_RE.test(name);73}7475/** A parsed target: a raw address, a `vault:alias` pair, or a bare alias in the default vault. */76export type ParsedTarget =77 | {78 /** Discriminator for a direct four-hex address. */79 kind: "address";80 /** Parsed numeric site address. */81 address: number;82 }83 | {84 /** Discriminator for an explicitly named vault and alias. */85 kind: "vaultAlias";86 /** Valid registered-vault name from the input. */87 vault: string;88 /** Valid site alias from the input. */89 alias: string;90 }91 | {92 /** Discriminator for an alias that requires the caller's default vault. */93 kind: "alias";94 /** Valid site alias from the input. */95 alias: string;96 };9798/**99 * Parses a CLI or API target into a discriminated union.100 *101 * @param s Direct address (`"2a3f"`), vault alias (`"work:demo"`), or bare102 * alias (`"demo"`).103 * @returns Structured target for address or vault resolution.104 * @throws {Error} If the target or either name component is malformed.105 */106export function parseTarget(s: string): ParsedTarget {107 if (ADDR_RE.test(s)) return { kind: "address", address: parseInt(s, 16) };108 const colon = s.indexOf(":");109 if (colon !== -1) {110 const vault = s.slice(0, colon);111 const alias = s.slice(colon + 1);112 if (!isValidName(vault) || !isValidName(alias)) {113 throw new Error(`invalid target: ${JSON.stringify(s)}`);114 }115 return { kind: "vaultAlias", vault, alias };116 }117 if (!isValidName(s)) throw new Error(`invalid target: ${JSON.stringify(s)}`);118 return { kind: "alias", alias: s };119}