@sentry/wizard
Version:
Sentry wizard helping you to configure your project
208 lines (187 loc) • 5.78 kB
text/typescript
import type { Answers } from 'inquirer';
import { prompt } from 'inquirer';
import * as _ from 'lodash';
import { dim } from '../Helper/Logging';
import { getCurrentIntegration } from '../Helper/Wizard';
import { BaseStep } from './BaseStep';
export class PromptForParameters extends BaseStep {
public async emit(answers: Answers): Promise<Answers> {
this.debug(answers);
if (!(await getCurrentIntegration(answers).shouldEmit(answers))) {
return {};
}
if (this._argv.quiet) {
return {};
}
let url = this._getFullUrl(answers);
const organization: any = await prompt([
{
message: 'Organization Slug:',
name: 'slug',
type: 'input',
// eslint-disable-next-line @typescript-eslint/unbound-method
validate: this._validateSlug,
when: this._shouldAsk(answers, 'config.organization.slug', () => {
dim('Please copy/paste your organization slug');
dim(`It can be found in the url ${url}`);
}),
},
]);
url = this._getFullUrl(answers, organization.slug);
const project: any = await prompt([
{
message: 'Project Slug:',
name: 'slug',
type: 'input',
// eslint-disable-next-line @typescript-eslint/unbound-method
validate: this._validateSlug,
when: this._shouldAsk(answers, 'config.project.slug', () => {
dim('Please copy/paste your project slug');
dim(`It can be found in the url ${url}`);
}),
},
]);
url = this._getFullUrl(answers, organization.slug, project.slug);
const dsnKeyUrl = this._getDSNKeyUrl(answers, project.slug);
const dsn = await prompt([
{
message: 'DSN:',
name: 'public',
type: 'input',
// eslint-disable-next-line @typescript-eslint/unbound-method
validate: this._validateDSN,
when: this._shouldAsk(answers, 'config.dsn.public', () => {
dim('Please copy/paste your DSN');
dim(`It can be found here: ${dsnKeyUrl}`);
}),
},
]);
const auth = await prompt([
{
message: 'Auth Token:',
name: 'token',
type: 'input',
// eslint-disable-next-line @typescript-eslint/unbound-method
validate: this._validateAuthToken,
when: this._shouldAsk(answers, 'config.auth.token', () => {
dim('Please copy/paste your auth token');
dim(`It can be found here: ${this._argv.url}api/`);
dim(
'In case there is none yet, create one with [project:releases] permission',
);
}),
},
]);
return {
config: _.merge(_.get(answers, 'config'), {
auth,
dsn,
project,
organization,
}),
};
}
private _getFullUrl(
answers: Answers,
organizationSlug?: string,
projectSlug?: string,
): string {
const baseUrl = this._argv.url;
const orgSlug = _.get(
answers,
'config.organization.slug',
organizationSlug || 'organization_slug',
);
const projSlug = _.get(
answers,
'config.project.slug',
projectSlug || 'project_slug',
);
return `${baseUrl}${orgSlug}/${projSlug}`;
}
private _getDSNKeyUrl(answers: Answers, projectSlug?: string): string {
const baseUrl = this._argv.url;
const projSlug = _.get(
answers,
'config.project.slug',
projectSlug || 'project_slug',
);
return `${baseUrl}settings/projects/${projSlug}/keys`;
}
private _shouldAsk(
answers: Answers,
configKey: string,
preHook?: () => void,
): boolean {
const shouldAsk = _.get(answers, configKey, null) === null;
if (shouldAsk && preHook) {
preHook();
}
return shouldAsk;
}
private _validateAuthToken(input: string): boolean | string {
const isOrgToken = input.startsWith('sntrys_');
if (isOrgToken) {
if (!isValidOrgToken(input)) {
return 'Make sure you correctly copied your auth token. It should start with "sntrys_"';
}
return true;
}
if (!input.match(/(sntrys_)?[0-9a-f]{64}/g)) {
return 'Make sure you copied the correct auth token, it should be 64 hex chars';
}
return true;
}
private _validateSlug(input: string): boolean | string {
if (input.match(/[A-Z]/g)) {
return 'Please copy the slug from the url, it should be all lowercase';
}
if (input.length === 0) {
return "Can't be empty";
}
return true;
}
private _validateDSN(input: string): boolean | string {
const match = input.match(
/^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w.-]+)(?::(\d+))?(\/.*)$/,
);
if (!match) {
return 'Invalid DSN format';
}
if (match[1] !== 'http' && match[1] !== 'https') {
return `Unsupported protocol for DSN: ${match[1]}`;
}
return true;
}
}
type MaybeOrgAuthToken = {
iat?: number;
url?: string;
org?: string;
region_url?: string;
};
/**
* Trying to parse and decode an org auth token. Based on:
* - https://github.com/getsentry/rfcs/blob/main/text/0091-ci-upload-tokens.md#parsing-tokens
* - https://github.com/getsentry/rfcs/blob/main/text/0091-ci-upload-tokens.md#token-facts
*/
function isValidOrgToken(input: string): boolean {
if (!input.startsWith('sntrys_')) {
return false;
}
const tokenParts = input.split('_');
if (tokenParts.length < 3) {
return false;
}
try {
const payload = tokenParts[1];
const decodedPayload = Buffer.from(payload, 'base64').toString();
const jsonPayload = JSON.parse(decodedPayload) as MaybeOrgAuthToken;
if (!jsonPayload.iat || !jsonPayload.url || !jsonPayload.org) {
return false;
}
} catch {
return false;
}
return true;
}