{"version":3,"file":"url-source.mjs","names":[],"sources":["../../../../../../../../ai/src/skills/sources/url-source.ts"],"sourcesContent":["import { fetchTextWithPolicy } from \"../../security/outbound-policy\";\nimport type { OutboundPolicy } from \"../../security/outbound-policy.type\";\nimport type {\n  SkillCatalogEntry,\n  SkillRecord,\n} from \"../contracts/skill-record.type\";\nimport type { SkillsStoreContract } from \"../contracts/skills-store.contract\";\n\n/**\n * The JSON manifest shape a `url` source fetches — a flat array of skill\n * records. Bodies travel inline; the catalog projection drops them so the\n * always-injected metadata block stays cheap.\n */\ntype SkillManifest = SkillRecord[];\n\n/** Options for {@link urlSource} (S3). */\nexport type UrlSourceOptions = {\n  headers?: Record<string, string>;\n  /** Fetch hardening for the manifest request. */\n  policy?: OutboundPolicy;\n  /** Cache the manifest for this many ms; omit to cache for the source's lifetime. */\n  cacheTtlMs?: number;\n};\n\n/**\n * Read skills from a remote JSON manifest at `url`. The manifest is a flat\n * array of {@link SkillRecord}s (bodies inline).\n *\n * **Trust boundary (S3).** A remote skill source is a prompt supply chain —\n * its bodies flow straight into model context — so the manifest fetch runs\n * through the shared `OutboundPolicy` (scheme + host allowlist, post-DNS\n * private-IP deny, max bytes, timeout) and **every record is runtime-\n * validated** before it can be served. A malformed record fails loudly\n * rather than being cast blindly into a `SkillRecord`.\n *\n * The request is made lazily on the first `list()` / `load()`. The result\n * is cached for the source's lifetime, or for `cacheTtlMs` when set\n * (a stale cache refetches on next access).\n */\nexport function urlSource(\n  url: string,\n  options: UrlSourceOptions = {},\n): SkillsStoreContract {\n  const { headers, policy, cacheTtlMs } = options;\n\n  let cache: Promise<Map<string, SkillRecord>> | undefined;\n  let cachedAtMs: number | undefined;\n\n  const records = (): Promise<Map<string, SkillRecord>> => {\n    const expired =\n      cacheTtlMs !== undefined &&\n      cachedAtMs !== undefined &&\n      Date.now() - cachedAtMs > cacheTtlMs;\n\n    if (!cache || expired) {\n      cachedAtMs = Date.now();\n      cache = fetchManifest(url, headers, policy);\n    }\n\n    return cache;\n  };\n\n  return {\n    async list(scope?: { tags?: string[] }): Promise<SkillCatalogEntry[]> {\n      const all = await records();\n      const wanted = scope?.tags;\n\n      return [...all.values()]\n        .filter(record => record.type !== \"candidate\")\n        .filter(record => intersects(record.tags, wanted))\n        .map(toCatalogEntry);\n    },\n    async load(name: string, version?: number): Promise<SkillRecord | undefined> {\n      const all = await records();\n      const record = all.get(name);\n\n      if (!record || record.type === \"candidate\") {\n        return undefined;\n      }\n\n      if (version !== undefined && record.version !== version) {\n        return undefined;\n      }\n\n      return record;\n    },\n    async saveCandidate(): Promise<SkillRecord> {\n      throw new Error(\n        \"url source is read-only — saveCandidate requires a writable store (set `review.store`)\",\n      );\n    },\n    async promote(): Promise<SkillRecord> {\n      throw new Error(\n        \"url source is read-only — promote requires a writable store (set `review.store`)\",\n      );\n    },\n  };\n}\n\n/** Fetch + validate + parse the manifest once into a name → record map. */\nasync function fetchManifest(\n  url: string,\n  headers: Record<string, string> | undefined,\n  policy: OutboundPolicy | undefined,\n): Promise<Map<string, SkillRecord>> {\n  const result = await fetchTextWithPolicy(\n    url,\n    policy ?? {},\n    headers ? { headers } : undefined,\n  );\n\n  if (!result.ok) {\n    throw new Error(\n      `url skill source failed: ${result.status} ${result.statusText} for ${url}`,\n    );\n  }\n\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(result.text);\n  } catch (cause) {\n    throw new Error(`url skill source returned invalid JSON from ${url}`, {\n      cause,\n    });\n  }\n\n  if (!Array.isArray(parsed)) {\n    throw new Error(\n      `url skill source at ${url} must return a JSON array of skill records`,\n    );\n  }\n\n  const records = new Map<string, SkillRecord>();\n\n  (parsed as SkillManifest).forEach((raw, index) => {\n    const record = validateManifestRecord(raw, url, index);\n    records.set(record.name, record);\n  });\n\n  return records;\n}\n\n/**\n * Runtime-validate one manifest record before it is trusted as a\n * {@link SkillRecord}. Untyped remote JSON cast blindly into the context\n * is both an injection surface and a correctness bug; this rejects a\n * record missing the required `name` / `description` / `body` strings, and\n * fills `version` / `type` defaults for a thin record.\n */\nfunction validateManifestRecord(\n  raw: unknown,\n  url: string,\n  index: number,\n): SkillRecord {\n  if (!raw || typeof raw !== \"object\") {\n    throw new Error(`url skill source at ${url}: record #${index} is not an object`);\n  }\n\n  const r = raw as Record<string, unknown>;\n  const requireString = (field: string): string => {\n    const value = r[field];\n    if (typeof value !== \"string\" || value.length === 0) {\n      throw new Error(\n        `url skill source at ${url}: record #${index} is missing a string \"${field}\"`,\n      );\n    }\n    return value;\n  };\n\n  const name = requireString(\"name\");\n  const description = requireString(\"description\");\n  const body = requireString(\"body\");\n\n  const type =\n    r.type === \"authored\" || r.type === \"promoted\" || r.type === \"candidate\"\n      ? r.type\n      : \"authored\";\n  const version = typeof r.version === \"number\" ? r.version : 1;\n  const tags = Array.isArray(r.tags)\n    ? r.tags.filter((t): t is string => typeof t === \"string\")\n    : undefined;\n  const metadata =\n    r.metadata && typeof r.metadata === \"object\"\n      ? (r.metadata as Record<string, unknown>)\n      : undefined;\n\n  return { name, description, body, version, type, tags, metadata };\n}\n\n/** Project a full record down to its catalog entry (body omitted). */\nfunction toCatalogEntry(record: SkillRecord): SkillCatalogEntry {\n  return {\n    name: record.name,\n    description: record.description,\n    version: record.version,\n    tags: record.tags,\n    type: record.type,\n  };\n}\n\n/** True when no filter is requested or the record shares a requested tag. */\nfunction intersects(recordTags: string[] | undefined, wanted: string[] | undefined): boolean {\n  if (!wanted || wanted.length === 0) {\n    return true;\n  }\n\n  if (!recordTags || recordTags.length === 0) {\n    return false;\n  }\n\n  return recordTags.some(tag => wanted.includes(tag));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuCA,SAAgB,UACd,KACA,UAA4B,CAAC,GACR;CACrB,MAAM,EAAE,SAAS,QAAQ,eAAe;CAExC,IAAI;CACJ,IAAI;CAEJ,MAAM,gBAAmD;EACvD,MAAM,UACJ,eAAe,UACf,eAAe,UACf,KAAK,IAAI,IAAI,aAAa;EAE5B,IAAI,CAAC,SAAS,SAAS;GACrB,aAAa,KAAK,IAAI;GACtB,QAAQ,cAAc,KAAK,SAAS,MAAM;EAC5C;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM,KAAK,OAA2D;GACpE,MAAM,MAAM,MAAM,QAAQ;GAC1B,MAAM,SAAS,OAAO;GAEtB,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,CACrB,QAAO,WAAU,OAAO,SAAS,WAAW,CAAC,CAC7C,QAAO,WAAU,WAAW,OAAO,MAAM,MAAM,CAAC,CAAC,CACjD,IAAI,cAAc;EACvB;EACA,MAAM,KAAK,MAAc,SAAoD;GAE3E,MAAM,UAAS,MADG,QAAQ,EACR,CAAC,IAAI,IAAI;GAE3B,IAAI,CAAC,UAAU,OAAO,SAAS,aAC7B;GAGF,IAAI,YAAY,UAAa,OAAO,YAAY,SAC9C;GAGF,OAAO;EACT;EACA,MAAM,gBAAsC;GAC1C,MAAM,IAAI,MACR,wFACF;EACF;EACA,MAAM,UAAgC;GACpC,MAAM,IAAI,MACR,kFACF;EACF;CACF;AACF;;AAGA,eAAe,cACb,KACA,SACA,QACmC;CACnC,MAAM,SAAS,MAAM,oBACnB,KACA,UAAU,CAAC,GACX,UAAU,EAAE,QAAQ,IAAI,MAC1B;CAEA,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MACR,4BAA4B,OAAO,OAAO,GAAG,OAAO,WAAW,OAAO,KACxE;CAGF,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO,IAAI;CACjC,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,+CAA+C,OAAO,EACpE,MACF,CAAC;CACH;CAEA,IAAI,CAAC,MAAM,QAAQ,MAAM,GACvB,MAAM,IAAI,MACR,uBAAuB,IAAI,2CAC7B;CAGF,MAAM,0BAAU,IAAI,IAAyB;CAE7C,AAAC,OAAyB,SAAS,KAAK,UAAU;EAChD,MAAM,SAAS,uBAAuB,KAAK,KAAK,KAAK;EACrD,QAAQ,IAAI,OAAO,MAAM,MAAM;CACjC,CAAC;CAED,OAAO;AACT;;;;;;;;AASA,SAAS,uBACP,KACA,KACA,OACa;CACb,IAAI,CAAC,OAAO,OAAO,QAAQ,UACzB,MAAM,IAAI,MAAM,uBAAuB,IAAI,YAAY,MAAM,kBAAkB;CAGjF,MAAM,IAAI;CACV,MAAM,iBAAiB,UAA0B;EAC/C,MAAM,QAAQ,EAAE;EAChB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAI,MACR,uBAAuB,IAAI,YAAY,MAAM,wBAAwB,MAAM,EAC7E;EAEF,OAAO;CACT;CAEA,MAAM,OAAO,cAAc,MAAM;CACjC,MAAM,cAAc,cAAc,aAAa;CAC/C,MAAM,OAAO,cAAc,MAAM;CAEjC,MAAM,OACJ,EAAE,SAAS,cAAc,EAAE,SAAS,cAAc,EAAE,SAAS,cACzD,EAAE,OACF;CAUN,OAAO;EAAE;EAAM;EAAa;EAAM,SATlB,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;EASjB;EAAM,MARpC,MAAM,QAAQ,EAAE,IAAI,IAC7B,EAAE,KAAK,QAAQ,MAAmB,OAAO,MAAM,QAAQ,IACvD;EAMmD,UAJrD,EAAE,YAAY,OAAO,EAAE,aAAa,WAC/B,EAAE,WACH;CAE0D;AAClE;;AAGA,SAAS,eAAe,QAAwC;CAC9D,OAAO;EACL,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,MAAM,OAAO;CACf;AACF;;AAGA,SAAS,WAAW,YAAkC,QAAuC;CAC3F,IAAI,CAAC,UAAU,OAAO,WAAW,GAC/B,OAAO;CAGT,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO;CAGT,OAAO,WAAW,MAAK,QAAO,OAAO,SAAS,GAAG,CAAC;AACpD"}