Compare commits

..

14 Commits

Author SHA1 Message Date
Steven
a6807c9d21 Publish Canary
- vercel@23.0.1-canary.1
 - @vercel/python@2.0.4-canary.0
2021-06-01 14:35:09 -04:00
Steven
c628090d08 [cli] Fix vc projects rm race condition (#6306)
The call to `GET /projects/info` is used to check existence but it can cause a race condition if the project was removed before the `DELETE /v2/projects` is called.

Instead, we rely on the response from `DELETE /v2/projects` to determine if the project exists or not.

This will also allow us to remove a legacy API endpoint in the future (see related API PR)
2021-06-01 18:31:20 +00:00
Hydrophobefireman
4e0b291ed1 [python] Remove imports from werkzeug._compat (#6283) 2021-06-01 09:15:06 -04:00
JJ Kasper
ee0bc9b0c8 Publish Canary
- @vercel/build-utils@2.11.1-canary.0
 - vercel@23.0.1-canary.0
 - @vercel/client@10.1.1-canary.0
 - @vercel/frameworks@0.4.1-canary.0
 - @vercel/routing-utils@1.11.2-canary.0
2021-05-26 13:44:39 -05:00
JJ Kasper
e516c1f49f Ensure beforeFiles rewrites come after redirects when continuing (#6289) 2021-05-26 12:50:51 -05:00
JJ Kasper
01f53f36fc [routing-utils] Ensure header key value casing is normalized (#6284)
This ensures we normalize header `key` values in `has` items to be lower-case as the proxy currently only matches against the lower-case variant. Updated superstatic tests to ensure the header key is normalized correctly. 

### Related Issues

[related thread](https://vercel.slack.com/archives/C01N3RWTE5V/p1621937306006400)

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-05-25 22:46:28 +00:00
Nathan Rajlich
f2d396caae Publish Stable
- @vercel/build-utils@2.11.0
 - vercel@23.0.0
 - @vercel/client@10.1.0
 - @vercel/frameworks@0.4.0
 - @vercel/node@1.11.0
 - @vercel/python@2.0.3
 - @vercel/routing-utils@1.11.1
2021-05-25 14:56:11 -07:00
Matheus Fernandes
001f2f60b8 Use proper Apache License format (#6189)
The existing LICENSE file was missing the Appendix, and also
used weird formatting. Now it's an exact copy of the original:

https://www.apache.org/licenses/LICENSE-2.0.txt.
2021-05-25 13:27:32 -07:00
Nathan Rajlich
78ca930287 [cli] Show user's name in vc switch command (#6288)
This more closely matches the Team picker on vercel.com.

Will still show "email" if no "name" is defined.
2021-05-25 13:18:14 -07:00
Nathan Rajlich
b03e18df12 Publish Canary
- vercel@22.0.2-canary.7
 - @vercel/python@2.0.3-canary.0
2021-05-25 11:07:16 -07:00
Nathan Rajlich
3a6b8b072c [cli] Reauthenticate scopes with limited access in vc switch (#6280)
In the `vc switch` command, if your current access token results in
"limited" Team information being returned, then show a lock emoji
next to the team/user name in the select input.

When a locked scope is selected, then pre-emptively prompt the
user to re-authenticate using a valid login method in relation to
the desired scope.

https://user-images.githubusercontent.com/71256/119441172-87abae80-bcda-11eb-801a-cb6837bae353.mov

[ch21964]
2021-05-25 11:03:06 -07:00
Steven
d480cd6bbd [cli] Bump codecov to 3.8.2 (#6279)
Closes #5814
2021-05-24 23:16:02 +00:00
Nathan Rajlich
181b624bf4 [cli] Add SAML reauthentication logic when using different scope (#6263)
When the API returns a SAML error, then show the proper reauthentication prompt depending on the scope being requested:

Team with SAML enforced, shows only SSO login option:

<img width="476" alt="Screen Shot 2021-05-24 at 1 50 29 PM" src="https://user-images.githubusercontent.com/71256/119406131-31694c00-bc97-11eb-858a-52e5fe7052d1.png">

Team with SAML enabled, but not enforced, prompts with all login methods:

<img width="352" alt="Screen Shot 2021-05-24 at 1 50 36 PM" src="https://user-images.githubusercontent.com/71256/119406134-3201e280-bc97-11eb-9166-60fbfec47ee0.png">

Team without SAML enabled, or User scope, shows prompt with SSO option removed:

<img width="366" alt="Screen Shot 2021-05-24 at 1 50 44 PM" src="https://user-images.githubusercontent.com/71256/119406137-3201e280-bc97-11eb-8c5c-b88eb9983500.png">

[ch21964]
2021-05-24 15:16:24 -07:00
Nathan Rajlich
200495e4ce [cli] Use ts-eager to execute build script (#6278)
* [cli] Use `ts-eager` to execute build script

* Use `node -r ts-eager/register` to workaround Windows issue
2021-05-24 18:14:55 -04:00
37 changed files with 563 additions and 335 deletions

16
LICENSE
View File

@@ -1,6 +1,7 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -175,13 +176,24 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Vercel, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

View File

@@ -2,7 +2,7 @@
"name": "vercel-monorepo",
"version": "0.0.0",
"private": true,
"license": "MIT",
"license": "Apache-2.0",
"workspaces": {
"packages": [
"packages/*"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.10.3-canary.4",
"version": "2.11.1-canary.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -29,7 +29,7 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.3.3-canary.3",
"@vercel/frameworks": "0.4.1-canary.0",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "22.0.2-canary.6",
"version": "23.0.1-canary.1",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -17,8 +17,8 @@
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"build": "ts-node ./scripts/build.ts",
"build-dev": "ts-node ./scripts/build.ts --dev"
"build": "node -r ts-eager/register ./scripts/build.ts",
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev"
},
"nyc": {
"include": [
@@ -61,10 +61,10 @@
"node": ">= 12"
},
"dependencies": {
"@vercel/build-utils": "2.10.3-canary.4",
"@vercel/build-utils": "2.11.1-canary.0",
"@vercel/go": "1.2.2",
"@vercel/node": "1.10.1-canary.2",
"@vercel/python": "2.0.2",
"@vercel/node": "1.11.0",
"@vercel/python": "2.0.4-canary.0",
"@vercel/ruby": "1.2.6",
"update-notifier": "4.1.0"
},
@@ -89,7 +89,7 @@
"@types/mri": "1.1.0",
"@types/ms": "0.7.30",
"@types/node": "11.11.0",
"@types/node-fetch": "2.1.4",
"@types/node-fetch": "2.5.10",
"@types/npm-package-arg": "6.1.0",
"@types/pluralize": "0.0.29",
"@types/progress": "2.0.3",
@@ -100,7 +100,7 @@
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.3.3-canary.3",
"@vercel/frameworks": "0.4.1-canary.0",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",
@@ -117,7 +117,7 @@
"chalk": "4.1.0",
"chokidar": "3.3.1",
"clipboardy": "2.1.0",
"codecov": "3.7.1",
"codecov": "3.8.2",
"cpy": "7.2.0",
"credit-card": "3.0.1",
"date-fns": "1.29.0",
@@ -169,6 +169,7 @@
"title": "3.4.1",
"tmp-promise": "1.0.3",
"tree-kill": "1.2.2",
"ts-eager": "2.0.2",
"ts-node": "8.3.0",
"typescript": "3.9.3",
"universal-analytics": "0.4.20",

View File

@@ -73,9 +73,9 @@ export default async function login(client: Client): Promise<number> {
if (input) {
// Email or Team slug was provided via command line
if (validateEmail(input)) {
result = await doEmailLogin(input, params);
result = await doEmailLogin(params, input);
} else {
result = await doSsoLogin(input, params);
result = await doSsoLogin(params, input);
}
} else {
// Interactive mode

View File

@@ -187,25 +187,22 @@ async function run({ client, contextName }) {
const name = args[0];
// Check the existence of the project
try {
await client.fetch(`/projects/info/${e(name)}`);
} catch (err) {
if (err.status === 404) {
console.error(error('No such project exists'));
return exit(1);
}
}
const yes = await readConfirmation(name);
if (!yes) {
console.error(error('User abort'));
return exit(0);
}
await client.fetch(`/v2/projects/${name}`, {
try {
await client.fetch(`/v2/projects/${e(name)}`, {
method: 'DELETE',
});
} catch (err) {
if (err.status === 404) {
console.error(error('No such project exists'));
return exit(1);
}
}
const elapsed = ms(new Date() - start);
console.log(
`${chalk.cyan('> Success!')} Project ${chalk.bold(

View File

@@ -115,8 +115,6 @@ export default async function main(client) {
try {
({ contextName } = await getScope(client));
} catch (err) {
client.close();
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
@@ -210,7 +208,6 @@ export default async function main(client) {
.map(id => chalk.bold(`"${id}"`))
.join(', ')}. Run ${getCommandName('ls')} to list.`
);
client.close();
return 1;
}
@@ -233,7 +230,6 @@ export default async function main(client) {
if (confirmation !== 'y' && confirmation !== 'yes') {
output.log('Aborted');
client.close();
return 1;
}
}
@@ -265,7 +261,6 @@ export default async function main(client) {
console.log(`${chalk.gray('-')} ${chalk.bold(project.name)}`);
});
client.close();
return 0;
}

View File

@@ -3,9 +3,10 @@ import chalk from 'chalk';
// Utilities
import Client from '../../util/client';
import listInput from '../../util/input/list';
import { emoji } from '../../util/emoji';
import getUser from '../../util/get-user';
import getTeams from '../../util/get-teams';
import listInput from '../../util/input/list';
import { Team, GlobalConfig } from '../../types';
import { writeToConfigFile } from '../../util/config/files';
@@ -31,7 +32,7 @@ export default async function main(client: Client, desiredSlug?: string) {
: teams.find(team => team.id === config.currentTeam);
if (!personalScopeSelected && !currentTeam) {
output.error(`You are not a part of the current team anymore.`);
output.error(`You are not a member of the current team anymore.`);
return 1;
}
@@ -41,30 +42,39 @@ export default async function main(client: Client, desiredSlug?: string) {
.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
})
.map(({ id, slug, name }) => {
let title = `${name} (${slug})`;
const selected = id === currentTeam?.id;
.map(team => {
let title = `${team.name} (${team.slug})`;
const selected = team.id === currentTeam?.id;
if (selected) {
title += ` ${chalk.bold('(current)')}`;
}
if (team.limited) {
title += ` ${emoji('locked')}`;
}
return {
name: title,
value: slug,
short: slug,
value: team.slug,
short: team.slug,
selected,
};
});
// Add the User scope entry at the top
const suffix = personalScopeSelected ? ` ${chalk.bold('(current)')}` : '';
let suffix = personalScopeSelected ? ` ${chalk.bold('(current)')}` : '';
// SAML tokens can not interact with the user scope
if (user.limited) {
suffix += ` ${emoji('locked')}`;
}
const choices = [
{ separator: 'Personal Account' },
{
name: `${user.email} (${user.username})${suffix}`,
value: user.email,
name: `${user.name || user.email} (${user.username})${suffix}`,
value: user.username,
short: user.username,
selected: personalScopeSelected,
},
@@ -93,9 +103,18 @@ export default async function main(client: Client, desiredSlug?: string) {
return 0;
}
if (user.limited) {
await client.reauthenticate({
scope: user.username,
teamId: null,
});
}
updateCurrentTeam(config);
output.success(`Your account (${chalk.bold(desiredSlug)}) is now active!`);
output.success(
`Your account (${chalk.bold(user.username)}) is now active!`
);
return 0;
}
@@ -114,6 +133,15 @@ export default async function main(client: Client, desiredSlug?: string) {
return 0;
}
if (newTeam.limited) {
const samlEnabled = newTeam.saml?.connection?.state === 'active';
await client.reauthenticate({
teamId: samlEnabled ? newTeam.id : null,
scope: newTeam.slug,
enforced: samlEnabled && newTeam.saml?.enforced === true,
});
}
updateCurrentTeam(config, newTeam);
output.success(

View File

@@ -1,3 +1,20 @@
export type Primitive =
| bigint
| boolean
| null
| number
| string
| symbol
| undefined;
export type JSONArray = JSONValue[];
export type JSONValue = Primitive | JSONObject | JSONArray;
export interface JSONObject {
[key: string]: JSONValue;
}
export interface AuthConfig {
token: string;
skipWrite?: boolean;
@@ -45,18 +62,26 @@ export type User = {
updatedAt: number;
};
name?: string;
limited?: boolean;
};
export type Team = {
export interface Team {
id: string;
avatar?: string;
avatar?: string | null;
billing: Billing;
created: string;
creatorId: string;
membership: { uid: string; role: 'MEMBER' | 'OWNER'; created: number };
name: string;
slug: string;
limited?: boolean;
saml?: {
enforced: boolean;
connection?: {
state: string;
};
};
}
export type Domain = {
id: string;
@@ -268,3 +293,13 @@ export type ProjectLinkResult =
| { status: 'linked'; org: Org; project: Project }
| { status: 'not_linked'; org: null; project: null }
| { status: 'error'; exitCode: number };
export interface Token {
id: string;
name: string;
type: string;
origin?: string;
activeAt: number;
createdAt: number;
teamId?: string;
}

View File

@@ -1,22 +1,28 @@
import { URLSearchParams } from 'url';
import { EventEmitter } from 'events';
import { URLSearchParams } from 'url';
import { parse as parseUrl } from 'url';
import fetch, { RequestInit, Response } from 'node-fetch';
import { VercelConfig } from '@vercel/client';
import retry, { RetryFunction, Options as RetryOptions } from 'async-retry';
import fetch, { BodyInit, Headers, RequestInit, Response } from 'node-fetch';
import ua from './ua';
import { Output } from './output/create-output';
import responseError from './response-error';
import ua from './ua';
import printIndications from './print-indications';
import { AuthConfig, GlobalConfig } from '../types';
import { VercelConfig } from './dev/types';
import doSsoLogin from './login/sso';
import reauthenticate from './login/reauthenticate';
import { SAMLError } from './login/types';
import { writeToAuthConfigFile } from './config/files';
import { AuthConfig, GlobalConfig, JSONObject } from '../types';
import { sharedPromise } from './promise';
import { APIError } from './errors-ts';
import { bold } from 'chalk';
export interface FetchOptions {
body?: NodeJS.ReadableStream | object | string;
headers?: { [key: string]: string };
const isSAMLError = (v: any): v is SAMLError => {
return v && v.saml;
};
export interface FetchOptions extends Omit<RequestInit, 'body'> {
body?: BodyInit | JSONObject;
json?: boolean;
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
retry?: RetryOptions;
useCurrentTeam?: boolean;
accountId?: string;
@@ -31,6 +37,10 @@ export interface ClientOptions {
localConfig: VercelConfig;
}
const isJSONObject = (v: any): v is JSONObject => {
return v && typeof v == 'object' && v.constructor === Object;
};
export default class Client extends EventEmitter {
argv: string[];
apiUrl: string;
@@ -47,7 +57,6 @@ export default class Client extends EventEmitter {
this.output = opts.output;
this.config = opts.config;
this.localConfig = opts.localConfig;
this._onRetry = this._onRetry.bind(this);
}
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
@@ -78,34 +87,30 @@ export default class Client extends EventEmitter {
}
_url = `${apiUrl}${parsedUrl.pathname}?${query}`;
delete opts.useCurrentTeam;
delete opts.accountId;
}
if (opts.json !== false && opts.body && typeof opts.body === 'object') {
Object.assign(opts, {
body: JSON.stringify(opts.body),
headers: Object.assign({}, opts.headers, {
'Content-Type': 'application/json',
}),
});
}
const headers = new Headers(opts.headers);
headers.set('authorization', `Bearer ${this.authConfig.token}`);
headers.set('user-agent', ua);
opts.headers = opts.headers || {};
opts.headers.Authorization = `Bearer ${this.authConfig.token}`;
opts.headers['user-agent'] = ua;
let body;
if (isJSONObject(opts.body)) {
body = JSON.stringify(opts.body);
headers.set('content-type', 'application/json; charset=utf8');
} else {
body = opts.body;
}
const url = `${apiUrl ? '' : this.apiUrl}${_url}`;
return this.output.time(
`${opts.method || 'GET'} ${url} ${JSON.stringify(opts.body) || ''}`,
fetch(url, opts as RequestInit)
fetch(url, { ...opts, headers, body })
);
}
fetch(url: string, opts: { json: false }): Promise<Response>;
fetch<T>(url: string, opts?: FetchOptions): Promise<T>;
async fetch<T>(url: string, opts: FetchOptions = {}): Promise<T> {
fetch(url: string, opts: FetchOptions = {}) {
return this.retry(async bail => {
const res = await this._fetch(url, opts);
@@ -114,19 +119,11 @@ export default class Client extends EventEmitter {
if (!res.ok) {
const error = await responseError(res);
if (error.saml && error.teamId) {
// If a SAML error is encountered then we re-trigger the SAML
// authentication flow for the team specified in the error.
const result = await doSsoLogin(error.teamId, this);
if (typeof result === 'number') {
this.output.prettyError(error);
process.exit(1);
return;
}
this.authConfig.token = result;
writeToAuthConfigFile(this.authConfig);
if (isSAMLError(error)) {
// A SAML error means the token is expired, or is not
// designated for the requested team, so the user needs
// to re-authenticate
await this.reauthenticate(error);
} else if (res.status >= 400 && res.status < 500) {
// Any other 4xx should bail without retrying
return bail(error);
@@ -140,19 +137,37 @@ export default class Client extends EventEmitter {
return res;
}
if (!res.headers.get('content-type')) {
const contentType = res.headers.get('content-type');
if (!contentType) {
return null;
}
return res.headers.get('content-type').includes('application/json')
? res.json()
: res;
return contentType.includes('application/json') ? res.json() : res;
}, opts.retry);
}
_onRetry(error: Error) {
this.output.debug(`Retrying: ${error}\n${error.stack}`);
reauthenticate = sharedPromise(async function (
this: Client,
error: SAMLError
) {
const result = await reauthenticate(this, error);
if (typeof result === 'number') {
if (error instanceof APIError) {
this.output.prettyError(error);
} else {
this.output.error(
`Failed to re-authenticate for ${bold(error.scope)} scope`
);
}
process.exit(1);
}
close() {}
this.authConfig.token = result;
writeToAuthConfigFile(this.authConfig);
});
_onRetry = (error: Error) => {
this.output.debug(`Retrying: ${error}\n${error.stack}`);
};
}

View File

@@ -39,7 +39,9 @@ export default async function getDeploymentsByProjectId(
query.set('from', options.from.toString());
}
const { deployments } = await client.fetch<Response>(`/v4/now/deployments?${query}`);
const { deployments } = await client.fetch<Response>(
`/v4/now/deployments?${query}`
);
total += deployments.length;
if (options.max && total >= options.max) {
@@ -49,15 +51,15 @@ export default async function getDeploymentsByProjectId(
if (options.continue && deployments.length === limit) {
const nextFrom = deployments[deployments.length - 1].created;
const nextOptions = Object.assign({}, options, { from: nextFrom });
deployments.push(...(await getDeploymentsByProjectId(client, projectId, nextOptions, total)));
deployments.push(
...(await getDeploymentsByProjectId(
client,
projectId,
nextOptions,
total
))
);
}
return deployments;
}
export async function getAllDeploymentsByProjectId(
client: Client,
projectId: string
) {
return getDeploymentsByProjectId(client, projectId, { from: null, limit: 100, continue: true });
}

View File

@@ -5,6 +5,7 @@ export const emojiLabels = {
link: '🔗',
inspect: '🔍',
success: '✅',
locked: '🔒',
} as const;
export type EmojiLabel = keyof typeof emojiLabels;

View File

@@ -6,15 +6,12 @@ export default async function confirm(
): Promise<boolean> {
require('./patch-inquirer');
const name = `${Date.now()}`;
const answers = await inquirer.prompt({
type: 'confirm',
name,
name: 'value',
message,
default: preferred,
});
const answer = answers[name] as boolean;
return answer;
return answers.value;
}

View File

@@ -9,5 +9,5 @@ export default function doBitbucketLogin(params: LoginParams) {
// cookie that the OAuth callback URL depends on
'https://vercel.com'
);
return doOauthLogin(url, 'Bitbucket', params);
return doOauthLogin(params, url, 'Bitbucket');
}

View File

@@ -7,8 +7,8 @@ import executeLogin from './login';
import { LoginParams } from './types';
export default async function doEmailLogin(
email: string,
params: LoginParams
params: LoginParams,
email: string
): Promise<number | string> {
let securityCode;
let verificationToken;

View File

@@ -9,5 +9,5 @@ export default function doGithubLogin(params: LoginParams) {
// cookie that the OAuth callback URL depends on
'https://vercel.com'
);
return doOauthLogin(url, 'GitHub', params);
return doOauthLogin(params, url, 'GitHub');
}

View File

@@ -6,5 +6,5 @@ export default function doGitlabLogin(params: LoginParams) {
// Can't use `apiUrl` here because this URL sets a
// cookie that the OAuth callback URL depends on
const url = new URL('/api/registration/gitlab/connect', 'https://vercel.com');
return doOauthLogin(url, 'GitLab', params);
return doOauthLogin(params, url, 'GitLab');
}

View File

@@ -10,9 +10,9 @@ import { getTitleName } from '../pkg-name';
import highlight from '../output/highlight';
export default async function doOauthLogin(
params: LoginParams,
url: URL,
provider: string,
params: LoginParams
provider: string
): Promise<number | string> {
const { output } = params;

View File

@@ -2,14 +2,17 @@ import inquirer from 'inquirer';
import error from '../output/error';
import listInput from '../input/list';
import { getCommandName } from '../pkg-name';
import { LoginParams } from './types';
import { LoginParams, SAMLError } from './types';
import doSsoLogin from './sso';
import doEmailLogin from './email';
import doGithubLogin from './github';
import doGitlabLogin from './gitlab';
import doBitbucketLogin from './bitbucket';
export default async function prompt(params: LoginParams) {
export default async function prompt(
params: LoginParams,
error?: Pick<SAMLError, 'teamId'>
) {
let result: number | string = 1;
const choices = [
@@ -17,11 +20,12 @@ export default async function prompt(params: LoginParams) {
{ name: 'Continue with GitLab', value: 'gitlab', short: 'gitlab' },
{ name: 'Continue with Bitbucket', value: 'bitbucket', short: 'bitbucket' },
{ name: 'Continue with Email', value: 'email', short: 'email' },
{ name: 'Continue with SAML Single Sign-On', value: 'saml', short: 'saml' },
{ name: 'Continue with SAML Single Sign-On', value: 'sso', short: 'sso' },
];
if (params.ssoUserId) {
// Remove SAML login option if we're connecting SAML Profile
if (params.ssoUserId || (error && !error.teamId)) {
// Remove SAML login option if we're connecting SAML Profile,
// or if this is a SAML error for a user / team without SAML
choices.pop();
}
@@ -38,10 +42,10 @@ export default async function prompt(params: LoginParams) {
result = await doBitbucketLogin(params);
} else if (choice === 'email') {
const email = await readInput('Enter your email address');
result = await doEmailLogin(email, params);
} else if (choice === 'saml') {
const slug = await readInput('Enter your Team slug');
result = await doSsoLogin(slug, params);
result = await doEmailLogin(params, email);
} else if (choice === 'sso') {
const slug = error?.teamId || (await readInput('Enter your Team slug'));
result = await doSsoLogin(params, slug);
}
return result;

View File

@@ -0,0 +1,28 @@
import { bold } from 'chalk';
import doSsoLogin from './sso';
import showLoginPrompt from './prompt';
import { LoginParams, SAMLError } from './types';
import confirm from '../input/confirm';
export default async function reauthenticate(
params: LoginParams,
error: Pick<SAMLError, 'enforced' | 'scope' | 'teamId'>
): Promise<string | number> {
let result: string | number = 1;
if (error.teamId && error.enforced) {
// If team has SAML enforced then trigger the SSO login directly
params.output.log(
`You must re-authenticate with SAML to use ${bold(error.scope)} scope.`
);
if (await confirm(`Log in with SAML?`, true)) {
result = await doSsoLogin(params, error.teamId);
}
} else {
// Personal account, or team that does not have SAML enforced
params.output.log(
`You must re-authenticate to use ${bold(error.scope)} scope.`
);
result = await showLoginPrompt(params, error);
}
return result;
}

View File

@@ -2,8 +2,8 @@ import { URL } from 'url';
import { LoginParams } from './types';
import doOauthLogin from './oauth';
export default function doSsoLogin(teamIdOrSlug: string, params: LoginParams) {
export default function doSsoLogin(params: LoginParams, teamIdOrSlug: string) {
const url = new URL('/auth/sso', params.apiUrl);
url.searchParams.append('teamId', teamIdOrSlug);
return doOauthLogin(url, 'SAML Single Sign-On', params);
return doOauthLogin(params, url, 'SAML Single Sign-On');
}

View File

@@ -10,3 +10,10 @@ export interface LoginData {
token: string;
securityCode: string;
}
export interface SAMLError {
saml?: true;
teamId: string | null;
scope: string;
enforced?: boolean;
}

View File

@@ -0,0 +1,24 @@
/**
* Wraps a function such that only one in-flight invocation is active at a time.
*
* That is, if the returned function is invoked more that one time before the
* promise returned from the initial invocation resolves, then the same promise
* is returned for subsequent invocations.
*
* Once the promise has resolved, the next invocation of the returned function
* will re-invoke the original function again.
*/
export function sharedPromise<P extends any[], V, T>(
fn: (this: T, ...args: P) => Promise<V>
) {
let promise: Promise<V> | null = null;
return function (this: T, ...args: P) {
if (!promise) {
promise = fn.apply(this, args);
promise.finally(() => {
promise = null;
});
}
return promise;
};
}

View File

@@ -7,9 +7,9 @@ import { parse } from 'url';
* google.com => google.com
*/
function toHost(url: string) {
function toHost(url: string): string {
if (/^https?:\/\//.test(url)) {
return parse(url).host;
return parse(url).host!;
}
// Remove any path if present

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "10.0.1-canary.5",
"version": "10.1.1-canary.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -40,7 +40,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.10.3-canary.4",
"@vercel/build-utils": "2.11.1-canary.0",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.3.3-canary.3",
"version": "0.4.1-canary.0",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [
@@ -20,7 +20,7 @@
"@types/js-yaml": "3.12.1",
"@types/node": "12.0.4",
"@types/node-fetch": "2.5.8",
"@vercel/routing-utils": "1.11.1-canary.1",
"@vercel/routing-utils": "1.11.2-canary.0",
"ajv": "6.12.2",
"jest": "24.9.0",
"ts-jest": "24.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "1.10.1-canary.2",
"version": "1.11.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/python",
"version": "2.0.2",
"version": "2.0.4-canary.0",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",

View File

@@ -82,14 +82,27 @@ elif 'app' in __vc_variables:
not inspect.iscoroutinefunction(__vc_module.app.__call__)
):
print('using Web Server Gateway Interface (WSGI)')
from io import BytesIO
from urllib.parse import urlparse
from werkzeug._compat import BytesIO
from werkzeug._compat import string_types
from werkzeug._compat import to_bytes
from werkzeug._compat import wsgi_encoding_dance
from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response
string_types = (str,)
def to_bytes(x, charset=sys.getdefaultencoding(), errors="strict"):
if x is None:
return None
if isinstance(x, (bytes, bytearray, memoryview)):
return bytes(x)
if isinstance(x, str):
return x.encode(charset, errors)
raise TypeError("Expected bytes")
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
if isinstance(s, str):
s = s.encode(charset)
return s.decode("latin1", errors)
def vc_handler(event, context):
payload = json.loads(event['body'])

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/routing-utils",
"version": "1.11.1-canary.1",
"version": "1.11.2-canary.0",
"description": "Vercel routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@@ -39,7 +39,7 @@ function getCheckAndContinue(
);
} else if (route.check) {
checks.push(route);
} else if (route.continue) {
} else if (route.continue && !route.override) {
continues.push(route);
} else {
others.push(route);

View File

@@ -104,6 +104,9 @@ export const routesSchema = {
continue: {
type: 'boolean',
},
override: {
type: 'boolean',
},
check: {
type: 'boolean',
},

View File

@@ -187,6 +187,10 @@ function collectHasSegments(has?: HasField) {
const hasSegments = new Set<string>();
for (const hasItem of has || []) {
if ('key' in hasItem && hasItem.type === 'header') {
hasItem.key = hasItem.key.toLowerCase();
}
if (!hasItem.value && 'key' in hasItem) {
hasSegments.add(hasItem.key);
}

View File

@@ -27,6 +27,7 @@ export type Source = {
headers?: { [name: string]: string };
methods?: string[];
continue?: boolean;
override?: boolean;
check?: boolean;
important?: boolean;
status?: number;

View File

@@ -419,3 +419,50 @@ test('mergeRoutes ensure `handle: error` comes last', () => {
];
deepStrictEqual(actual, expected);
});
test('mergeRoutes ensure beforeFiles comes after redirects', () => {
const userRoutes = [];
const builds = [
{
use: '@vercel/next',
entrypoint: 'package.json',
routes: [
{
src: '^/home$',
status: 301,
headers: {
Location: '/',
},
},
{
src: '^/hello$',
dest: '/somewhere',
continue: true,
override: true,
},
{
handle: 'filesystem',
},
{
src: '^/404$',
dest: '/404',
status: 404,
check: true,
},
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ src: '^/home$', status: 301, headers: { Location: '/' } },
{
src: '^/hello$',
dest: '/somewhere',
continue: true,
override: true,
},
{ handle: 'filesystem' },
{ src: '^/404$', dest: '/404', status: 404, check: true },
];
deepStrictEqual(actual, expected);
});

View File

@@ -247,7 +247,7 @@ test('convertRedirects', () => {
},
{
type: 'header',
key: 'x-pathname',
key: 'X-Pathname',
value: '(?<another>hello|world)',
},
],
@@ -549,7 +549,7 @@ test('convertRewrites', () => {
},
{
type: 'header',
key: 'x-pathname',
key: 'X-Pathname',
value: '(?<another>hello|world)',
},
],
@@ -893,7 +893,7 @@ test('convertHeaders', () => {
},
{
type: 'header',
key: 'x-pathname',
key: 'X-Pathname',
value: '(?<another>hello|world)',
},
],

View File

@@ -1860,12 +1860,13 @@
dependencies:
"@types/node" "*"
"@types/node-fetch@2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.1.4.tgz#093d1beae11541aef25999d70aa09286fd025b1a"
integrity sha512-tR1ekaXUGpmzOcDXWU9BW73YfA2/VW1DF1FH+wlJ82BbCSnWTbdX+JkqWQXWKIGsFPnPsYadbXfNgz28g+ccWg==
"@types/node-fetch@2.5.10":
version "2.5.10"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132"
integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node-fetch@2.5.4":
version "2.5.4"
@@ -2294,11 +2295,6 @@ agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0:
dependencies:
es6-promisify "^5.0.0"
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
agent-base@6:
version "6.0.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a"
@@ -3479,15 +3475,15 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codecov@3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.7.1.tgz#434cb8d55f18ef01672e5739d3d266696bebc202"
integrity sha512-JHWxyPTkMLLJn9SmKJnwAnvY09kg2Os2+Ux+GG7LwZ9g8gzDDISpIN5wAsH1UBaafA/yGcd3KofMaorE8qd6Lw==
codecov@3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.8.2.tgz#ab24f18783998c39e809ea210af899f8dbcc790e"
integrity sha512-6w/kt/xvmPsWMfDFPE/T054txA9RTgcJEw36PNa6MYX+YV29jCHCRFXwbQ3QZBTOgnex1J2WP8bo2AT8TWWz9g==
dependencies:
argv "0.0.2"
ignore-walk "3.0.3"
js-yaml "3.13.1"
teeny-request "6.0.1"
js-yaml "3.14.1"
teeny-request "7.0.1"
urlgrey "0.4.4"
collection-visit@^1.0.0:
@@ -4474,6 +4470,11 @@ es6-promisify@^5.0.0:
dependencies:
es6-promise "^4.0.3"
esbuild@^0.11.20:
version "0.11.23"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.23.tgz#c42534f632e165120671d64db67883634333b4b8"
integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q==
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
@@ -5736,14 +5737,6 @@ https-proxy-agent@^2.2.3:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
dependencies:
agent-base "5"
debug "4"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
@@ -6830,6 +6823,14 @@ js-yaml@3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.10.0, js-yaml@^3.13.1:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
@@ -7929,7 +7930,7 @@ node-fetch@2.6.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-fetch@2.6.1, node-fetch@^2.2.0, node-fetch@^2.2.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.1:
node-fetch@2.6.1, node-fetch@^2.2.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -9858,7 +9859,7 @@ source-map-support@0.5.12:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map-support@^0.5.12, source-map-support@^0.5.17, source-map-support@^0.5.6:
source-map-support@^0.5.12, source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -10384,16 +10385,16 @@ tar@^6.1.0:
mkdirp "^1.0.3"
yallist "^4.0.0"
teeny-request@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-6.0.1.tgz#9b1f512cef152945827ba7e34f62523a4ce2c5b0"
integrity sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==
teeny-request@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c"
integrity sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==
dependencies:
http-proxy-agent "^4.0.0"
https-proxy-agent "^4.0.0"
node-fetch "^2.2.0"
https-proxy-agent "^5.0.0"
node-fetch "^2.6.1"
stream-events "^1.0.5"
uuid "^3.3.2"
uuid "^8.0.0"
temp-dir@^1.0.0:
version "1.0.0"
@@ -10637,6 +10638,14 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
ts-eager@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ts-eager/-/ts-eager-2.0.2.tgz#2c1a4d37529effa321f3438793650a84028a87d5"
integrity sha512-xzFPL2z7mgLs0brZXaIHTm91Pjl/Cuu9AMKprgSuK+kIS2LjiG8fqqg4eqz3tgBy9OIdupb9w55pr7ea3JBB+Q==
dependencies:
esbuild "^0.11.20"
source-map-support "^0.5.19"
ts-jest@24.1.0:
version "24.1.0"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.1.0.tgz#2eaa813271a2987b7e6c3fefbda196301c131734"
@@ -10992,6 +11001,11 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"