Ctrl K
ComunidadGuías…

src/setup.lambda.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as AWSLambda from 'aws-lambda';
import { baseUrlFromDomain, GitHubSecrets, loadOctokitRest } from './lambda-github';
import { getSecretJsonValue, updateSecretValue } from './lambda-helpers';

type ApiGatewayEvent = AWSLambda.APIGatewayProxyEvent | AWSLambda.APIGatewayProxyEventV2;

const nonce = crypto.randomBytes(64).toString('hex');

function getHtml(baseUrl: string, token: string, domain: string): string {
  return fs.readFileSync('index.html', 'utf-8')
    .replace(/INSERT_WEBHOOK_URL_HERE/g, process.env.WEBHOOK_URL!)
    .replace(/INSERT_BASE_URL_HERE/g, baseUrl)
    .replace(/INSERT_TOKEN_HERE/g, token)
    .replace(/INSERT_SECRET_ARN_HERE/g, process.env.SETUP_SECRET_ARN!)
    .replace(/INSERT_DOMAIN_HERE/g, domain)
    .replace(/<script/g, `<script nonce="${nonce}"`)
    .replace(/<style/g, `<style nonce="${nonce}"`);
}

function response(code: number, body: string): AWSLambda.APIGatewayProxyResultV2 {
  return {
    statusCode: code,
    headers: {
      'Content-Type': 'text/html',
      'Content-Security-Policy': `default-src 'unsafe-inline' 'nonce-${nonce}'; img-src data:; connect-src 'self'; form-action https:; frame-ancestors 'none'; object-src 'none'; base-uri 'self'`,
    },
    body: body,
  };
}

async function handleRoot(event: ApiGatewayEvent, setupToken: string): Promise<AWSLambda.APIGatewayProxyResultV2> {
  const stage = event.requestContext.stage == '$default' ? '' : `/${event.requestContext.stage}`;
  const setupBaseUrl = `https://${event.requestContext.domainName}${stage}`;
  const githubSecrets: GitHubSecrets = await getSecretJsonValue(process.env.GITHUB_SECRET_ARN);

  return response(200, getHtml(setupBaseUrl, setupToken, githubSecrets.domain));
}

function decodeBody(event: ApiGatewayEvent) {
  let body = event.body;
  if (!body) {
    throw new Error('No body found');
  }
  if (event.isBase64Encoded) {
    body = Buffer.from(body, 'base64').toString('utf-8');
  }
  return JSON.parse(body);
}

async function handleDomain(event: ApiGatewayEvent): Promise<AWSLambda.APIGatewayProxyResultV2> {
  const body = decodeBody(event);
  if (!body.domain) {
    return response(400, 'Invalid domain');
  }
  if (body.runnerLevel !== 'repo' && body.runnerLevel !== 'org') {
    return response(400, 'Invalid runner registration level');
  }

  const githubSecrets: GitHubSecrets = await getSecretJsonValue(process.env.GITHUB_SECRET_ARN);
  githubSecrets.domain = body.domain;
  githubSecrets.runnerLevel = body.runnerLevel;
  await updateSecretValue(process.env.GITHUB_SECRET_ARN, JSON.stringify(githubSecrets));
  return response(200, 'Domain set');
}

async function handlePat(event: ApiGatewayEvent): Promise<AWSLambda.APIGatewayProxyResultV2> {
  const body = decodeBody(event);
  if (!body.pat || !body.domain) {
    return response(400, 'Invalid personal access token');
  }

  // default to 'repo' for backwards compatibility with older setup pages that didn't ask for a registration level
  const runnerLevel = body.runnerLevel ?? 'repo';
  if (runnerLevel !== 'repo' && runnerLevel !== 'org') {
    return response(400, 'Invalid runner registration level');
  }

  await updateSecretValue(process.env.GITHUB_SECRET_ARN, JSON.stringify(<GitHubSecrets>{
    domain: body.domain,
    appId: -1,
    personalAuthToken: body.pat,
    runnerLevel,
  }));
  await updateSecretValue(process.env.SETUP_SECRET_ARN, JSON.stringify({ token: '' }));

  return response(200, 'Personal access token set');
}

async function handleNewApp(event: ApiGatewayEvent): Promise<AWSLambda.APIGatewayProxyResultV2> {
  if (!event.queryStringParameters) {
    return response(400, 'Invalid code');
  }

  const code = event.queryStringParameters.code;

  if (!code) {
    return response(400, 'Invalid code');
  }

  const githubSecrets: GitHubSecrets = await getSecretJsonValue(process.env.GITHUB_SECRET_ARN);
  const baseUrl = baseUrlFromDomain(githubSecrets.domain);
  const { Octokit } = await loadOctokitRest();
  const newApp = await new Octokit({ baseUrl }).rest.apps.createFromManifest({ code });

  githubSecrets.appId = newApp.data.id;
  githubSecrets.domain = new URL(newApp.data.html_url).host; // just in case it's different
  githubSecrets.personalAuthToken = '';
  // don't update runnerLevel as it was set by handleDomain() above

  await updateSecretValue(process.env.GITHUB_SECRET_ARN, JSON.stringify(githubSecrets));
  await updateSecretValue(process.env.GITHUB_PRIVATE_KEY_SECRET_ARN, newApp.data.pem);
  await updateSecretValue(process.env.WEBHOOK_SECRET_ARN, JSON.stringify({
    webhookSecret: newApp.data.webhook_secret,
  }));
  await updateSecretValue(process.env.SETUP_SECRET_ARN, JSON.stringify({ token: '' }));

  return response(200, `New app set. <a href="${newApp.data.html_url}/installations/new">Install it</a> for your repositories.`);
}

async function handleExistingApp(event: ApiGatewayEvent): Promise<AWSLambda.APIGatewayProxyResultV2> {
  const body = decodeBody(event);

  if (!body.appid || !body.pk || !body.domain || (body.runnerLevel !== 'repo' && body.runnerLevel !== 'org')) {
    return response(400, 'Missing fields');
  }

  await updateSecretValue(process.env.GITHUB_SECRET_ARN, JSON.stringify(<GitHubSecrets>{
    domain: body.domain,
    appId: body.appid,
    personalAuthToken: '',
    runnerLevel: body.runnerLevel,
  }));
  await updateSecretValue(process.env.GITHUB_PRIVATE_KEY_SECRET_ARN, body.pk as string);
  await updateSecretValue(process.env.SETUP_SECRET_ARN, JSON.stringify({ token: '' }));

  return response(200, 'Existing app set. Don\'t forget to set up the webhook.');
}

export async function handler(event: ApiGatewayEvent): Promise<AWSLambda.APIGatewayProxyResultV2> {
  // confirm required environment variables
  if (!process.env.WEBHOOK_URL) {
    throw new Error('Missing environment variables');
  }

  const setupToken = (await getSecretJsonValue(process.env.SETUP_SECRET_ARN)).token;

  // bail out if setup was already completed
  if (!setupToken) {
    return response(200, 'Setup already complete. Put a new token in the setup secret if you want to redo it.');
  }

  if (!event.queryStringParameters) {
    return response(403, 'Wrong setup token.');
  }

  // safely confirm url token matches our secret
  const urlToken = event.queryStringParameters.token || event.queryStringParameters.state || '';
  if (urlToken.length != setupToken.length || !crypto.timingSafeEqual(Buffer.from(urlToken, 'utf-8'), Buffer.from(setupToken, 'utf-8'))) {
    return response(403, 'Wrong setup token.');
  }

  // handle requests
  try {
    const path = (event as AWSLambda.APIGatewayProxyEvent).path ?? (event as AWSLambda.APIGatewayProxyEventV2).rawPath;
    const method = (event as AWSLambda.APIGatewayProxyEvent).httpMethod ?? (event as AWSLambda.APIGatewayProxyEventV2).requestContext.http.method;
    if (path == '/') {
      return await handleRoot(event, setupToken);
    } else if (path == '/domain' && method == 'POST') {
      return await handleDomain(event);
    } else if (path == '/pat' && method == 'POST') {
      return await handlePat(event);
    } else if (path == '/complete-new-app' && method == 'GET') {
      return await handleNewApp(event);
    } else if (path == '/app' && method == 'POST') {
      return await handleExistingApp(event);
    } else {
      return response(404, 'Not found');
    }
  } catch (e) {
    console.error({
      notice: 'Setup handler failed',
      error: e,
    });
    return response(500, `<b>Error:</b> ${e}`);
  }
}