{"version":3,"file":"docker-hub-utils.cjs.production.min.js","sources":["../src/types/DockerHubRepo.ts","../src/utils/log.ts","../src/services/DockerHubAPI.ts","../src/utils/constants.ts"],"sourcesContent":["/**\n * This is a direct representation of what we get back from the `/repositories`\n * API call.\n */\nexport interface DockerHubAPIRepo {\n readonly can_edit: boolean\n readonly description: string\n readonly is_automated: boolean\n readonly is_migrated: boolean\n readonly is_private: boolean\n readonly last_updated: string\n readonly name: string\n readonly namespace: string\n readonly pull_count: number\n readonly repository_type: string\n readonly star_count: number\n readonly status: number\n readonly user: string\n}\n\n/**\n * Union type representing the architecture defined in part of an OCI image's\n * manifest list.\n *\n * As specified in the Docker Manifest spec, any valid GOARCH values are valid\n * image architecture values, and vice versa:\n * > The platform object describes the platform which the image in the manifest\n * > runs on. A full list of valid operating system and architecture values are\n * > listed in the Go language documentation for $GOOS and $GOARCH\n * @see https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list-field-descriptions\n */\nexport enum Architecture {\n i386 = '386',\n amd64 = 'amd64',\n arm = 'arm',\n arm64 = 'arm64',\n mips = 'mips',\n mips64 = 'mips64',\n mips64le = 'mips64le',\n mipsle = 'mipsle',\n ppc64 = 'ppc64',\n ppc64le = 'ppc64le',\n s390x = 's390x',\n wasm = 'wasm',\n}\n\n/**\n * Union type representing the OS defined in part of an OCI image's\n * manifest list.\n * See the docs for the `Architecture` type above for more info.\n */\nexport enum OS {\n aix = 'aix',\n android = 'android',\n darwin = 'darwin',\n dragonfly = 'dragonfly',\n freebsd = 'freebsd',\n illumos = 'illumos',\n js = 'js',\n linux = 'linux',\n netbsd = 'netbsd',\n openbsd = 'openbsd',\n plan9 = 'plan9',\n solaris = 'solaris',\n windows = 'windows',\n}\n\nexport enum ManifestMediaType {\n Manifest = 'application/vnd.docker.distribution.manifest.v2+json',\n ManifestList = 'application/vnd.docker.distribution.manifest.list.v2+json',\n}\n\n/**\n * Yes, there's *way* more information contained in the manifest / \"fat\"\n * manifestList than just architectures, but I find this to be the most\n * relevant section for my projects. PR's welcome.\n */\nexport interface DockerManifest {\n readonly digest: string\n readonly mediaType: ManifestMediaType\n readonly platform: Array<{\n architecture: Architecture\n os: OS\n }>\n readonly schemaVersion: 1 | 2 | number\n}\n\nexport interface DockerManifestList {\n readonly manifests: DockerManifest[]\n readonly mediaType: ManifestMediaType\n readonly schemaVersion: 1 | 2 | any\n}\n\nexport interface DockerHubRepo {\n // ========================\n // Main fields of interest\n // ========================\n readonly description: string | null | undefined\n readonly lastUpdated: string\n readonly name: string\n readonly pullCount: number\n readonly starCount: number\n readonly user: string\n\n // Manifest type *may* be nested within this interface, but is usually\n // fetched and returned separately.\n readonly manifestList?: DockerManifestList\n readonly tags?: Tag[]\n\n // =============================================\n // Other stuff that comes down through the API,\n // that some may find useful\n // =============================================\n readonly canEdit?: boolean\n readonly isAutomated?: boolean\n readonly isMigrated?: boolean\n readonly isPrivate?: boolean\n readonly namespace?: string\n readonly repositoryType?: string\n readonly status?: number\n}\n\nexport interface Tag {\n creator: number\n fullSize: number\n id: number\n images: TagElement[]\n lastUpdated: string\n lastUpdater: number\n lastUpdaterUsername: string\n name: string\n repository: number\n v2: boolean\n}\n\nexport interface TagElement {\n architecture: Architecture\n digest: string\n features: string\n os: OS\n size: number\n}\n","import pino from 'pino'\n\nexport default pino({ base: null, useLevelLabels: true })\n","import axios from 'axios'\nimport camelcaseKeys from 'camelcase-keys'\nimport { DateTime } from 'luxon'\nimport R from 'ramda'\nimport log from '../utils/log'\n\nimport {\n DockerHubAPIRepo,\n DockerHubRepo,\n DockerManifestList,\n Tag,\n} from '../types/DockerHubRepo'\nimport {\n DOCKER_HUB_API_AUTH_URL,\n DOCKER_HUB_API_ROOT,\n} from '../utils/constants'\n\n/**\n * Currently only supports fetching the manifest for the `latest` tag; in\n * reality, we can pass any valid content digest[1] to retrieve the manifest(s)\n * for that image.\n *\n * [1]: https://github.com/opencontainers/distribution-spec/blob/master/spec.md#content-digests\n */\nconst createManifestListURL = ({ repo }: { repo: DockerHubRepo }): string =>\n `https://registry-1.docker.io/v2/${repo.user}/${repo.name}/manifests/latest`\n\nconst createUserReposListURL = (user: string): string =>\n `${DOCKER_HUB_API_ROOT}repositories/${user}`\n\n/**\n * The OCI distribution spec requires a unique token for each repo manifest queried.\n */\nexport const fetchDockerHubToken = async (\n repo: DockerHubRepo,\n): Promise => {\n const { name, user } = repo\n const tokenRequest = await axios.get(DOCKER_HUB_API_AUTH_URL, {\n params: {\n scope: `repository:${user}/${name}:pull`,\n service: 'registry.docker.io',\n },\n })\n\n const token: string | undefined = R.path(['data', 'token'], tokenRequest)\n if (!token) {\n throw new Error('Unable to retrieve auth token from registry.')\n }\n return token\n}\n\n/**\n * Pure function that massages the Docker Hub API response into the\n * format we want to return. e.g., only extracting certain fields;\n * converting snake_case to camelCase, etc.\n */\nexport const extractRepositoryDetails = (\n repos: DockerHubAPIRepo[],\n lastUpdatedSince?: DateTime,\n): DockerHubRepo[] => {\n if (!repos || R.isEmpty(repos)) {\n return []\n }\n\n const parsedRepos: DockerHubRepo[] = (camelcaseKeys(\n repos,\n ) as unknown) as DockerHubRepo[]\n\n if (R.isNil(lastUpdatedSince)) {\n return parsedRepos\n }\n\n return parsedRepos.filter(\n repo => DateTime.fromISO(repo.lastUpdated) < lastUpdatedSince,\n )\n}\n\n/**\n * Query a single repository given a repo name and username.\n *\n * @param user The DockerHub username or org name to query for.\n * @param name The DockerHub repo name -- restrict to this single repo.\n */\nexport const queryRepo = async ({\n name,\n user,\n}: {\n name: string\n user: string\n}): Promise => {\n const repoResult = await axios.request({\n url: `${DOCKER_HUB_API_ROOT}repositories/${user}/${name}/`,\n })\n const repo: DockerHubRepo | undefined = R.prop('data', repoResult)\n if (repoResult.status !== 200 || !repo || R.isEmpty(repo)) {\n return\n }\n return (camelcaseKeys(repo) as unknown) as DockerHubRepo\n}\n\n/**\n * Top-level function for querying repositories.\n *\n * @TODO Rename to just `queryRepos`.\n *\n * @param user The DockerHub username or org name to query for.\n * @param numRepos The number of repos to query (max 100).\n * @param lastUpdatedSince Filter by the DateTime at which a repo was last updated.\n */\nexport const queryTopRepos = async ({\n lastUpdatedSince,\n numRepos = 100,\n user,\n}: {\n lastUpdatedSince?: DateTime\n numRepos?: number\n user: string\n}): Promise => {\n if (numRepos > 100) {\n throw new RangeError('Number of repos to query cannot exceed 100.')\n }\n\n const listReposURL = createUserReposListURL(user)\n const repoResults = await axios.get(listReposURL, {\n params: { page: 1, page_size: numRepos },\n })\n const repos: DockerHubAPIRepo[] = R.path(\n ['data', 'results'],\n repoResults,\n ) as DockerHubAPIRepo[]\n\n return extractRepositoryDetails(repos, lastUpdatedSince)\n}\n\n/**\n * Query image tags.\n */\nexport const queryTags = async (\n repo: DockerHubRepo,\n): Promise => {\n const repoUrl = createUserReposListURL(repo.user)\n const tagsUrl = `${repoUrl}/${repo.name}/tags?page_size=100`\n const tagsResults = await axios.get(tagsUrl)\n const tags = R.path(['data', 'results'], tagsResults)\n if (!tags || R.isEmpty(tags)) {\n return\n }\n // @ts-ignore\n return camelcaseKeys(tags)\n}\n\n/**\n * Queries the Docker Hub API to retrieve a \"fat manifest\", an object of\n * `Content-Type` `application/vnd.docker.distribution.manifest.list.v2+json/`.\n * Read up on the Manifest v2, Schema 2 Spec in more detail:\n * @see https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md\n * Or the shiny new OCI distribution spec which builds on it:\n * @see https://github.com/opencontainers/distribution-spec/blob/f67bc11ba3a083a9c62f8fa53ad14c5bcf2116af/spec.md\n */\nexport const fetchManifestList = async (\n repo: DockerHubRepo,\n): Promise => {\n // Docker Hub requires a unique token for each repo manifest queried.\n const token = await fetchDockerHubToken(repo)\n\n const manifestListURL = createManifestListURL({ repo })\n const manifestListResponse = await axios.get(manifestListURL, {\n headers: {\n Accept: 'application/vnd.docker.distribution.manifest.list.v2+json',\n Authorization: `Bearer ${token}`,\n },\n })\n // For now, just ignore legacy V1 schema manifests. They have an entirely\n // different response shape and it's not worth mucking up the schema to\n // support a legacy format.\n if (manifestListResponse.data.schemaVersion === 1) {\n log.info('Schema version 1 is unsupported.', repo.name)\n return\n }\n\n return R.path(['data'], manifestListResponse)\n}\n","export const DOCKER_CLOUD_URL = 'https://cloud.docker.com/repository/docker/'\nexport const DOCKER_HUB_API_ROOT = 'https://hub.docker.com/v2/'\nexport const DOCKER_HUB_API_AUTH_URL = 'https://auth.docker.io/token'\n"],"names":["Architecture","OS","ManifestMediaType","pino","base","useLevelLabels","createUserReposListURL","user","DOCKER_HUB_API_ROOT","fetchDockerHubToken","repo","axios","get","params","scope","name","service","tokenRequest","token","R","path","Error","extractRepositoryDetails","repos","lastUpdatedSince","isEmpty","parsedRepos","camelcaseKeys","isNil","filter","DateTime","fromISO","lastUpdated","manifestListURL","createManifestListURL","headers","Accept","Authorization","manifestListResponse","data","schemaVersion","log","info","request","url","repoResult","prop","status","repoUrl","tagsResults","tags","numRepos","RangeError","listReposURL","page","page_size","repoResults"],"mappings":"8IA+BYA,EAoBAC,EAgBAC,sHApCAF,EAAAA,uBAAAA,qCAEVA,gBACAA,YACAA,gBACAA,cACAA,kBACAA,sBACAA,kBACAA,gBACAA,oBACAA,gBACAA,cAQF,SAAYC,GACVA,YACAA,oBACAA,kBACAA,wBACAA,oBACAA,oBACAA,UACAA,gBACAA,kBACAA,oBACAA,gBACAA,oBACAA,oBAbF,CAAYA,IAAAA,QAgBAC,EAAAA,4BAAAA,+FAEVA,2ECnEF,MAAeC,EAAK,CAAEC,KAAM,KAAMC,gBAAgB,ICyB5CC,EAAyB,SAACC,SAC3BC,0CAAmCD,GAK3BE,WACXC,8BAG2BC,EAAMC,ICnCI,+BDmCyB,CAC5DC,OAAQ,CACNC,oBAHmBJ,EAATH,SAASG,EAAfK,aAIJC,QAAS,wCAHPC,OAOAC,EAA4BC,EAAEC,KAAK,CAAC,OAAQ,SAAUH,OACvDC,QACG,IAAIG,MAAM,uDAEXH,yCAQII,EAA2B,SACtCC,EACAC,OAEKD,GAASJ,EAAEM,QAAQF,SACf,OAGHG,EAAgCC,EACpCJ,UAGEJ,EAAES,MAAMJ,GACHE,EAGFA,EAAYG,QACjB,SAAAnB,UAAQoB,WAASC,QAAQrB,EAAKsB,aAAeR,sCCvEV,2DADJ,iID+JjCd,8BAGoBD,EAAoBC,mBAAlCQ,OAEAe,EA7IsB,gBAAGvB,IAAAA,8CACIA,EAAKH,SAAQG,EAAKK,yBA4I7BmB,CAAsB,CAAExB,KAAAA,2BACbC,EAAMC,IAAIqB,EAAiB,CAC5DE,QAAS,CACPC,OAAQ,4DACRC,wBAAyBnB,qBAHvBoB,MAS0C,IAA5CA,EAAqBC,KAAKC,qBAKvBrB,EAAEC,KAAK,CAAC,QAASkB,GAJtBG,EAAIC,KAAK,mCAAoChC,EAAKK,kFA5FpDA,IAAAA,KACAR,IAAAA,gCAKyBI,EAAMgC,QAAQ,CACrCC,IAAQpC,0CAAmCD,MAAQQ,wBAD/C8B,OAGAnC,EAAkCS,EAAE2B,KAAK,OAAQD,MAC7B,MAAtBA,EAAWE,QAAmBrC,IAAQS,EAAEM,QAAQf,UAG5CiB,EAAcjB,qEAyCtBA,WAEMsC,EAAU1C,EAAuBI,EAAKH,6BAElBI,EAAMC,IADboC,MAAWtC,EAAKK,4CAC7BkC,OACAC,EAAO/B,EAAEC,KAAK,CAAC,OAAQ,WAAY6B,MACpCC,IAAQ/B,EAAEM,QAAQyB,UAIhBvB,EAAcuB,gFAtCrB1B,IAAAA,qBACA2B,SAAAA,aAAW,MACX5C,IAAAA,YAMI4C,EAAW,UACP,IAAIC,WAAW,mDAGjBC,EAAe/C,EAAuBC,0BAClBI,EAAMC,IAAIyC,EAAc,CAChDxC,OAAQ,CAAEyC,KAAM,EAAGC,UAAWJ,qBAD1BK,OAGAjC,EAA4BJ,EAAEC,KAClC,CAAC,OAAQ,WACToC,UAGKlC,EAAyBC,EAAOC"}