akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
457 lines (456 loc) • 18.8 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
import { toErrorMessage } from "./common.js";
export const REGISTRY_CREDENTIALS_UNSUPPORTED = "Registry URLs must not include username or password credentials. Authenticated registries are not supported by the built-in static-index or skills-sh providers; configure a credential-free HTTPS endpoint.";
const INVALID_REGISTRY_URL_LABEL = "(invalid registry URL)";
const HTTP_SCHEME_START = /h[\t\r\n]*t[\t\r\n]*t[\t\r\n]*p[\t\r\n]*(?:s[\t\r\n]*)?:/giu;
const AUTHORITY_PREFIX_SEPARATOR = new Set(["\t", "\r", "\n", "\\", "/"]);
const AUTHORITY_STRUCTURAL_BOUNDARY = new Set(["\\", "/", "?", "#"]);
const AUTHORITY_AMBIGUOUS_BOUNDARY = new Set(["&", ",", ";", ")"]);
const AUTHORITY_SOFT_BOUNDARY = new Set(["\t", "\r", "\n", " ", '"', "'", "`", "<", ">"]);
const STRICT_MAX_DEPTH = 8;
const STRICT_MAX_CANDIDATES = 128;
const STRICT_MAX_DECODED_BYTES = 65_536;
const HTTP_SCHEME_EVIDENCE = /h[\t\r\n]*t[\t\r\n]*t[\t\r\n]*p[\t\r\n]*(?:s[\t\r\n]*)?:/iu;
/**
* Detect URL userinfo without broadening this issue into general URL/network
* validation (the SSRF/redirect boundary is tracked separately in #767).
* Whole configured values use WHATWG parsing; strict bounded scanning catches
* nested, control-obfuscated, and special-scheme URL starts. Free-text error
* sanitization uses the separate conservative mode below.
*/
export function hasRegistryUrlCredentials(raw) {
return inspectStrictRegistryUrl(raw).unsafe;
}
/**
* The only formatter for configured registry URLs. Credential-free valid URLs
* retain their authored spelling; userinfo is removed structurally. Invalid
* strings fail closed rather than being echoed into a diagnostic.
*/
export function formatRegistryUrl(raw) {
const parsed = parseWholeHttpUrl(raw);
const inspection = inspectStrictRegistryUrl(raw);
if (parsed) {
if (!inspection.unsafe)
return raw;
let candidate = raw;
if (parsed.username || parsed.password) {
// A whole configured value is unambiguous. Always clear WHATWG userinfo
// structurally; textual deletion can otherwise stop at the first of
// several `@` characters and leave the real credential behind.
parsed.username = "";
parsed.password = "";
candidate = parsed.toString();
}
candidate = redactDetectedUserinfo(candidate, "strict");
return inspectStrictRegistryUrl(candidate).unsafe ? INVALID_REGISTRY_URL_LABEL : candidate;
}
const formatted = redactDetectedUserinfo(raw, "strict");
if (inspection.unsafe &&
registryUrlStarts(raw)[0]?.start === 0 &&
formatted !== raw &&
!inspectStrictRegistryUrl(formatted).unsafe) {
return formatted;
}
return INVALID_REGISTRY_URL_LABEL;
}
/** A consistent human label built on the safe URL formatter. */
export function formatRegistryLabel(entry) {
const url = formatRegistryUrl(entry.url);
return entry.name ? `${entry.name} (${url})` : url;
}
/** Actionable fail-closed warning for runtime inputs that bypass config load. */
export function formatRegistryCredentialWarning(entry) {
return `Registry ${formatRegistryLabel(entry)} was ignored. ${REGISTRY_CREDENTIALS_UNSUPPORTED}`;
}
/** Copy a registry entry for any structured-output boundary. */
export function registryEntryForOutput(entry) {
return { ...entry, url: formatRegistryUrl(entry.url) };
}
/**
* Remove userinfo from every HTTP(S) URL token in an untrusted error/warning
* string while leaving ordinary credential-free query/path text untouched.
*/
export function redactCredentialBearingUrls(message) {
return redactDetectedUserinfo(message, "diagnostic");
}
/** Fetch errors are untrusted: runtimes often include the request URL. */
export function formatRegistryError(error) {
return redactCredentialBearingUrls(toErrorMessage(error));
}
/**
* Configured/provider registry values are not prose. Inspect their URL
* structure recursively so encoded nested URLs cannot bypass validation,
* while keeping arbitrary diagnostic tokenization deliberately conservative.
*/
function inspectStrictRegistryUrl(raw) {
const state = { candidates: 0, decodedBytes: 0 };
const parsed = parseWholeHttpUrl(raw);
if (!parsed) {
inspectStrictComponent(raw, state, 0);
return state.reason ? { unsafe: true, reason: state.reason } : { unsafe: false };
}
inspectParsedStrictUrl(parsed, state, 0);
return state.reason ? { unsafe: true, reason: state.reason } : { unsafe: false };
}
function inspectParsedStrictUrl(parsed, state, depth) {
if (state.reason)
return;
if (parsed.username || parsed.password) {
state.reason = "credentials";
return;
}
inspectAuthoredStrictComponent(parsed.pathname, state, depth, false);
if (state.reason)
return;
// Inspect each authored query key/value independently. This mirrors
// URLSearchParams' first decoding layer while retaining whether a delimiter
// was structural (`&`) or encoded data (`%26`) inside one value.
const rawQuery = parsed.search.startsWith("?") ? parsed.search.slice(1) : parsed.search;
for (const pair of rawQuery.split("&")) {
const equals = pair.indexOf("=");
const rawKey = equals >= 0 ? pair.slice(0, equals) : pair;
const rawValue = equals >= 0 ? pair.slice(equals + 1) : "";
inspectAuthoredStrictComponent(rawKey, state, depth, true);
inspectAuthoredStrictComponent(rawValue, state, depth, true);
if (state.reason)
return;
}
const fragment = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
inspectAuthoredStrictComponent(fragment, state, depth, false);
}
function inspectAuthoredStrictComponent(raw, state, depth, queryEncoded) {
for (const authoredPart of splitAuthoredFieldComponents(raw)) {
let component = authoredPart;
if (queryEncoded) {
const decoded = decodePercentSafely(authoredPart, "query");
if (!decoded.valid && hasCredentialLikeUrlEvidence(authoredPart)) {
state.reason = "malformed-encoding";
return;
}
component = decoded.value;
}
inspectStrictComponent(component, state, depth);
if (state.reason)
return;
}
}
function inspectStrictComponent(raw, state, depth) {
if (state.reason || raw.length === 0)
return;
let decoded = raw;
let currentDepth = depth;
const seenCandidateKeys = new Set();
while (true) {
const starts = registryUrlStarts(decoded);
if (starts.length > 0) {
const unseenIndices = unseenCandidateIndices(decoded, starts, seenCandidateKeys);
const percentFullyCovered = inspectStrictUrlCandidates(decoded, starts, unseenIndices, state, currentDepth);
if (state.reason)
return;
if (percentFullyCovered)
return;
}
if (!decoded.includes("%"))
return;
if (currentDepth >= STRICT_MAX_DEPTH) {
if (looksLikeStrictUrlComponent(decoded))
state.reason = "inspection-limit";
return;
}
const next = decodePercentSafely(decoded, "component");
if (!next.valid) {
if (hasCredentialLikeUrlEvidence(decoded))
state.reason = "malformed-encoding";
return;
}
if (next.value === decoded)
return;
decoded = next.value;
currentDepth++;
}
}
function unseenCandidateIndices(decoded, starts, seenCandidateKeys) {
const occurrences = new Map();
const unseen = new Set();
for (let index = 0; index < starts.length; index++) {
const start = starts[index];
if (!start)
continue;
const authority = strictAuthoritySlice(decoded.slice(start.start));
const occurrence = (occurrences.get(authority) ?? 0) + 1;
occurrences.set(authority, occurrence);
const key = `${authority}\u0000${occurrence}`;
if (seenCandidateKeys.has(key))
continue;
seenCandidateKeys.add(key);
unseen.add(index);
}
return unseen;
}
function inspectStrictUrlCandidates(decoded, starts, unseenIndices, state, depth) {
const recursivelyInspectedRanges = [];
const bytes = new TextEncoder().encode(decoded).byteLength;
if (bytes > STRICT_MAX_DECODED_BYTES - state.decodedBytes) {
state.reason = "inspection-limit";
return false;
}
state.decodedBytes += bytes;
if (unseenIndices.size > STRICT_MAX_CANDIDATES - state.candidates) {
state.reason = "inspection-limit";
return false;
}
state.candidates += unseenIndices.size;
for (const start of starts) {
if (findStrictComponentUserinfoSpan(decoded, start)) {
state.reason = "credentials";
return false;
}
}
for (let index = 0; index < starts.length; index++) {
if (!unseenIndices.has(index))
continue;
const start = starts[index];
if (!start)
continue;
const nextStart = starts[index + 1]?.start ?? decoded.length;
const candidate = trimStrictCandidateSuffix(decoded.slice(start.start, nextStart));
const candidateDepth = inspectDecodedCandidateUserinfo(candidate, state, depth);
if (state.reason)
return false;
if (candidateDepth >= STRICT_MAX_DEPTH) {
state.reason = "inspection-limit";
return false;
}
const parsed = parseWholeHttpUrl(candidate);
if (!parsed)
continue;
// A later query/list email can make WHATWG interpret surrounding prose as
// userinfo. Trust that interpretation only when the component-bounded
// authority scan above found the same userinfo.
if (parsed.username || parsed.password)
continue;
inspectParsedStrictUrl(parsed, state, candidateDepth + 1);
if (state.reason)
return false;
recursivelyInspectedRanges.push({ start: start.start, end: nextStart });
}
return everyPercentEscapeIsCovered(decoded, recursivelyInspectedRanges);
}
function everyPercentEscapeIsCovered(raw, ranges) {
for (let cursor = raw.indexOf("%"); cursor >= 0; cursor = raw.indexOf("%", cursor + 1)) {
if (!ranges.some((range) => cursor >= range.start && cursor < range.end))
return false;
}
return true;
}
function inspectDecodedCandidateUserinfo(raw, state, depth) {
let decoded = strictAuthoritySlice(raw);
let currentDepth = depth;
while (true) {
if (registryUrlStarts(decoded).some((start) => findStrictComponentUserinfoSpan(decoded, start))) {
state.reason = "credentials";
return currentDepth;
}
if (!decoded.includes("%"))
return currentDepth;
if (currentDepth >= STRICT_MAX_DEPTH) {
state.reason = "inspection-limit";
return currentDepth;
}
const next = decodePercentSafely(decoded, "component");
if (!next.valid) {
if (hasCredentialLikeUrlEvidence(decoded))
state.reason = "malformed-encoding";
return currentDepth;
}
if (next.value === decoded)
return currentDepth;
decoded = strictAuthoritySlice(next.value);
currentDepth++;
}
}
function strictAuthoritySlice(raw) {
const start = registryUrlStarts(raw)[0];
if (!start)
return raw;
let end = raw.length;
for (let cursor = start.authorityStart; cursor < raw.length; cursor++) {
const char = raw[cursor];
if (char && AUTHORITY_STRUCTURAL_BOUNDARY.has(char)) {
end = cursor;
break;
}
}
return raw.slice(start.start, end);
}
function decodePercentSafely(raw, mode) {
const input = mode === "query" ? raw.replaceAll("+", "%20") : raw;
let valid = true;
let value = raw;
try {
value = decodeURIComponent(input);
}
catch {
valid = false;
}
if (mode === "query") {
return { value: new URLSearchParams(`value=${raw}`).get("value") ?? "", valid };
}
if (!valid)
return { value: raw, valid: false };
return { value, valid: true };
}
function splitAuthoredFieldComponents(raw) {
const parts = [];
let partStart = 0;
for (let cursor = 0; cursor < raw.length; cursor++) {
const delimiter = raw[cursor];
if (!delimiter || !AUTHORITY_AMBIGUOUS_BOUNDARY.has(delimiter))
continue;
if (!/^[a-z0-9_.-]+=/iu.test(raw.slice(cursor + 1)))
continue;
const prefix = raw.slice(partStart, cursor);
const starts = registryUrlStarts(prefix);
const lastStart = starts.at(-1);
if (!lastStart)
continue;
const candidate = trimStrictCandidateSuffix(prefix.slice(lastStart.start));
const parsed = parseWholeHttpUrl(candidate);
if (!parsed || parsed.username || parsed.password || !looksLikeDiagnosticHostPrefix(parsed.hostname))
continue;
parts.push(raw.slice(partStart, cursor));
partStart = cursor + 1;
}
parts.push(raw.slice(partStart));
return parts;
}
function findStrictComponentUserinfoSpan(raw, urlStart) {
const conventional = findUserinfoSpan(raw, urlStart, "strict");
if (conventional)
return conventional;
const { authorityStart } = urlStart;
let lastAt = -1;
for (let cursor = authorityStart; cursor < raw.length; cursor++) {
const char = raw[cursor];
if (char === undefined || AUTHORITY_STRUCTURAL_BOUNDARY.has(char))
break;
if (char === "@" && cursor > authorityStart)
lastAt = cursor;
}
if (lastAt <= authorityStart)
return undefined;
const prefix = raw.slice(authorityStart, lastAt);
const ambiguousBoundary = Array.from(prefix).findIndex((char) => AUTHORITY_AMBIGUOUS_BOUNDARY.has(char));
if (ambiguousBoundary < 0)
return undefined;
return { start: authorityStart, end: lastAt + 1 };
}
function trimStrictCandidateSuffix(raw) {
return raw.replace(/[\t\r\n ]+$/u, "").replace(/[,&;]+$/u, "");
}
function hasCredentialLikeUrlEvidence(raw) {
const evidence = decodePercentForEvidence(raw);
return HTTP_SCHEME_EVIDENCE.test(stripMalformedPercentForEvidence(evidence)) && evidence.includes("@");
}
function looksLikeStrictUrlComponent(raw) {
const evidence = decodePercentForEvidence(raw);
return HTTP_SCHEME_EVIDENCE.test(stripMalformedPercentForEvidence(evidence));
}
function decodePercentForEvidence(raw) {
let decoded = raw;
for (let layer = 0; layer <= STRICT_MAX_DEPTH; layer++) {
const next = decoded.replace(/%([0-9a-f]{2})/giu, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
if (next === decoded)
break;
decoded = next;
}
return decoded;
}
function stripMalformedPercentForEvidence(raw) {
return raw.replace(/%[^%]{0,2}/gu, "");
}
function redactDetectedUserinfo(raw, mode) {
let formatted = raw;
const starts = registryUrlStarts(raw);
for (let i = starts.length - 1; i >= 0; i--) {
const start = starts[i];
if (start === undefined)
continue;
const span = findUserinfoSpan(formatted, start, mode);
if (!span)
continue;
formatted = `${formatted.slice(0, span.start)}${formatted.slice(span.end)}`;
}
return formatted;
}
function registryUrlStarts(raw) {
return Array.from(raw.matchAll(HTTP_SCHEME_START), (match) => {
let authorityStart = match.index + match[0].length;
while (authorityStart < raw.length && AUTHORITY_PREFIX_SEPARATOR.has(raw[authorityStart] ?? "")) {
authorityStart++;
}
return { start: match.index, authorityStart };
});
}
function findUserinfoSpan(raw, urlStart, mode) {
const { authorityStart } = urlStart;
let crossedAmbiguousBoundary = false;
let lastAt = -1;
for (let cursor = authorityStart; cursor < raw.length; cursor++) {
const char = raw[cursor];
if (char === undefined)
return undefined;
if (char === "@") {
if (cursor > authorityStart)
lastAt = cursor;
continue;
}
if (AUTHORITY_STRUCTURAL_BOUNDARY.has(char))
return userinfoSpan(authorityStart, lastAt);
const isSoftBoundary = AUTHORITY_SOFT_BOUNDARY.has(char);
const isAmbiguousBoundary = AUTHORITY_AMBIGUOUS_BOUNDARY.has(char);
if (!isSoftBoundary && !isAmbiguousBoundary)
continue;
if (mode === "diagnostic" && cursor === authorityStart && isSoftBoundary)
return undefined;
if (lastAt >= 0 && mode === "diagnostic") {
const apparentHost = raw.slice(lastAt + 1, cursor);
if (looksLikeDiagnosticHostPrefix(apparentHost))
return userinfoSpan(authorityStart, lastAt);
}
if (crossedAmbiguousBoundary)
continue;
const prefix = raw.slice(authorityStart, cursor);
// Strict configured/provider handling crosses soft delimiters even after a
// host-shaped username: WHATWG accepts those values as nested userinfo.
// Conservative free-text handling stops at an apparent host so a later
// ordinary email is not consumed. Ambiguous query/list punctuation still
// requires an explicit username:password prefix in either mode.
if ((mode === "diagnostic" && looksLikeDiagnosticHostPrefix(prefix)) ||
(isAmbiguousBoundary && !prefix.includes(":"))) {
return userinfoSpan(authorityStart, lastAt);
}
crossedAmbiguousBoundary = true;
}
return userinfoSpan(authorityStart, lastAt);
}
function userinfoSpan(authorityStart, lastAt) {
return lastAt > authorityStart ? { start: authorityStart, end: lastAt + 1 } : undefined;
}
function looksLikeDiagnosticHostPrefix(raw) {
if (raw.startsWith("["))
return true;
const hostname = raw.split(":", 1)[0]?.toLowerCase() ?? "";
return hostname === "localhost" || hostname.startsWith("xn--") || hostname.includes(".");
}
function parseWholeHttpUrl(raw) {
try {
const parsed = new URL(raw);
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed : undefined;
}
catch {
return undefined;
}
}