{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/discovery/registry.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAUpD,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,KAAK,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,GAAG,mBAAmB,GAAG,oBAAoB,CAAC;AAElG,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,gCAAgC,GAAG,IAAI,CAAC,wBAAwB,EAAE,WAAW,GAAG,QAAQ,CAAC,CAAC;AAEtG,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,CAAC,CAAC;IAClB,OAAO,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,wBAAwB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,yBAAyB,GAClC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmGlE,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAEtE;AAgCD,+DAA+D;AAC/D,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAmB,GAAG,oBAAoB,CAiCxG;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CA4B9E;AAeD,uFAAuF;AACvF,wBAAgB,qCAAqC,CACpD,KAAK,EAAE,OAAO,EACd,iBAAiB,CAAC,EAAE,gCAAgC,GAClD,MAAM,CAeR;AAkBD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,SAAS,wBAAwB,EAAE,GAAG,IAAI,CAqBpG;AAED,4FAA4F;AAC5F,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE,SAAS,wBAAwB,EAAE,EAC5C,QAAQ,EAAE,MAAM,GACd,wBAAwB,GAAG,SAAS,CAGtC;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC9C,KAAK,EAAE,OAAO,EACd,cAAc,EAAE,SAAS,wBAAwB,EAAE,GACjD,yBAAyB,CAe3B","sourcesContent":["import { createPublicKey, type KeyObject, verify as verifySignature } from \"node:crypto\";\nimport { canonicalJson } from \"../storage.ts\";\n\nexport const EVO_PACK_REGISTRY_FORMAT = 1;\nexport const EVO_PACK_REGISTRY_MAX_ENTRIES = 10_000;\n\nconst PACK_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;\nconst PACK_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;\nconst SIGNER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;\nconst INTEGRITY_PATTERN = /^sha256:[0-9a-f]{64}$/;\nconst IMMUTABLE_REVISION_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;\nconst GIST_ID_PATTERN = /^[0-9a-f]{5,64}$/;\nconst TERMINAL_CONTROL_PATTERN = /[\\u0000-\\u001f\\u007f-\\u009f]/u;\n\nexport interface EvoHttpsRawFileSource {\n\tkind: \"https\";\n\trawUrl: string;\n}\n\nexport interface EvoGitRawFileSource {\n\tkind: \"git\";\n\trepository: string;\n\trevision: string;\n\tpath: string;\n\trawUrl: string;\n}\n\nexport interface EvoGistRawFileSource {\n\tkind: \"gist\";\n\tgistId: string;\n\trevision: string;\n\tfile: string;\n\trawUrl: string;\n}\n\n/**\n * A raw-file locator records both the retrieval URL and immutable provenance\n * when the file comes from git or a gist. Direct HTTPS locations rely on the\n * signed pack integrity for content identity.\n */\nexport type EvoRawFileSource = EvoHttpsRawFileSource | EvoGitRawFileSource | EvoGistRawFileSource;\n\nexport interface EvoPackRegistrySignature {\n\talgorithm: \"ed25519\";\n\tsigner: string;\n\tvalue: string;\n}\n\nexport type EvoPackRegistrySignatureIdentity = Pick<EvoPackRegistrySignature, \"algorithm\" | \"signer\">;\n\nexport interface EvoPackRegistryEntry {\n\tname: string;\n\tversion: string;\n\tintegrity: string;\n\tauthor?: string;\n\tdescription?: string;\n\tsource: EvoRawFileSource;\n\tsignature?: EvoPackRegistrySignature;\n}\n\nexport interface EvoPackRegistryIndex {\n\tregistryFormat: 1;\n\tentries: EvoPackRegistryEntry[];\n}\n\nexport interface EvoTrustedRegistrySigner {\n\tid: string;\n\tpublicKeyPem: string;\n}\n\nexport type EvoPackRegistryEntryTrust =\n\t| { status: \"trusted\"; trusted: true; signer: string }\n\t| { status: \"unsigned\"; trusted: false }\n\t| { status: \"untrusted-signer\"; trusted: false; signer: string };\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be an object`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction exactKeys(record: Record<string, unknown>, allowed: readonly string[], label: string): void {\n\tconst allowedKeys = new Set(allowed);\n\tfor (const key of Object.keys(record)) {\n\t\tif (!allowedKeys.has(key)) throw new Error(`${label} has unknown key: ${key}`);\n\t}\n}\n\nfunction asBoundedString(value: unknown, label: string, maximumLength: number): string {\n\tif (typeof value !== \"string\" || !value || value.length > maximumLength || value.includes(\"\\0\")) {\n\t\tthrow new Error(`${label} must be a non-empty string of at most ${maximumLength} characters`);\n\t}\n\treturn value;\n}\n\nfunction assertNoTerminalControls(value: string, label: string): void {\n\tif (TERMINAL_CONTROL_PATTERN.test(value)) {\n\t\tthrow new Error(`${label} must not contain terminal control characters`);\n\t}\n}\n\nfunction assertHttpsUrl(value: string, label: string): void {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(value);\n\t} catch {\n\t\tthrow new Error(`${label} must be a canonical HTTPS URL`);\n\t}\n\tif (\n\t\turl.protocol !== \"https:\" ||\n\t\t!url.hostname ||\n\t\turl.username ||\n\t\turl.password ||\n\t\turl.search ||\n\t\turl.hash ||\n\t\turl.href !== value\n\t) {\n\t\tthrow new Error(`${label} must be a canonical HTTPS URL without credentials, query, or fragment`);\n\t}\n}\n\nfunction assertSafeRelativePath(value: string, label: string): void {\n\tif (\n\t\tvalue.startsWith(\"/\") ||\n\t\tvalue.includes(\"\\\\\") ||\n\t\tvalue.includes(\"\\0\") ||\n\t\tvalue.split(\"/\").some((segment) => !segment || segment === \".\" || segment === \"..\")\n\t) {\n\t\tthrow new Error(`${label} must be a safe relative POSIX path`);\n\t}\n}\n\nfunction parseRawFileSource(value: unknown, label: string): EvoRawFileSource {\n\tconst record = asRecord(value, label);\n\tif (record.kind === \"https\") {\n\t\texactKeys(record, [\"kind\", \"rawUrl\"], label);\n\t\tconst rawUrl = asBoundedString(record.rawUrl, `${label}.rawUrl`, 4_096);\n\t\tassertHttpsUrl(rawUrl, `${label}.rawUrl`);\n\t\treturn { kind: \"https\", rawUrl };\n\t}\n\tif (record.kind === \"git\") {\n\t\texactKeys(record, [\"kind\", \"repository\", \"revision\", \"path\", \"rawUrl\"], label);\n\t\tconst repository = asBoundedString(record.repository, `${label}.repository`, 4_096);\n\t\tassertHttpsUrl(repository, `${label}.repository`);\n\t\tconst revision = asBoundedString(record.revision, `${label}.revision`, 64);\n\t\tif (!IMMUTABLE_REVISION_PATTERN.test(revision)) {\n\t\t\tthrow new Error(`${label}.revision must be a full lowercase git object ID`);\n\t\t}\n\t\tconst path = asBoundedString(record.path, `${label}.path`, 1_024);\n\t\tassertSafeRelativePath(path, `${label}.path`);\n\t\tconst rawUrl = asBoundedString(record.rawUrl, `${label}.rawUrl`, 4_096);\n\t\tassertHttpsUrl(rawUrl, `${label}.rawUrl`);\n\t\treturn { kind: \"git\", repository, revision, path, rawUrl };\n\t}\n\tif (record.kind === \"gist\") {\n\t\texactKeys(record, [\"kind\", \"gistId\", \"revision\", \"file\", \"rawUrl\"], label);\n\t\tconst gistId = asBoundedString(record.gistId, `${label}.gistId`, 64);\n\t\tif (!GIST_ID_PATTERN.test(gistId)) throw new Error(`${label}.gistId is invalid`);\n\t\tconst revision = asBoundedString(record.revision, `${label}.revision`, 64);\n\t\tif (!IMMUTABLE_REVISION_PATTERN.test(revision)) {\n\t\t\tthrow new Error(`${label}.revision must be a full lowercase gist revision`);\n\t\t}\n\t\tconst file = asBoundedString(record.file, `${label}.file`, 1_024);\n\t\tassertSafeRelativePath(file, `${label}.file`);\n\t\tconst rawUrl = asBoundedString(record.rawUrl, `${label}.rawUrl`, 4_096);\n\t\tassertHttpsUrl(rawUrl, `${label}.rawUrl`);\n\t\treturn { kind: \"gist\", gistId, revision, file, rawUrl };\n\t}\n\tthrow new Error(`${label}.kind must be 'https', 'git', or 'gist'`);\n}\n\n/** Parse a registry or pack raw-file locator without performing I/O. */\nexport function parseEvoRawFileSource(value: unknown): EvoRawFileSource {\n\treturn parseRawFileSource(value, \"raw-file source\");\n}\n\nfunction assertPackManifestSource(source: EvoRawFileSource, label: string): void {\n\tif (!new URL(source.rawUrl).pathname.endsWith(\"/pack.json\")) {\n\t\tthrow new Error(`${label}.rawUrl must identify a pack.json file`);\n\t}\n\tif (source.kind === \"git\" && source.path !== \"pack.json\" && !source.path.endsWith(\"/pack.json\")) {\n\t\tthrow new Error(`${label}.path must identify a pack.json file`);\n\t}\n\tif (source.kind === \"gist\" && source.file !== \"pack.json\" && !source.file.endsWith(\"/pack.json\")) {\n\t\tthrow new Error(`${label}.file must identify a pack.json file`);\n\t}\n}\n\nfunction parseSignatureIdentity(value: unknown, label: string): EvoPackRegistrySignatureIdentity {\n\tconst record = asRecord(value, label);\n\texactKeys(record, [\"algorithm\", \"signer\"], label);\n\tif (record.algorithm !== \"ed25519\") throw new Error(`${label}.algorithm must be 'ed25519'`);\n\tconst signer = asBoundedString(record.signer, `${label}.signer`, 128);\n\tif (!SIGNER_ID_PATTERN.test(signer)) throw new Error(`${label}.signer is invalid`);\n\treturn { algorithm: \"ed25519\", signer };\n}\n\nfunction parseSignature(value: unknown, label: string): EvoPackRegistrySignature {\n\tconst record = asRecord(value, label);\n\texactKeys(record, [\"algorithm\", \"signer\", \"value\"], label);\n\tconst identity = parseSignatureIdentity({ algorithm: record.algorithm, signer: record.signer }, label);\n\tconst signatureValue = asBoundedString(record.value, `${label}.value`, 128);\n\tdecodeEd25519Signature(signatureValue, `${label}.value`);\n\treturn { ...identity, value: signatureValue };\n}\n\n/** Parse one exact, signed-or-unsigned pack registry entry. */\nexport function parseEvoPackRegistryEntry(value: unknown, label = \"registry entry\"): EvoPackRegistryEntry {\n\tconst record = asRecord(value, label);\n\texactKeys(record, [\"name\", \"version\", \"integrity\", \"author\", \"description\", \"source\", \"signature\"], label);\n\tconst name = asBoundedString(record.name, `${label}.name`, 128);\n\tif (!PACK_NAME_PATTERN.test(name)) throw new Error(`${label}.name is invalid`);\n\tconst version = asBoundedString(record.version, `${label}.version`, 128);\n\tif (!PACK_VERSION_PATTERN.test(version)) throw new Error(`${label}.version is invalid`);\n\tconst integrity = asBoundedString(record.integrity, `${label}.integrity`, 71);\n\tif (!INTEGRITY_PATTERN.test(integrity)) throw new Error(`${label}.integrity is invalid`);\n\tif (record.author !== undefined && (typeof record.author !== \"string\" || record.author.length > 256)) {\n\t\tthrow new Error(`${label}.author must be a string of at most 256 characters`);\n\t}\n\tif (\n\t\trecord.description !== undefined &&\n\t\t(typeof record.description !== \"string\" || record.description.length > 4_096)\n\t) {\n\t\tthrow new Error(`${label}.description must be a string of at most 4096 characters`);\n\t}\n\tif (typeof record.author === \"string\") assertNoTerminalControls(record.author, `${label}.author`);\n\tif (typeof record.description === \"string\") {\n\t\tassertNoTerminalControls(record.description, `${label}.description`);\n\t}\n\tconst source = parseRawFileSource(record.source, `${label}.source`);\n\tassertPackManifestSource(source, `${label}.source`);\n\treturn {\n\t\tname,\n\t\tversion,\n\t\tintegrity,\n\t\t...(record.author === undefined ? {} : { author: record.author as string }),\n\t\t...(record.description === undefined ? {} : { description: record.description as string }),\n\t\tsource,\n\t\t...(record.signature === undefined ? {} : { signature: parseSignature(record.signature, `${label}.signature`) }),\n\t};\n}\n\n/** Parse a fail-closed v1 registry index and reject ambiguous identities. */\nexport function parseEvoPackRegistryIndex(value: unknown): EvoPackRegistryIndex {\n\tconst record = asRecord(value, \"pack registry index\");\n\texactKeys(record, [\"registryFormat\", \"entries\"], \"pack registry index\");\n\tif (record.registryFormat !== EVO_PACK_REGISTRY_FORMAT) {\n\t\tthrow new Error(`pack registry index registryFormat must be ${EVO_PACK_REGISTRY_FORMAT}`);\n\t}\n\tif (!Array.isArray(record.entries)) throw new Error(\"pack registry index entries must be an array\");\n\tif (record.entries.length > EVO_PACK_REGISTRY_MAX_ENTRIES) {\n\t\tthrow new Error(`pack registry index exceeds ${EVO_PACK_REGISTRY_MAX_ENTRIES} entries`);\n\t}\n\tconst entries = record.entries.map((entry, index) => parseEvoPackRegistryEntry(entry, `registry entries[${index}]`));\n\tconst identities = new Set<string>();\n\tconst integrities = new Set<string>();\n\tconst sources = new Set<string>();\n\tfor (const entry of entries) {\n\t\tconst identity = `${entry.name}\\0${entry.version}`;\n\t\tif (identities.has(identity))\n\t\t\tthrow new Error(`pack registry index has duplicate pack version: ${entry.name}@${entry.version}`);\n\t\tif (integrities.has(entry.integrity)) {\n\t\t\tthrow new Error(`pack registry index has duplicate pack integrity: ${entry.integrity}`);\n\t\t}\n\t\tconst source = canonicalJson(entry.source);\n\t\tif (sources.has(source)) throw new Error(`pack registry index has duplicate pack source: ${entry.source.rawUrl}`);\n\t\tidentities.add(identity);\n\t\tintegrities.add(entry.integrity);\n\t\tsources.add(source);\n\t}\n\treturn { registryFormat: EVO_PACK_REGISTRY_FORMAT, entries };\n}\n\nfunction decodeEd25519Signature(value: string, label: string): Buffer {\n\tif (!value.startsWith(\"base64:\")) throw new Error(`${label} must use the base64: encoding`);\n\tconst encoded = value.slice(\"base64:\".length);\n\tif (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || encoded.length % 4 !== 0) {\n\t\tthrow new Error(`${label} is not canonical base64`);\n\t}\n\tconst decoded = Buffer.from(encoded, \"base64\");\n\tif (decoded.length !== 64 || decoded.toString(\"base64\") !== encoded) {\n\t\tthrow new Error(`${label} must encode one 64-byte Ed25519 signature`);\n\t}\n\treturn decoded;\n}\n\n/** Canonical, domain-separated bytes that bind one entry and its signer provenance. */\nexport function canonicalEvoPackRegistryEntryMetadata(\n\tvalue: unknown,\n\tsignatureIdentity?: EvoPackRegistrySignatureIdentity,\n): string {\n\tconst entry = parseEvoPackRegistryEntry(value);\n\tconst { signature: _signature, ...metadata } = entry;\n\tconst identityValue =\n\t\tsignatureIdentity ??\n\t\t(entry.signature ? { algorithm: entry.signature.algorithm, signer: entry.signature.signer } : undefined);\n\tif (!identityValue) {\n\t\tthrow new Error(\"registry entry signature identity is required for canonical signing metadata\");\n\t}\n\tconst identity = parseSignatureIdentity(identityValue, \"registry entry signature identity\");\n\treturn canonicalJson({\n\t\tevoPackRegistryEntrySignatureFormat: 2,\n\t\tsignature: identity,\n\t\tmetadata,\n\t});\n}\n\nfunction trustedSignerPublicKey(signer: EvoTrustedRegistrySigner): KeyObject {\n\tif (signer.publicKeyPem.includes(\"PRIVATE KEY\")) {\n\t\tthrow new Error(`trusted signer ${signer.id} must use a public key`);\n\t}\n\tlet publicKey: KeyObject;\n\ttry {\n\t\tpublicKey = createPublicKey(signer.publicKeyPem);\n\t} catch {\n\t\tthrow new Error(`trusted signer ${signer.id} has an invalid public key`);\n\t}\n\tif (publicKey.asymmetricKeyType !== \"ed25519\") {\n\t\tthrow new Error(`trusted signer ${signer.id} must use an Ed25519 public key`);\n\t}\n\treturn publicKey;\n}\n\nexport function validateTrustedEvoRegistrySigners(signers: readonly EvoTrustedRegistrySigner[]): void {\n\tconst ids = new Set<string>();\n\tconst publicKeyOwners = new Map<string, string>();\n\tfor (const [index, signer] of signers.entries()) {\n\t\tif (typeof signer !== \"object\" || signer === null) throw new Error(`trusted signers[${index}] must be an object`);\n\t\tif (!SIGNER_ID_PATTERN.test(signer.id) || signer.id.length > 128) {\n\t\t\tthrow new Error(`trusted signers[${index}].id is invalid`);\n\t\t}\n\t\tif (typeof signer.publicKeyPem !== \"string\" || !signer.publicKeyPem) {\n\t\t\tthrow new Error(`trusted signers[${index}].publicKeyPem must be a non-empty string`);\n\t\t}\n\t\tif (ids.has(signer.id)) throw new Error(`trusted signers has duplicate signer id: ${signer.id}`);\n\t\tids.add(signer.id);\n\t\tconst publicKey = trustedSignerPublicKey(signer);\n\t\tconst fingerprint = publicKey.export({ format: \"der\", type: \"spki\" }).toString(\"base64\");\n\t\tconst existingOwner = publicKeyOwners.get(fingerprint);\n\t\tif (existingOwner) {\n\t\t\tthrow new Error(`trusted signers has duplicate public key aliases: ${existingOwner}, ${signer.id}`);\n\t\t}\n\t\tpublicKeyOwners.set(fingerprint, signer.id);\n\t}\n}\n\n/** Select a trusted key by exact signer ID; keys are never guessed or tried in sequence. */\nexport function selectTrustedEvoRegistrySigner(\n\tsigners: readonly EvoTrustedRegistrySigner[],\n\tsignerId: string,\n): EvoTrustedRegistrySigner | undefined {\n\tvalidateTrustedEvoRegistrySigners(signers);\n\treturn signers.find((signer) => signer.id === signerId);\n}\n\n/**\n * Evaluate one entry against the caller's trust roots. Unsigned and unknown-key\n * entries remain discoverable but explicitly untrusted. A bad signature from a\n * selected trusted signer rejects the entry.\n */\nexport function verifyEvoPackRegistryEntryTrust(\n\tvalue: unknown,\n\ttrustedSigners: readonly EvoTrustedRegistrySigner[],\n): EvoPackRegistryEntryTrust {\n\tconst entry = parseEvoPackRegistryEntry(value);\n\tif (!entry.signature) {\n\t\tvalidateTrustedEvoRegistrySigners(trustedSigners);\n\t\treturn { status: \"unsigned\", trusted: false };\n\t}\n\tconst signer = selectTrustedEvoRegistrySigner(trustedSigners, entry.signature.signer);\n\tif (!signer) return { status: \"untrusted-signer\", trusted: false, signer: entry.signature.signer };\n\tconst publicKey = trustedSignerPublicKey(signer);\n\tconst payload = Buffer.from(canonicalEvoPackRegistryEntryMetadata(entry), \"utf8\");\n\tconst signature = decodeEd25519Signature(entry.signature.value, \"registry entry signature.value\");\n\tif (!verifySignature(null, payload, publicKey, signature)) {\n\t\tthrow new Error(`registry entry signature is invalid for trusted signer ${signer.id}`);\n\t}\n\treturn { status: \"trusted\", trusted: true, signer: signer.id };\n}\n"]}