lib/generator/index.ts
import 'colors';
import * as cheerio from 'cheerio';
import * as fs from 'fs';
import * as glob from 'glob';
import * as request from 'request';
import {
OptionalKind,
ParameterDeclarationStructure,
Project,
QuoteKind,
Scope,
} from 'ts-morph';
import { Operator, ResourceTypes } from '../shared';
import { AccessLevelList } from '../shared/access-level';
import { Conditions } from './condition';
import {
arnFixer,
conditionFixer,
conditionKeyFixer,
fixes,
serviceFixer,
} from './fixes';
import { formatCode } from './format';
export { indexManagedPolicies } from './managed-policies';
const project = new Project();
project.manipulationSettings.set({
quoteKind: QuoteKind.Single,
});
const modules: Module[] = [];
const timeThreshold = new Date();
let threshold = 25;
const thresholdOverride = process.env.NOCACHE;
if (thresholdOverride?.length) {
threshold += 999999999;
}
timeThreshold.setHours(timeThreshold.getHours() - threshold);
interface Stats {
actions: string[];
conditions: string[];
resources: string[];
}
const serviceStats: string[] = [];
const conditionTypeDefaults: Record<
string,
{
url: string;
default: Operator | string;
type: string[];
}
> = {
string: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_String',
default: Operator.stringLike,
type: ['string'],
},
arn: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_ARN',
default: Operator.arnLike,
type: ['string'],
},
numeric: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_Numeric',
default: Operator.numericEquals,
type: ['number'],
},
date: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_Date',
default: Operator.dateEquals,
type: ['Date', 'string'],
},
ipaddress: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_IPAddress',
default: Operator.ipAddress,
type: ['string'],
},
binary: {
url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_BinaryEquals',
default: Operator.binaryEquals,
type: ['string'],
},
};
export interface Module {
name?: string;
servicePrefix?: string;
filename: string;
url?: string;
actionList?: Actions;
resourceTypes?: ResourceTypes;
fixes?: Record<string, any>;
conditions?: Conditions;
}
export type Actions = Record<string, Action>;
export interface Action {
url: string;
description: string;
accessLevel: string;
resourceTypes?: Record<string, ResourceTypeOnAction>;
conditions?: string[];
dependentActions?: string[];
}
export interface ResourceTypeOnAction {
required: boolean;
conditions?: string[];
}
export function getAwsServices(): Promise<string[]> {
return getAwsServicesFromIamDocs();
}
function getAwsServicesFromIamDocs(): Promise<string[]> {
return new Promise((resolve, reject) => {
const url =
'https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actions-resources-contextkeys.html';
requestWithRetry(url)
.then((body) => {
const re = /href="\.\/list_(.*?)\.html"/g;
let match: RegExpExecArray;
const services: string[] = [];
do {
match = re.exec(body)!;
if (match) {
services.push(match[1]);
}
} while (match);
if (!services.length) {
return reject(`Unable to find services on ${url}`);
}
// set env `SERVICE` to generate only a single service for testing purpose
const testOverride = process.env.SERVICE;
if (testOverride?.length) {
return resolve([testOverride]);
}
const unique = services.filter((elem, pos) => {
return services.indexOf(elem) == pos;
});
resolve(unique.sort());
})
.catch((err: Error) => {
reject(err);
});
});
}
export function getContent(service: string): Promise<Module> {
service = serviceFixer(service);
process.stdout.write(`${service}: `.white);
process.stdout.write('Fetching '.grey);
const urlPattern =
'https://docs.aws.amazon.com/service-authorization/latest/reference/list_%s.html';
return new Promise(async (resolve, reject) => {
const shortName = service.replace(/^(amazon|aws)/, '');
try {
let module: Module = {
filename: shortName.replace(/[^a-z0-9-]/i, '-'),
};
const url = urlPattern.replace('%s', service);
const cachedFile = `lib/generated/policy-statements/.cache/${module.filename}.ts`;
if (fs.existsSync(cachedFile)) {
const lastModified = await getLastModified(url);
if (lastModified < timeThreshold) {
console.log(`Skipping, last modified on ${lastModified}`.green);
return resolve(module);
}
}
requestWithRetry(url)
.then((body) => {
process.stdout.write('Parsing '.blue);
const $ = cheerio.load(body);
const servicePrefix = $('code').first().text().trim();
if (servicePrefix == '') {
console.error(`PREFIX NOT FOUND FOR ${service} / ${url}`.red.bold);
}
module.name = servicePrefix;
module.servicePrefix = servicePrefix;
module.url = url;
if (shortName in fixes) {
module.fixes = fixes[shortName];
}
module = addConditions($, module);
module = addActions($, module);
module = addResourceTypes($, module);
resolve(module);
})
.catch((err: Error) => {
reject(err);
});
} catch (error: Error) {
reject(error);
}
});
}
export function createModules(services: string[]): Promise<void> {
createCache();
return new Promise(async (resolve, reject) => {
for (const service of services) {
await getContent(service).then(createModule).catch(reject);
}
writeServiceStats();
resolve();
});
}
function writeStatsFile(file: string, data: string[]) {
if (fs.existsSync(file)) {
const contents = fs
.readFileSync(file, 'utf8')
.split('\n')
.filter((n) => n);
data.push(...contents);
}
const uniqueValues = data
.filter(function (elem, pos) {
return data.indexOf(elem) == pos;
})
.sort();
const content = `${uniqueValues.join('\n')}\n`;
fs.writeFileSync(file, content);
}
function writeStats(module: string, stats: Stats) {
process.stdout.write('Stats '.grey);
(Object.keys(stats) as (keyof Stats)[]).forEach(function (key) {
const filePath = `./stats/${key}/${module}`;
writeStatsFile(filePath, stats[key]);
});
}
function writeServiceStats() {
const servicesFile = './stats/services';
if (fs.existsSync(servicesFile)) {
fs.unlinkSync(servicesFile);
}
writeStatsFile(servicesFile, serviceStats);
}
export function createModule(module: Module): Promise<void> {
const stats: Stats = {
actions: [],
conditions: [],
resources: [],
};
if (typeof module.name === 'undefined') {
//it was skipped, restore from cache
const moduleFilePath = `lib/generated/policy-statements/${module.filename}.ts`;
restoreFileFromCache(moduleFilePath);
const moduleProject = new Project();
moduleProject.manipulationSettings.set({
quoteKind: QuoteKind.Single,
});
const moduleFile = moduleProject.addSourceFileAtPath(moduleFilePath);
module.servicePrefix = moduleFile
.getClasses()[0]
.getProperty('servicePrefix')!
.getInitializer()!
.getText()
.split("'")
.join('');
}
serviceStats.push(module.servicePrefix!);
if (typeof module.name === 'undefined') {
restoreFileFromCache(`stats/actions/${module.servicePrefix}`);
restoreFileFromCache(`stats/conditions/${module.servicePrefix}`);
restoreFileFromCache(`stats/resources/${module.servicePrefix}`);
modules.push(module);
return Promise.resolve();
}
process.stdout.write(`Generating `.cyan);
if (module.fixes && 'name' in module.fixes) {
module.name = module.fixes.name;
} else if (
module.filename.endsWith('v2') &&
module.name.slice(-2).toLowerCase() !== 'v2'
) {
module.name += '-v2';
}
modules.push(module);
const sourceFile = project.createSourceFile(
`./lib/generated/policy-statements/${module.filename}.ts`,
);
const description = `\nStatement provider for service [${module.name}](${module.url}).\n\n@param sid [SID](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html) of the statement`;
sourceFile.addImportDeclaration({
namedImports: ['AccessLevelList'],
moduleSpecifier: '../../shared/access-level',
});
const classDeclaration = sourceFile.addClass({
name: camelCase(module.name!),
extends: 'PolicyStatement',
isExported: true,
});
classDeclaration.addJsDoc({
description: description,
});
classDeclaration.addProperty({
name: 'servicePrefix',
scope: Scope.Public,
initializer: `'${module.servicePrefix}'`,
});
const constructor = classDeclaration.addConstructor({});
constructor.addParameter({
name: 'sid',
type: 'string',
hasQuestionToken: true,
});
constructor.setBodyText('super(sid);');
constructor.addJsDoc({
description: description,
});
/**
* We collect the access levels and their actions in this object
*/
const accessLevelList: AccessLevelList = {};
for (const [name, action] of Object.entries(module.actionList!)) {
if (!(action.accessLevel in accessLevelList)) {
accessLevelList[action.accessLevel] = [];
}
accessLevelList[action.accessLevel].push(name);
stats.actions.push(`${module.servicePrefix}:${name};${action.accessLevel}`);
const method = classDeclaration.addMethod({
name: `to${upperFirst(name)}`,
scope: Scope.Public,
});
method.setBodyText(`return this.to('${name}');`);
let desc = `\n${action.description}\n\nAccess Level: ${action.accessLevel}`;
if ('conditions' in action) {
desc += '\n\nPossible conditions:';
action.conditions?.forEach((condition) => {
desc += `\n- .${createConditionName(
module.conditions![condition].key,
module.servicePrefix!,
)}()`;
});
}
if ('dependentActions' in action) {
desc += '\n\nDependent actions:';
action.dependentActions?.forEach((dependentAction) => {
desc += `\n- ${dependentAction}`;
});
}
if (action.url.length && action.url != 'https://docs.aws.amazon.com/') {
desc += `\n\n${action.url}`;
}
method.addJsDoc({
description: desc,
});
}
classDeclaration.addProperty({
name: 'accessLevelList',
scope: Scope.Protected,
type: 'AccessLevelList',
initializer: JSON.stringify(accessLevelList, null, 2)
.split('"') // ensure we use single quotes
.join("'")
.replace(/^ {2}'([^' ]+)'/gm, '$1'), // remove quotes from single word keys
});
for (const [name, resourceType] of Object.entries(module.resourceTypes)) {
const method = classDeclaration.addMethod({
name: `on${camelCase(name)}`,
scope: Scope.Public,
});
stats.resources.push(`${module.servicePrefix}:${name}`);
const params = getArnPlaceholders(resourceType.arn);
const optionalMethodParameters: OptionalKind<ParameterDeclarationStructure>[] =
[];
params.forEach((param) => {
if (/^(Partition|Region|Account(Id)?)$/.test(param)) {
optionalMethodParameters.push({
name: lowerFirst(camelCase(param)),
type: 'string',
hasQuestionToken: true,
});
} else {
method.addParameter({
name: lowerFirst(camelCase(param)),
type: 'string',
hasQuestionToken: false,
});
}
});
if (optionalMethodParameters.length) {
method.addParameters(optionalMethodParameters);
}
let arn = resourceType.arn;
const methodBody: string[] = [];
let paramDocs = '';
params.forEach((param) => {
const paramName = lowerFirst(camelCase(param));
let orDefault = '';
if (param == 'Partition') {
orDefault = ` ?? this.defaultPartition`;
paramDocs += `\n@param ${paramName} - Partition of the AWS account [aws, aws-cn, aws-us-gov]; defaults to \`aws\`, unless using the CDK, where the default is the current Stack's partition.`;
} else if (param == 'Region') {
orDefault = ` ?? this.defaultRegion`;
paramDocs += `\n@param ${paramName} - Region of the resource; defaults to \`*\`, unless using the CDK, where the default is the current Stack's region.`;
} else if (param.match(/^Account(Id)?$/)) {
orDefault = ` ?? this.defaultAccount`;
paramDocs += `\n@param ${paramName} - Account of the resource; defaults to \`*\`, unless using the CDK, where the default is the current Stack's account.`;
} else {
paramDocs += `\n@param ${paramName} - Identifier for the ${paramName}.`;
}
if (orDefault) {
arn = arn.replace(`\$\{${param}\}`, `\$\{${paramName}${orDefault}\}`);
} else {
arn = arn.replace(`\$\{${param}\}`, `\$\{${paramName}\}`);
}
});
let desc = `\nAdds a resource of type ${resourceType.name} to the statement`;
if (
resourceType.url.length &&
resourceType.url != 'https://docs.aws.amazon.com/'
) {
desc += `\n\n${resourceType.url}`;
}
desc += `\n${paramDocs}`;
if (resourceType.conditionKeys.length) {
desc += '\n\nPossible conditions:';
resourceType.conditionKeys.forEach((key) => {
desc += `\n- .${createConditionName(
module.conditions![key].key,
module.servicePrefix!,
)}()`;
});
}
method.addJsDoc({
description: desc,
});
methodBody.push(`return this.on(\`${arn}\`);`);
method.setBodyText(methodBody.join('\n'));
}
let hasConditions = false;
for (let [key, condition] of Object.entries(module.conditions!)) {
key = condition.key;
const parts = key.split(':');
const name = parts[1].split(/\/(?=[$<]|$)/);
stats.conditions.push(`${module.servicePrefix}:${parts[1]}`);
// boolean conditions don't take operators
if (condition.type != 'boolean') {
hasConditions = true;
}
var desc = '';
if (condition.description.length) {
desc += `\n${condition.description}\n`;
}
if (condition.url.length) {
desc += `\n${condition.url}\n`;
}
if ('relatedActions' in condition && condition.relatedActions?.length) {
desc += '\nApplies to actions:\n';
condition.relatedActions
.filter((elem, pos) => {
return condition.relatedActions?.indexOf(elem) == pos;
})
.forEach((relatedAction) => {
desc += `- .to${camelCase(relatedAction)}()\n`;
});
}
if (
'relatedResourceTypes' in condition &&
condition.relatedResourceTypes?.length
) {
desc += '\nApplies to resource types:\n';
condition.relatedResourceTypes
.filter((elem, pos) => {
return condition.relatedResourceTypes?.indexOf(elem) == pos;
})
.forEach((resourceType) => {
desc += `- ${resourceType}\n`;
});
}
const type = condition.type.toLowerCase();
const methodBody: string[] = [];
let methodName = createConditionName(key, module.servicePrefix!);
if (name.length > 1 && !name[1].length) {
// special case for ec2:ResourceTag/ - not sure this is correct, the description makes zero sense...
methodName += 'Exists';
} else if (name.length == 1 && name[0] == 'Attribute') {
// special case for ec2:Attribute
methodName += 'Exists';
}
const method = classDeclaration.addMethod({
name: methodName,
scope: Scope.Public,
});
let propsKey = '';
if (parts[0] != module.servicePrefix) {
propsKey += `${parts[0]}:`;
}
propsKey += name[0];
if (name.length > 1) {
// it is a parameterized condition
propsKey += '/';
if (name[1].length) {
const paramName = name[1].replace(/[^a-zA-Z0-9]/g, '');
desc += `\n@param ${lowerFirst(paramName)} The tag key to check`;
method.addParameter({
name: lowerFirst(paramName),
type: 'string',
});
propsKey += `\${${lowerFirst(paramName)}}`;
}
}
if (type in conditionTypeDefaults) {
let types = [...conditionTypeDefaults[type].type];
if ('typeOverride' in condition) {
types = condition.typeOverride!;
}
if (types.length > 1) {
types.push(`(${types.join('|')})[]`);
} else {
types.push(`${types}[]`);
}
desc += `\n@param value The value(s) to check`;
method.addParameter({
name: 'value',
type: types.join(' | '),
});
desc += `\n@param operator Works with [${type} operators](${
conditionTypeDefaults[type].url
}). **Default:** \`${conditionTypeDefaults[type].default.toString()}\``;
method.addParameter({
name: 'operator',
type: 'Operator | string',
hasQuestionToken: true,
});
if (type == 'date') {
methodBody.push(
'if (typeof (value as Date).getMonth === "function") {',
' value = (value as Date).toISOString();',
'} else if (Array.isArray(value)) {',
' value = value.map((item) => {',
' if (typeof (item as Date).getMonth === "function") {',
' item = (item as Date).toISOString();',
' }',
' return item;',
' });',
'}',
);
}
methodBody.push(
`return this.if(\`${propsKey}\`, value, operator ?? '${conditionTypeDefaults[
type
].default.toString()}')`,
);
} else if (type == 'boolean') {
desc += '\n@param value `true` or `false`. **Default:** `true`';
method.addParameter({
name: 'value',
type: type,
hasQuestionToken: true,
});
methodBody.push(
`return this.if(\`${propsKey}\`, (typeof value !== 'undefined' ? value : true), 'Bool');`,
);
} else {
throw new Error(`Unexpected condition type: ${type} for ${name}`);
}
method.addJsDoc({
description: desc,
});
method.setBodyText(methodBody.join('\n'));
}
const sharedClasses = ['PolicyStatement'];
if (hasConditions) {
sharedClasses.push('Operator');
}
sourceFile.addImportDeclaration({
namedImports: sharedClasses,
moduleSpecifier: '../../shared',
});
formatCode(sourceFile);
const done = sourceFile.save();
writeStats(module.servicePrefix!, stats);
console.log('Done'.green);
return done;
}
export function createIndex() {
const filePath = './lib/generated/index.ts';
process.stdout.write('index: '.white);
process.stdout.write('Generating '.cyan);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
const sourceFile = project.createSourceFile(filePath, '', {
overwrite: true,
});
modules.sort().forEach((module) => {
const source = project.addSourceFileAtPath(
`./lib/generated/policy-statements/${module.filename}.ts`,
);
const exports: string[] = [];
source.getClasses().forEach((item) => {
if (item.isExported()) {
exports.push(item.getName()!);
}
});
sourceFile.addExportDeclaration({
namedExports: exports,
moduleSpecifier: `./policy-statements/${module.filename}`,
});
});
formatCode(sourceFile);
const done = sourceFile.save();
console.log('Done'.green);
return done;
}
function cleanDescription(description: string): string {
return description
.replace(/[\r\n]+/g, ' ')
.replace(/\s{2,}/g, ' ')
.replace(/<code>(.*?)<\/code>/g, '`$1`')
.trim();
}
export function getArnPlaceholders(arn: string): RegExpMatchArray {
const matches = arn.match(/(?<=\$\{)[a-z0-9_-]+(?=\})/gi);
const toTheEnd: string[] = [];
while (matches?.length) {
if (/^(Partition|Region|Account(Id)?)$/.test(matches[0])) {
toTheEnd.push(matches.shift()!);
} else {
break;
}
}
matches?.push(...toTheEnd.reverse());
return matches!;
}
function upperFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function lowerFirst(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}
export function camelCase(str: string) {
return str
.split(/[_-\s\./]/)
.map((str) => {
return upperFirst(str);
})
.join('');
}
function createCache() {
createLibCache();
createStatsCache();
}
function createLibCache() {
const dir = 'lib/generated/policy-statements';
mkDirCache(dir, '*.ts');
}
function createStatsCache() {
['actions', 'conditions', 'resources'].forEach((dirName) => {
const dir = `stats/${dirName}`;
mkDirCache(dir, '*');
});
}
function mkDirCache(dir: string, pattern: string) {
const cacheDir = `${dir}/.cache`;
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
for (const file of glob.sync(`${dir}/${pattern}`)) {
const fileName = file.split('/').slice(-1)[0];
fs.renameSync(file, `${cacheDir}/${fileName}`);
}
}
function restoreFileFromCache(filename: string) {
const splitPath = filename.split('/');
const file = splitPath.pop();
splitPath.push('.cache');
splitPath.push(file!);
const cachedFile = splitPath.join('/');
if (!fs.existsSync(cachedFile)) {
return;
}
fs.renameSync(cachedFile, filename);
}
function getLastModified(url: string): Promise<Date> {
return new Promise((resolve, reject) => {
requestWithRetry(url, { method: 'HEAD' })
.then((lastModified: string) => {
let mod = new Date();
if (lastModified !== '') {
mod = new Date(lastModified);
}
resolve(mod);
})
.catch((err: Error) => {
reject(err);
});
});
}
function getTable($: cheerio.Root, title: string) {
const table = $('.table-container table')
.toArray()
.filter((element) => {
return $(element).find('th').first().text() == title;
});
return $(table[0]);
}
function addActions($: cheerio.Root, module: Module): Module {
const actions: Actions = {};
const tableActions = getTable($, 'Actions');
let action: string;
tableActions.find('tr').each((_, element) => {
const tds = $(element).find('td');
const tdLength = tds.length;
let first = tds.first();
if (tdLength == 6) {
// it's a new action
action = first.text().replace('[permission only]', '').trim();
actions[action] = {
url: validateUrl(first.find('a[href]').attr('href')?.trim()),
description: cleanDescription(first.next().text().trim()),
accessLevel: first.next().next().text().trim(),
};
first = first.next().next().next();
}
if (tdLength != 6 && tdLength != 3) {
const content = cleanDescription(tds.text());
if (content.length && !content.startsWith('SCENARIO:')) {
console.warn(
`skipping row due to unexpected number of fields: ${content}`.yellow,
);
}
return;
}
let resourceType = first.text().trim();
let required = false;
const conditionKeys = first.next().find('p');
const dependentActions = first.next().next().find('p');
const conditions: string[] = [];
if (conditionKeys.length) {
conditionKeys.each((_: unknown, conditionKey: string) => {
const condition = conditionKeyFixer(
module.servicePrefix!,
cleanDescription($(conditionKey).text()),
);
if (!module.conditions![condition]) {
console.log(
`[Skipping referenced condition, since it is not documented: ${condition}]`
.red,
);
return;
}
conditions.push(condition);
if (!('relatedActions' in module.conditions![condition])) {
module.conditions![condition].relatedActions = [];
}
module.conditions![condition].relatedActions?.push(action);
});
}
if (dependentActions.length) {
actions[action].dependentActions = [];
dependentActions.each((_: unknown, dependentAction: string) => {
actions[action].dependentActions?.push(
cleanDescription($(dependentAction).text()),
);
});
}
if (resourceType.length) {
if (typeof actions[action].resourceTypes == 'undefined') {
actions[action].resourceTypes = {};
}
if (resourceType.indexOf('*') >= 0) {
resourceType = resourceType.slice(0, -1);
required = true;
}
actions[action].resourceTypes![resourceType] = {
required: required,
};
if (conditions.length) {
actions[action].resourceTypes![resourceType].conditions = conditions;
}
} else if (conditions.length) {
actions[action].conditions = conditions;
}
});
module.actionList = actions;
return module;
}
function addResourceTypes($: cheerio.Root, module: Module): Module {
const resourceTypes: ResourceTypes = {};
const tableResourceTypes = getTable($, 'Resource types');
tableResourceTypes.find('tr').each((_, element) => {
const tds = $(element).find('td');
const name = tds.first().text().trim();
const url = validateUrl(tds.first().find('a[href]').attr('href')?.trim());
const arn = tds.first().next().text().trim();
if (!name.length && !arn.length) {
return;
}
const conditionKeys = tds
.first()
.next()
.next()
.find('p')
.toArray()
.map((element) => {
return conditionKeyFixer(
module.servicePrefix!,
$(element).text().trim(),
);
});
conditionKeys.forEach((condition: string) => {
if (!('relatedResourceTypes' in module.conditions![condition])) {
module.conditions![condition].relatedResourceTypes = [];
}
module.conditions![condition].relatedResourceTypes?.push(name);
});
if (name.length) {
resourceTypes[name] = {
name: name,
url: url,
arn: arnFixer(module.servicePrefix!, name, arn),
conditionKeys: conditionKeys,
};
}
});
module.resourceTypes = resourceTypes;
return module;
}
function addConditions($: cheerio.Root, module: Module): Module {
const conditions: Conditions = {};
const table = getTable($, 'Condition keys');
table.find('tr').each((_, element) => {
const tds = $(element).find('td');
const key = tds.first().text().trim();
const url = validateUrl(tds.first().find('a[href]').attr('href')?.trim());
const description = cleanDescription(tds.first().next().text());
const type = tds.first().next().next().text().trim();
if (key.length) {
const condition = conditionFixer(module.servicePrefix!, {
key: key,
description: description,
type: type,
url: url,
isGlobal: key.startsWith('aws:'),
});
conditions[condition.key] = condition;
}
});
module.conditions = conditions;
return module;
}
function createConditionName(key: string, servicePrefix: string): string {
let methodName = 'if';
const split = key.split(/:|\/(?=[$<]|$)/);
// these are exceptions for the Security Token Service to:
// - make it clear to which provider the condition is for
// - avoid duplicate method names
if (split[0] == 'accounts.google.com') {
methodName += 'Google';
} else if (split[0] == 'cognito-identity.amazonaws.com') {
methodName += 'Cognito';
} else if (split[0] == 'www.amazon.com') {
methodName += 'Amazon';
} else if (split[0] == 'graph.facebook.com') {
methodName += 'Facebook';
} else if (split[0] != servicePrefix) {
// for global conditions and conditions related to other services
methodName += upperFirst(split[0]);
}
methodName += upperFirst(camelCase(split[1]));
return methodName;
}
function validateUrl(url: string) {
if (typeof url == 'undefined') {
return '';
}
try {
new URL(url);
} catch (_: any) {
console.warn(`Removed invalid URL ${url}`.red);
return '';
}
return url;
}
function requestWithRetry(
url: string,
options: request.CoreOptions = {},
retries = 3,
backoff = 300,
): Promise<any> {
options.headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
};
return new Promise((resolve, reject) => {
const retry = (retries: number, backoff: number) => {
request(url, options, (err, response, body) => {
if (err) {
const failDetails =
retries > 0 ? `Retry in ${backoff * 2}` : 'Giving up';
console.log(`Failed to fetch ${url} - ${failDetails}`);
if (retries > 0) {
setTimeout(() => {
retry(--retries, backoff * 2);
}, backoff);
} else {
reject(err);
}
} else {
if ('method' in options && options.method == 'HEAD') {
if ('last-modified' in response.headers) {
resolve(response.headers['last-modified']);
} else resolve('');
} else {
resolve(body);
}
}
});
};
retry(retries, backoff);
});
}