├── .babelrc ├── .dockerignore ├── .eslintignore ├── .eslintrc.yml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .nycrc ├── .serverless_plugins ├── codebox-tools │ └── index.js ├── content-handling │ └── index.js ├── environment-variables │ └── index.js ├── remove-storage │ └── index.js └── set-api-host │ └── index.js ├── LICENSE ├── README.md ├── bin └── integration-test ├── bootstrap.js ├── circle.yml ├── integration ├── Dockerfile-latest ├── Dockerfile-lts ├── bin │ ├── deploy │ ├── remove │ └── test ├── helpers.js ├── npm.test.js └── test-package │ ├── index.js │ └── package.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src ├── adapters │ ├── logger.js │ ├── npm.js │ ├── package.js │ └── s3.js ├── authorizers │ └── github.js ├── contextFactory.js ├── dist-tags │ ├── delete.js │ ├── get.js │ └── put.js ├── get │ ├── index.js │ └── lib.js ├── put │ ├── deprecate.js │ ├── index.js │ └── publish.js ├── tar │ └── get.js ├── user │ ├── delete.js │ └── put.js └── whoami │ └── get.js ├── test ├── adapters │ ├── logger.test.js │ ├── npm.test.js │ └── s3.test.js ├── authorizers │ └── github.test.js ├── dist-tags │ ├── delete.test.js │ ├── get.test.js │ └── put.test.js ├── fixtures │ └── package.js ├── get │ └── lib.test.js ├── globals.js ├── mocha.opts ├── put │ ├── deprecate.test.js │ └── publish.test.js ├── serverless_plugins │ ├── codebox-tools │ │ └── index.test.js │ └── remove-storage │ │ └── index.test.js ├── tar │ └── get.test.js ├── user │ ├── delete.test.js │ └── put.test.js └── whoami │ └── get.test.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [ 6 | "rewire" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | plugins: 3 | - import 4 | env: 5 | mocha: true 6 | globals: 7 | assert: true 8 | stub: true 9 | spy: true 10 | createStubInstance: true 11 | useFakeTimers: true 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome, and thanks in advance for your help! 4 | 5 | ## When you want to propose a new feature or bug fix 6 | * Please make sure there is an open issue discussing your 7 | contribution. 8 | * If there isn't, please open an issue so we can talk about it before you invest 9 | time into the implementation. 10 | * When creating an issue follow the guide that Github shows so we have enough 11 | information about your proposal. 12 | 13 | ## Pull Requests 14 | Please follow these Pull Request guidelines when creating pull requests: 15 | * If an issue exists, leave a comment there that you are working on a solution 16 | so nobody else jumps on it. 17 | * If an issue does not exist, create a new Issue, detail your changes. We 18 | recommend waiting until we accept it, so you don't waste your precious time. 19 | * Follow our **Testing** and **Code Style** guidelines below. 20 | * Start commit messages with a uppercase verb such as "Add", "Fix", "Refactor", 21 | "Remove". 22 | 23 | ## Issues 24 | Please follow these issue guidelines for opening issues: 25 | * Make sure your issue is not a duplicate. 26 | * Make sure your issue is for a *feature*, *bug*, or *discussion*, use the 27 | labels provided in Github where applicable. 28 | 29 | ## Code Style 30 | We aim for clean, consistent code style. We're using ESlint to check for 31 | codestyle issues. If ESlint issues are found our build will fail and we can't 32 | merge the PR. 33 | 34 | Please follow these Code Style guidelines when writing your unit tests: 35 | * In the root of our repo, use this command to check for styling issues: `npm 36 | run lint` 37 | 38 | ## Testing 39 | We strive for 100% test coverage, so make sure your tests cover as much of your 40 | code as possible. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # This is a (Bug Report / Feature Proposal) 2 | 5 | 6 | ## Description 7 | 8 | For bug reports: 9 | * What went wrong? 10 | * What did you expect should have happened? 11 | * What was the config / env variables you used? 12 | * What stacktrace or error message did you 13 | experience? 14 | 15 | For feature proposals: 16 | * What is the use case that should be solved. The 17 | more detail you describe this in the easier it is 18 | to understand for us. 19 | * If there is additional config how would it look 20 | 21 | Similar or dependent issues: 22 | * #12345 23 | 24 | ## Additional Data 25 | 26 | * ***NPM CLI version you are using***: 27 | * ***Serverless version you're using***: 28 | * ***Node version you're using***: 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ## What did you implement: 11 | 12 | Closes #12345 (Github) 13 | 14 | 17 | 18 | ## How did you implement it: 19 | 20 | 25 | 26 | ## How can we verify it: 27 | 28 | 36 | 37 | ## Todos: 38 | 39 | 43 | 44 | - [ ] Write tests 45 | - [ ] Write documentation 46 | - [ ] Fix linting errors 47 | - [ ] Tag `ready for review` or `wip` 48 | 49 | ***Is this a breaking change?:*** NO/YES 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log* 4 | coverage 5 | .webpack 6 | .serverless 7 | *.env* 8 | .nyc_output 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text-summary" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.serverless_plugins/codebox-tools/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | class CodeboxTools { 4 | constructor(serverless, options) { 5 | this.options = options; 6 | this.serverless = serverless; 7 | this.provider = this.serverless.getProvider('aws'); 8 | this.s3 = new this.provider.sdk.S3({ 9 | signatureVersion: 'v4', 10 | }); 11 | 12 | this.bucket = this.serverless.service.resources 13 | .Resources 14 | .PackageStorage 15 | .Properties 16 | .BucketName; 17 | 18 | this.commands = { 19 | codebox: { 20 | usage: 'Useful tools provided by Codebox', 21 | commands: { 22 | domain: { 23 | usage: 'Update packages and migrate to a new domain.', 24 | lifecycleEvents: [ 25 | 'migrate', 26 | ], 27 | options: { 28 | host: { 29 | usage: 'New host only e.g. example.com', 30 | shortcut: 'h', 31 | required: true, 32 | }, 33 | }, 34 | }, 35 | encrypt: { 36 | usage: 'Re-encrypts all files within package storage.', 37 | lifecycleEvents: [ 38 | 'encrypt', 39 | ], 40 | }, 41 | index: { 42 | usage: 'Re-indexes all packages into Codebox Insights, license required.', 43 | lifecycleEvents: [ 44 | 'index', 45 | ], 46 | options: { 47 | clientId: { 48 | usage: 'Client id for your account.', 49 | shortcut: 'c', 50 | required: true, 51 | }, 52 | secret: { 53 | usage: 'Secret for your account.', 54 | shortcut: 's', 55 | required: true, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }; 62 | 63 | this.hooks = { 64 | 'codebox:domain:migrate': () => this.migrate(), 65 | 'codebox:index:index': () => this.index(), 66 | 'codebox:encrypt:encrypt': () => this.encrypt(), 67 | }; 68 | } 69 | 70 | _getObjects(token) { 71 | return this.s3.listObjectsV2({ 72 | Bucket: this.bucket, 73 | ContinuationToken: token, 74 | }) 75 | .promise() 76 | .then((data) => { 77 | const objectPromises = []; 78 | 79 | data.Contents.forEach((item) => { 80 | objectPromises.push( 81 | new Promise((resolve, reject) => { 82 | this.s3.getObject({ 83 | Bucket: this.bucket, 84 | Key: item.Key, 85 | }).promise().then((obj) => { 86 | resolve({ 87 | key: item.Key, 88 | data: obj.Body, 89 | }); 90 | }).catch(reject); 91 | })); 92 | }); 93 | 94 | if (data.IsTruncated) { 95 | return this._getObjectPromises(data.NextContinuationToken); 96 | } 97 | 98 | return Promise.all(objectPromises); 99 | }); 100 | } 101 | 102 | encrypt() { 103 | return this._getObjects() 104 | .then((items) => { 105 | const putPromises = []; 106 | 107 | items.forEach((item) => { 108 | putPromises.push( 109 | this.s3.putObject({ 110 | Bucket: this.bucket, 111 | Key: item.key, 112 | Body: item.data, 113 | ServerSideEncryption: 'AES256', 114 | }).promise()); 115 | }); 116 | 117 | return putPromises; 118 | }).then((promises) => { 119 | return Promise.all(promises) 120 | .then(() => this.serverless.cli.log('Encrypted all current files for registry')) 121 | }) 122 | .catch(err => { 123 | this.serverless.cli.log(`Failed file encryption migration ${err.message}`) 124 | }); 125 | } 126 | 127 | index() { 128 | return this._getObjects() 129 | .then((items) => { 130 | const fetchPromises = []; 131 | 132 | items.forEach((item) => { 133 | if (item.key.indexOf('index.json') === -1) { 134 | return; 135 | } 136 | 137 | const json = JSON.parse(item.data.toString()); 138 | 139 | const version = json.versions[ 140 | json['dist-tags'].latest 141 | ]; 142 | 143 | const logBody = { 144 | name: version.name, 145 | description: version.description, 146 | version: version.version, 147 | keywords: version.keywords, 148 | license: version.license, 149 | contributors: version.contributors, 150 | dependencies: version.dependencies, 151 | homepage: version.homepage, 152 | repository: version.repository, 153 | 'dist-tags': json['dist-tags'], 154 | }; 155 | 156 | const reqBody = JSON.stringify({ 157 | timestamp: new Date(), 158 | namespace: 'info:package:put', 159 | level: 'info', 160 | user: { 161 | name: "Codebox", 162 | avatar: "https://s3-eu-west-1.amazonaws.com/codebox-assets/logo.png", 163 | }, 164 | credentials: { 165 | clientId: this.options.clientId, 166 | secret: this.options.secret, 167 | }, 168 | body: logBody, 169 | }); 170 | 171 | fetchPromises.push(fetch('https://log.codebox.sh/v1/send', { 172 | method: 'POST', 173 | headers: { 174 | 'Content-Type': 'application/json', 175 | }, 176 | body: reqBody, 177 | })); 178 | }); 179 | 180 | return Promise.all(fetchPromises); 181 | }) 182 | .then((results) => { 183 | const failed = results.filter(r => r.status !== 200); 184 | 185 | if (failed.length > 0) { 186 | this.serverless.cli.log(`Codebox indexing had ${failed.length} failures`); 187 | } 188 | }) 189 | .then(() => { 190 | this.serverless.cli.log('Codebox Insights indexing tool completed'); 191 | }) 192 | .catch((err) => { 193 | this.serverless.cli.log(`Codebox Insights indexing of data failed for ${this.options.clientId}`); 194 | this.serverless.cli.log(err.message); 195 | }); 196 | } 197 | 198 | migrate() { 199 | return this._getObjects() 200 | .then((items) => { 201 | const putPromises = []; 202 | 203 | items.forEach((item) => { 204 | if (item.key.indexOf('index.json') === -1) { 205 | return; 206 | } 207 | 208 | const newItem = Object.assign({}, item); 209 | const json = JSON.parse(newItem.data.toString()); 210 | 211 | Object.keys(json.versions).forEach((name) => { 212 | const version = json.versions[name]; 213 | 214 | if (version.dist && version.dist.tarball) { 215 | const currentHost = version.dist.tarball.split('/')[2]; 216 | const currentProtocol = version.dist.tarball.split('/')[0]; 217 | 218 | version.dist.tarball = version.dist.tarball 219 | .replace(currentHost, this.options.host) 220 | .replace(currentProtocol, 'https:'); 221 | 222 | json.versions[name] = version; 223 | } 224 | }); 225 | 226 | putPromises.push( 227 | this.s3.putObject({ 228 | Bucket: this.bucket, 229 | Key: newItem.key, 230 | Body: JSON.stringify(json), 231 | }).promise()); 232 | }); 233 | 234 | return Promise.all(putPromises); 235 | }) 236 | .then(() => { 237 | const lambda = new this.provider.sdk.Lambda({ 238 | signatureVersion: 'v4', 239 | region: process.env.CODEBOX_REGION, 240 | }); 241 | 242 | const serviceName = this.serverless.config.serverless.service.service; 243 | const stage = this.options.stage; 244 | 245 | const deployedName = `${serviceName}-${stage}-put`; 246 | 247 | const params = { 248 | FunctionName: deployedName, 249 | }; 250 | 251 | return lambda 252 | .getFunctionConfiguration(params) 253 | .promise() 254 | .then((config) => { 255 | const env = config.Environment; 256 | const currentEndpoint = env.Variables.apiEndpoint; 257 | 258 | if (!currentEndpoint) { 259 | throw new Error('Please ensure you are on Codebox npm 0.20.0 or higher.'); 260 | } 261 | 262 | let endpoint = currentEndpoint.replace(currentEndpoint.split('/')[2], this.options.host); 263 | if (this.options.path) { 264 | endpoint = `${endpoint}${this.options.path}` 265 | } 266 | 267 | env.Variables = Object.assign({}, env.Variables, { 268 | apiEndpoint: endpoint, 269 | }); 270 | 271 | const updatedConfig = { 272 | FunctionName: deployedName, 273 | Environment: env, 274 | }; 275 | 276 | return lambda 277 | .updateFunctionConfiguration(updatedConfig) 278 | .promise(); 279 | }); 280 | }) 281 | .then(() => { 282 | this.serverless.cli.log(`Domain updated for ${this.options.host}`); 283 | }) 284 | .catch((err) => { 285 | this.serverless.cli.log(`Domain update failed for ${this.options.host}`); 286 | this.serverless.cli.log(err.message); 287 | }); 288 | } 289 | } 290 | 291 | module.exports = CodeboxTools; 292 | -------------------------------------------------------------------------------- /.serverless_plugins/content-handling/index.js: -------------------------------------------------------------------------------- 1 | class ContentHandling { 2 | constructor(serverless, options) { 3 | this.serverless = serverless; 4 | this.options = options; 5 | 6 | this.provider = this.serverless.getProvider('aws'); 7 | 8 | this.hooks = { 9 | 'after:deploy:deploy': this.afterDeploy.bind(this), 10 | }; 11 | } 12 | 13 | afterDeploy() { 14 | const apiName = this.provider.naming.getApiGatewayName(); 15 | const funcs = this.serverless.service.functions; 16 | const apigateway = new this.provider.sdk.APIGateway({ 17 | region: this.options.region, 18 | }); 19 | 20 | const integrationResponse = { 21 | statusCode: '200', 22 | }; 23 | 24 | apigateway 25 | .getRestApis() 26 | .promise() 27 | .then((apis) => { 28 | integrationResponse.restApiId = apis.items.find(api => api.name === apiName).id; 29 | 30 | return apigateway 31 | .getResources({ restApiId: integrationResponse.restApiId }) 32 | .promise(); 33 | }) 34 | .then((resources) => { 35 | const integrationPromises = []; 36 | 37 | Object.keys(funcs).forEach((fKey) => { 38 | funcs[fKey].events.forEach((e) => { 39 | if (e.http && e.http.contentHandling) { 40 | integrationResponse.httpMethod = e.http.method.toUpperCase(); 41 | integrationResponse.contentHandling = e.http.contentHandling; 42 | integrationResponse.resourceId = resources.items.find(r => r.path === `/${e.http.path}`).id; 43 | 44 | integrationPromises 45 | .push(apigateway.putIntegrationResponse(integrationResponse).promise()); 46 | } 47 | }); 48 | }); 49 | 50 | this.serverless.cli.log('Setting up content handling in AWS API Gateway (takes ~1 min)...'); 51 | return Promise.all(integrationPromises); 52 | }) 53 | // AWS Limit createDeployment: 3 requests per minute per account 54 | // 'Too Many Requests', error may occur as serverless calls this endpoint also. 55 | // Wait 1 minute to get reliable deployment. 56 | // http://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html 57 | .then(() => new Promise(resolve => setTimeout(() => resolve(), 60000))) 58 | .then(() => { 59 | this.serverless.cli.log('Deploying content handling updates to AWS API Gateway...'); 60 | return apigateway.createDeployment({ 61 | stageName: this.options.stage, 62 | restApiId: integrationResponse.restApiId, 63 | }).promise(); 64 | }) 65 | .then((result) => { 66 | if (result.id) { 67 | this.serverless.cli.log('AWS API Gateway Deployed'); 68 | } 69 | }) 70 | .catch(err => this.serverless.cli.log(err.message)); 71 | } 72 | } 73 | 74 | module.exports = ContentHandling; 75 | -------------------------------------------------------------------------------- /.serverless_plugins/environment-variables/index.js: -------------------------------------------------------------------------------- 1 | class EnvironmentVariablesCheck { 2 | constructor(serverless, options) { 3 | this.serverless = serverless; 4 | this.options = options; 5 | 6 | this.requiredVars = [ 7 | { 8 | name: 'CODEBOX_REGION', 9 | helpText: 'the AWS region you wish to deploy to.', 10 | }, 11 | { 12 | name: 'CODEBOX_ADMINS', 13 | helpText: 'a comma seperated list of GitHub usernames.', 14 | }, 15 | { 16 | name: 'CODEBOX_REGISTRY', 17 | helpText: 'the npm registry to proxy through to e.g. https://registry.npmjs.org/.', 18 | }, 19 | { 20 | name: 'CODEBOX_BUCKET', 21 | helpText: 'the S3 bucket use for storage of your private packages.', 22 | }, 23 | { 24 | name: 'CODEBOX_GITHUB_URL', 25 | helpText: 'the GitHub / GitHub Enterprise API url, ensure this the root API url and not the website.', 26 | }, 27 | { 28 | name: 'CODEBOX_GITHUB_CLIENT_ID', 29 | helpText: 'the client id for your GitHub / GitHub Enterprise OAuth application.', 30 | }, 31 | { 32 | name: 'CODEBOX_GITHUB_SECRET', 33 | helpText: 'the secret for your GitHub / GitHub Enterprise OAuth application.', 34 | }, 35 | ]; 36 | 37 | this.hooks = { 38 | 'before:deploy:initialize': this.beforeDeploy.bind(this), 39 | }; 40 | } 41 | 42 | beforeDeploy() { 43 | let hasErrors = false; 44 | 45 | this.requiredVars.forEach((v) => { 46 | if (!process.env[v.name]) { 47 | hasErrors = true; 48 | this.serverless.cli.log(`Missing ${v.name} ${v.helpText}`); 49 | } 50 | }); 51 | 52 | if (hasErrors) { 53 | throw new Error('Required environment variables missing, please see details above.'); 54 | } 55 | } 56 | } 57 | 58 | module.exports = EnvironmentVariablesCheck; 59 | -------------------------------------------------------------------------------- /.serverless_plugins/remove-storage/index.js: -------------------------------------------------------------------------------- 1 | class RemoveStorageBucket { 2 | constructor(serverless) { 3 | this.serverless = serverless; 4 | this.provider = this.serverless.getProvider('aws'); 5 | 6 | const profile = this.serverless 7 | .config 8 | .serverless 9 | .service 10 | .provider 11 | .profile; 12 | 13 | if (profile) { 14 | const credentials = new this.provider.sdk.SharedIniFileCredentials({ 15 | profile, 16 | }); 17 | 18 | this.provider.sdk.config.credentials = credentials; 19 | } 20 | 21 | this.s3 = new this.provider.sdk.S3({ 22 | signatureVersion: 'v4', 23 | }); 24 | 25 | this.bucket = this.serverless.service.resources 26 | .Resources 27 | .PackageStorage 28 | .Properties 29 | .BucketName; 30 | 31 | this.hooks = { 32 | 'before:remove:remove': this.beforeRemove.bind(this), 33 | }; 34 | } 35 | 36 | listAllKeys(token) { 37 | const allKeys = []; 38 | return this.s3.listObjectsV2({ 39 | Bucket: this.bucket, 40 | ContinuationToken: token, 41 | }) 42 | .promise() 43 | .then((data) => { 44 | allKeys.push(data.Contents); 45 | 46 | if (data.IsTruncated) { 47 | return this.listAllKeys(data.NextContinuationToken); 48 | } 49 | 50 | return [].concat(...allKeys).map(({ Key }) => ({ Key })); 51 | }); 52 | } 53 | 54 | beforeRemove() { 55 | return new Promise((resolve, reject) => { 56 | return this.listAllKeys() 57 | .then((keys) => { 58 | if (keys.length > 0) { 59 | return this.s3 60 | .deleteObjects({ 61 | Bucket: this.bucket, 62 | Delete: { 63 | Objects: keys, 64 | }, 65 | }).promise(); 66 | } 67 | 68 | return true; 69 | }) 70 | .then(() => { 71 | return this.s3 72 | .deleteBucket({ 73 | Bucket: this.bucket, 74 | }).promise() 75 | .then(() => { 76 | this.serverless.cli.log('AWS Package Storage Removed'); 77 | resolve(); 78 | }); 79 | }) 80 | .catch((err) => { 81 | this.serverless.cli.log(`Could not remove AWS package storage: ${err.message}`); 82 | reject(err); 83 | }); 84 | }); 85 | } 86 | } 87 | 88 | module.exports = RemoveStorageBucket; 89 | -------------------------------------------------------------------------------- /.serverless_plugins/set-api-host/index.js: -------------------------------------------------------------------------------- 1 | class SetAPIHost { 2 | constructor(serverless, options) { 3 | this.options = options; 4 | this.serverless = serverless; 5 | this.provider = this.serverless.getProvider('aws'); 6 | 7 | this.awsInfo = this.serverless 8 | .pluginManager 9 | .plugins 10 | .find(p => p.constructor.name === 'AwsInfo'); 11 | 12 | this.registry = this.serverless 13 | .service 14 | .provider 15 | .environment 16 | .registry; 17 | 18 | this.hooks = { 19 | 'after:deploy:deploy': this.afterDeploy.bind(this), 20 | }; 21 | } 22 | 23 | afterDeploy() { 24 | const lambda = new this.provider.sdk.Lambda({ 25 | signatureVersion: 'v4', 26 | region: this.options.region, 27 | }); 28 | 29 | const publishFunction = this 30 | .awsInfo 31 | .gatheredData 32 | .info 33 | .functions 34 | .find(f => f.name === 'put'); 35 | 36 | const params = { 37 | FunctionName: publishFunction.deployedName, 38 | }; 39 | 40 | lambda 41 | .getFunctionConfiguration(params) 42 | .promise() 43 | .then((config) => { 44 | const env = config.Environment; 45 | 46 | if (env.Variables.apiEndpoint) { 47 | // Already set / not a first time deployment. 48 | return; 49 | } 50 | 51 | env.Variables = Object.assign({}, env.Variables, { 52 | apiEndpoint: `${this.awsInfo.gatheredData.info.endpoint}/registry`, 53 | }); 54 | 55 | const updatedConfig = { 56 | FunctionName: publishFunction.deployedName, 57 | Environment: env, 58 | }; 59 | 60 | return lambda 61 | .updateFunctionConfiguration(updatedConfig) 62 | .promise(); 63 | }) 64 | .catch((err) => { 65 | this.serverless.cli.log(err.message); 66 | }); 67 | } 68 | } 69 | 70 | module.exports = SetAPIHost; 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Craftship Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 2 | [![CircleCI Status](https://circleci.com/gh/craftship/codebox-npm.svg?style=shield)](https://circleci.com/gh/craftship/codebox-npm) 3 | [![Coverage Status](https://coveralls.io/repos/github/craftship/codebox-npm/badge.svg?branch=master&cb=1)](https://coveralls.io/github/craftship/codebox-npm?branch=master) 4 | 5 | 6 | 7 | ## Overview 8 | Codebox npm is a serverless npm registry to allow companies that wish to keep their intellectual property. It allows sharing of npm modules within a company but additionally allows access to all of the modules on public npm. One other major difference is that it replaces `npm login` authentication to be via github / github enterprise. Users are always required to be authenticated when using codebox as their npm registry. 9 | 10 | It is currently compatible with the latest version of the npm & yarn cli. 11 | 12 | ## Local Deployment 13 | 14 | The quickest way to deploy your own npm registry from your local machine is to follow the following guide. 15 | 16 | ### Prerequisites 17 | * A GitHub / GitHub Enterprise application is registered (e.g. [for GitHub](https://github.com/settings/developers)), you will need the `Client ID` and `Secret`. 18 | * You have `AWS` environment credentials setup with enough access to deploy Serverless resources on your local machine, you can follow the standard guide from Amazon [here](http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html). 19 | * Latest version of Serverless installed globally (`npm install serverless -g` or `yarn global add serverless`). 20 | 21 | #### Steps 22 | * `serverless install --url https://github.com/craftship/codebox-npm/tree/0.21.2 --name my-npm-registry` - pick whichever name you prefer for your registry 23 | * `cd my-npm-registry` 24 | * `npm install` 25 | * Setup your environment variables: 26 | ``` 27 | export CODEBOX_REGION="eu-west-1" # Set the AWS region you wish your registry to be deployed to 28 | export CODEBOX_ADMINS="" # Comma seperated list of github usernames (e.g. "jon,kadi"), these users will be the only ones able to publish 29 | export CODEBOX_REGISTRY="https://registry.npmjs.org/" # The NPM mirror you wish to proxy through to 30 | export CODEBOX_BUCKET="my-npm-registry-storage" # The name of the bucket in which you wish to store your packages 31 | export CODEBOX_GITHUB_URL="https://api.github.com/" # The GitHub / GitHub Enterprise **api** url 32 | export CODEBOX_GITHUB_CLIENT_ID="client_id" # The client id for your GitHub application 33 | export CODEBOX_GITHUB_SECRET="secret" # The secret for your GitHub application 34 | export CODEBOX_RESTRICTED_ORGS="" # OPTIONAL: Comma seperated list of github organisations to only allow access to users in that org (e.g. "craftship,myorg"). Useful if using public GitHub for authentication, as by default all authenticated users would have access. 35 | ``` 36 | * `serverless deploy --stage prod` (pick which ever stage you wish) 37 | * `npm set registry ` - `` being the base url shown in the terminal after deployment completes, such as: 38 | `https://abcd12345.execute-api.eu-west-1.amazonaws.com/dev/registry/` 39 | 40 | ## Using it in your Repositories 41 | The easiest way to ensure developers are using the correct private registry url is to setup a `.npmrc` file. This contains default settings that npm will pick up on and will ensure the registry is set per repository. 42 | 43 | This is especially great for repositories you wish developers to allow publishing and keep private. Here is an example `.npmrc` file: 44 | 45 | 46 | ``` 47 | registry=https://ab1cd3ef4.execute-api.eu-west-1.amazonaws.com/prod/registry 48 | always-auth=true 49 | ``` 50 | 51 | If a user is doing any `npm` operation for the first time in the repository then they will need to `npm login`. `always-auth=true` allows yarn to be supported in your project. 52 | 53 | ## `npm login` Usage 54 | Once you are using the private registry you are required to always be authenticated with npm. This ensures not just anyone can request private packages that are not to be shared with the outside world. 55 | 56 | To login you can use the `npm login` cli command, if you have 2FA enabled you will need to (when prompted) enter the username in the format of your GitHub username.otp e.g. `jonsharratt.123456`. Once logged in it will store a long life token that will be used going forward. 57 | 58 | You are now able to use npm commands as normal. 59 | 60 | ## `yarn login` Usage 61 | The best way to setup yarn authentication is to do an initial `npm login` so it can support a 2FA login if you have it enabled. 62 | 63 | Once done ensure you have a project based `.npmrc` config setup a per the "Using it in your Repositories" guide above. The `always-auth=true` option ensures yarn will work with your `codebox-npm` registry. 64 | 65 | Yarn does not require an explicit `yarn login` as in this scenario it uses your `.npmrc` config instead. 66 | 67 | ## Admins / Publishing Packages 68 | `npm publish` works as it normally does via the npm CLI. By default all users that authenticate have read only access. If you wish to allow publish rights then you need to set the `CODEBOX_ADMINS` environment variable to a comma separated list of GitHub usernames such as `jonsharratt,kadikraman` and re-deploy. 69 | 70 | ## Setup with your CI 71 | We recommend creating a GitHub user that can represent your team as a service account. Once created you can then use that account to `npm login` to the private registry. 72 | 73 | You then need to get the generated token and login url (note the login url is not the same as the registry url). Do this by running `cat ~/.npmrc`. As an example you should see an entry that looks like the following: 74 | 75 | ``` 76 | //ab12cd34ef5.execute-api.eu-west-1.amazonaws.com/prod/:_authToken=dsfdsf678sdf78678768dsfsduihsd8798897989 77 | ``` 78 | 79 | In your CI tool you can then set the following environment variables (e.g. using the example above): 80 | ``` 81 | NPM_REGISTRY_LOGIN_URL=//ab12cd34ef5.execute-api.eu-west-1.amazonaws.com/prod/ 82 | NPM_AUTH_TOKEN=dsfdsf678sdf78678768dsfsduihsd8798897989 83 | ``` 84 | 85 | To allow your CI to access to the npm registry you should have a `.npmrc` file in the root of your repository, if not, as mentioned above we recommend doing this. 86 | 87 | Then as a pre build step before any `npm install` / package installs run the following to inject the authentication url into your `.npmrc` file. 88 | 89 | ``` 90 | echo "$NPM_REGISTRY_LOGIN_URL:_authToken=$NPM_AUTH_TOKEN" >> .npmrc 91 | ``` 92 | 93 | **Note:** 94 | You can then reuse this build step for all of your repositories using your private npm registry. 95 | 96 | ## Custom Domain 97 | If you are happy with Codebox on the AWS domain and wish to move it to a custom domain, instructions can be found on the AWS website [here](http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html). 98 | 99 | Once you have your custom domain setup you will need to ensure packages already published are migrated by running the following command (supply only the host of your custom domain): 100 | 101 | `serverless codebox domain --stage yourstage --host custom-domain.com` 102 | 103 | ## Other Resources 104 | 105 | [Blog (Previously named Yith)](https://craftship.io/open/source/serverless/private/npm/registry/yith/2016/09/26/serverless-yith.html) 106 | 107 | [FAQ](https://github.com/craftship/codebox-npm/wiki/FAQ) 108 | -------------------------------------------------------------------------------- /bin/integration-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NODE_TAG=$1 4 | 5 | docker build \ 6 | --rm=false \ 7 | --build-arg sls_version=1.6.1 \ 8 | -t codebox-npm-node-$NODE_TAG \ 9 | -q \ 10 | -f ./integration/Dockerfile-$NODE_TAG . 11 | 12 | docker run \ 13 | -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ 14 | -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ 15 | -e CODEBOX_REGION=${CODEBOX_REGION} \ 16 | -e CODEBOX_ADMINS=${CODEBOX_ADMINS} \ 17 | -e CODEBOX_REGISTRY=${CODEBOX_REGISTRY} \ 18 | -e CODEBOX_BUCKET=${CODEBOX_BUCKET} \ 19 | -e CODEBOX_GITHUB_URL=${CODEBOX_GITHUB_URL} \ 20 | -e CODEBOX_GITHUB_CLIENT_ID=${CODEBOX_GITHUB_CLIENT_ID} \ 21 | -e CODEBOX_GITHUB_SECRET=${CODEBOCODEBOX_GITHUB_SECRET} \ 22 | -e NPM_LOGIN_TOKEN=${NPM_LOGIN_TOKEN} \ 23 | -e CIRCLE_SHA1=${CIRCLE_SHA1} \ 24 | -e CIRCLE_NODE_INDEX=${CIRCLE_NODE_INDEX} \ 25 | -it \ 26 | --rm=false \ 27 | --name codebox-npm-node-$NODE_TAG codebox-npm-node-$NODE_TAG 28 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | if (!global._babelPolyfill) { 4 | require('babel-polyfill'); 5 | } 6 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | node: 5 | version: 8.4.0 6 | 7 | test: 8 | pre: 9 | - npm run nsp 10 | - npm run lint 11 | override: 12 | - npm run test 13 | - npm run coveralls 14 | # - case $CIRCLE_NODE_INDEX in 0) ./bin/integration-test latest ;; 1) ./bin/integration-test lts ;; esac: 15 | # parallel: true 16 | -------------------------------------------------------------------------------- /integration/Dockerfile-latest: -------------------------------------------------------------------------------- 1 | FROM node:7.5.0 2 | 3 | ARG sls_version 4 | 5 | ADD . /codebox 6 | 7 | WORKDIR /codebox 8 | 9 | RUN npm install --silent 10 | 11 | RUN npm install serverless@$sls_version -g --silent 12 | 13 | CMD ./integration/bin/test 14 | -------------------------------------------------------------------------------- /integration/Dockerfile-lts: -------------------------------------------------------------------------------- 1 | FROM node:6.9.5 2 | 3 | ARG sls_version 4 | 5 | ADD . /codebox 6 | 7 | WORKDIR /codebox 8 | 9 | RUN npm install --silent 10 | 11 | RUN npm install serverless@$sls_version -g --silent 12 | 13 | CMD ./integration/bin/test 14 | -------------------------------------------------------------------------------- /integration/bin/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | 5 | const path = require('path'); 6 | 7 | const spawn = require('child_process').spawn; 8 | 9 | let stage = 'integration'; 10 | 11 | if (process.env.CIRCLE_SHA1) { 12 | stage = `${process.env.CIRCLE_SHA1.substring(0, 8)}${process.env.CIRCLE_NODE_INDEX}`; 13 | } 14 | 15 | console.log(`Deploying codebox ${stage}...`); 16 | 17 | const slsDeploy = spawn('sls', ['deploy', '--stage', stage]); 18 | 19 | let registryUrl; 20 | 21 | slsDeploy.stdout.on('data', (data) => { 22 | if (!registryUrl) { 23 | const registryRegex = new RegExp('https://.+?(?={name})'); 24 | const urls = registryRegex.exec(data.toString()); 25 | 26 | if (urls) { 27 | registryUrl = urls[0]; 28 | const urlParts = registryUrl.split('/'); 29 | const loginUrl = `//${urlParts[2]}/${urlParts[3]}/${urlParts[4]}/:_authToken=${process.env.NPM_LOGIN_TOKEN}`; 30 | fs.writeFileSync(path.resolve(process.env.PWD, '.npmrc'), `registry=${registryUrl}\r\n${loginUrl}\r\n`); 31 | } 32 | } 33 | }); 34 | 35 | slsDeploy.on('exit', (code) => { 36 | if (code === 0) { 37 | console.log(`Finished deployment for codebox ${stage} : code ${code}`); 38 | } else { 39 | console.log(`Error deploying code ${code}`); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /integration/bin/remove: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | STAGE='integration' 4 | [ -n "${CIRCLE_SHA1}" ] && STAGE="${CIRCLE_SHA1:0:8}${CIRCLE_NODE_INDEX}" 5 | 6 | sls remove --stage $STAGE 7 | rm .npmrc 8 | -------------------------------------------------------------------------------- /integration/bin/test: -------------------------------------------------------------------------------- 1 | ./integration/bin/deploy 2 | $(npm bin)/mocha ./integration -t 15000 # Timeout set due to function cold starts 3 | ./integration/bin/remove 4 | -------------------------------------------------------------------------------- /integration/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import AWS from 'aws-sdk'; 3 | import { execFile as exec } from 'child_process'; 4 | 5 | const S3 = new AWS.S3({ 6 | signatureVersion: 'v4', 7 | }); 8 | 9 | export default { 10 | package: { 11 | publish: () => 12 | new Promise((resolve) => { 13 | exec('npm', ['publish', './integration/test-package'], () => { 14 | resolve(); 15 | }); 16 | }), 17 | delete: async () => { 18 | const fixture = 'test-package'; 19 | let stage = 'integration'; 20 | 21 | if (process.env.CIRCLE_SHA1) { 22 | stage = `${process.env.CIRCLE_SHA1.substring(0, 8)}${process.env.CIRCLE_NODE_INDEX || ''}`; 23 | } 24 | 25 | const bucket = `${process.env.YITH_BUCKET}-${stage}`; 26 | 27 | let items; 28 | try { 29 | items = await S3.listObjectsV2({ 30 | Bucket: bucket, 31 | Prefix: fixture, 32 | }).promise(); 33 | } catch (err) { 34 | throw new Error(`Could not list S3 objects for ${bucket}`); 35 | } 36 | 37 | items.Contents.forEach(async (item) => { 38 | try { 39 | await S3.deleteObject({ 40 | Bucket: bucket, 41 | Key: item.Key, 42 | }).promise(); 43 | } catch (err) { 44 | throw new Error(`Could not delete S3 object ${item.Key}`); 45 | } 46 | }); 47 | 48 | try { 49 | await S3.deleteObject({ 50 | Bucket: bucket, 51 | Key: fixture, 52 | }).promise(); 53 | } catch (err) { 54 | throw new Error(`Could not delee test package from ${bucket}`); 55 | } 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /integration/npm.test.js: -------------------------------------------------------------------------------- 1 | import { execFile as exec } from 'child_process'; 2 | import helpers from './helpers'; 3 | 4 | describe('npm', () => { 5 | context('get registry', () => { 6 | it('should use private registry', (done) => { 7 | exec('npm', ['get', 'registry'], { env: process.env }, (_, stdout) => { 8 | assert(!stdout.includes('registry.npmjs.org')); 9 | done(); 10 | }); 11 | }); 12 | }); 13 | 14 | context('publish ', () => { 15 | afterEach(async () => helpers.package.delete()); 16 | 17 | it('should not allow a re-publish', (done) => { 18 | exec('npm', ['publish', './integration/test-package'], () => { 19 | exec('npm', ['publish', './integration/test-package'], (_, stdout) => { 20 | assert(!stdout.includes('test-package@1.0.0')); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | 26 | it('should publish a new package correctly', (done) => { 27 | exec('npm', ['publish', './integration/test-package'], (_, stdout) => { 28 | assert(stdout.includes('test-package@1.0.0')); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | context('dist-tags', () => { 35 | beforeEach(async () => helpers.package.publish()); 36 | 37 | it('should list tags correctly', (done) => { 38 | exec('npm', ['dist-tags', 'add', 'test-package@1.0.0', 'alpha'], (_, stdout) => { 39 | assert(stdout.includes('alpha')); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should add tag correctly', (done) => { 45 | exec('npm', ['dist-tags', 'add', 'test-package@1.0.0', 'alpha'], (_, stdout) => { 46 | assert(stdout.includes('+alpha: test-package@1.0.0')); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should rm tag correctly', (done) => { 52 | exec('npm', ['dist-tags', 'add', 'test-package@1.0.0', 'alpha'], () => { 53 | exec('npm', ['dist-tags', 'rm', 'test-package', 'alpha'], (_, stdout) => { 54 | assert(stdout.includes('-alpha: test-package@1.0.0')); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | afterEach(async () => helpers.package.delete()); 61 | }); 62 | 63 | context('info', () => { 64 | it('should return package json ok', (done) => { 65 | exec('npm', ['info', 'serverless'], { env: process.env }, (error, stdout) => { 66 | assert(stdout.includes('name: \'serverless\'')); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /integration/test-package/index.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | console.log(`${pkg.name} ${pkg.version}`); 4 | -------------------------------------------------------------------------------- /integration/test-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-package", 3 | "version": "1.0.0", 4 | "description": "Test package for integration tests", 5 | "main": "index.js", 6 | "author": "Craftship Ltd ", 7 | "license": "MIT" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codebox-npm", 3 | "version": "0.21.2", 4 | "description": "Serverless private npm registry", 5 | "main": "index.js", 6 | "engines": { 7 | "node": "8.4.0", 8 | "npm": "5.3.0" 9 | }, 10 | "devDependencies": { 11 | "aws-sdk": "^2.7.10", 12 | "babel-loader": "^6.2.10", 13 | "babel-plugin-rewire": "^1.0.0", 14 | "babel-preset-env": "^1.1.8", 15 | "babel-register": "^6.22.0", 16 | "coveralls": "^2.11.15", 17 | "eslint": "^3.14.1", 18 | "eslint-config-airbnb-base": "^11.0.1", 19 | "eslint-plugin-import": "^2.2.0", 20 | "json-loader": "^0.5.4", 21 | "mocha": "^3.2.0", 22 | "nsp": "^2.6.2", 23 | "nyc": "^10.1.2", 24 | "serverless-webpack": "https://github.com/craftship/serverless-webpack.git", 25 | "sinon": "^1.17.7", 26 | "webpack-node-externals": "^1.5.4" 27 | }, 28 | "scripts": { 29 | "clean": "rm -rf ./lib && rm -rf ./coverage", 30 | "lint": "eslint .", 31 | "test": "npm run clean && nyc mocha", 32 | "nsp": "nsp check", 33 | "coveralls": "cat ./coverage/lcov.info | coveralls" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/craftship/codebox-npm.git" 38 | }, 39 | "keywords": [ 40 | "codebox", 41 | "npm", 42 | "registry", 43 | "private", 44 | "enterprise" 45 | ], 46 | "author": "Craftship Ltd ", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/craftship/codebox-npm/issues" 50 | }, 51 | "homepage": "https://github.com/craftship/codebox-npm#readme", 52 | "dependencies": { 53 | "@octokit/rest": "^15.8.1", 54 | "babel-polyfill": "^6.22.0", 55 | "babel-runtime": "^6.22.0", 56 | "node-fetch": "^1.6.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | frameworkVersion: '>=1.20.2' 2 | 3 | plugins: 4 | - environment-variables 5 | - remove-storage 6 | - serverless-webpack 7 | - content-handling 8 | - codebox-tools 9 | - set-api-host 10 | 11 | service: codebox-npm 12 | 13 | provider: 14 | name: aws 15 | runtime: nodejs6.10 16 | stage: ${opt:stage} 17 | region: ${env:CODEBOX_REGION} 18 | environment: 19 | admins: ${env:CODEBOX_ADMINS} 20 | restrictedOrgs: ${env:CODEBOX_RESTRICTED_ORGS} 21 | registry: ${env:CODEBOX_REGISTRY} 22 | githubUrl: ${env:CODEBOX_GITHUB_URL} 23 | githubClientId: ${env:CODEBOX_GITHUB_CLIENT_ID} 24 | githubSecret: ${env:CODEBOX_GITHUB_SECRET} 25 | bucket: ${env:CODEBOX_BUCKET}-${self:provider.stage} 26 | region: ${self:provider.region} 27 | 28 | clientId: ${env:CODEBOX_INSIGHTS_CLIENT_ID} 29 | secret: ${env:CODEBOX_INSIGHTS_SECRET} 30 | iamRoleStatements: 31 | - Effect: "Allow" 32 | Action: 33 | - "s3:ListBucket" 34 | - "s3:GetObject" 35 | - "s3:PutObject" 36 | - "logs:CreateLogGroup" 37 | - "logs:CreateLogStream" 38 | - "logs:PutLogEvents" 39 | - "sns:Publish" 40 | Resource: 41 | - "arn:aws:s3:::${self:provider.environment.bucket}*" 42 | - "Fn::Join": 43 | - "" 44 | - - "arn:aws:sns:" 45 | - Ref: "AWS::Region" 46 | - ":" 47 | - Ref: "AWS::AccountId" 48 | - ":${self:service}-${opt:stage}-log" 49 | 50 | functions: 51 | authorizerGithub: 52 | handler: authorizerGithub.default 53 | 54 | put: 55 | handler: put.default 56 | events: 57 | - http: 58 | path: 'registry/{name}' 59 | method: put 60 | authorizer: authorizerGithub 61 | get: 62 | handler: get.default 63 | events: 64 | - http: 65 | path: 'registry/{name}' 66 | method: get 67 | authorizer: authorizerGithub 68 | 69 | distTagsGet: 70 | handler: distTagsGet.default 71 | events: 72 | - http: 73 | path: 'registry/-/package/{name}/dist-tags' 74 | method: get 75 | authorizer: authorizerGithub 76 | distTagsPut: 77 | handler: distTagsPut.default 78 | events: 79 | - http: 80 | path: 'registry/-/package/{name}/dist-tags/{tag}' 81 | method: put 82 | authorizer: authorizerGithub 83 | distTagsDelete: 84 | handler: distTagsDelete.default 85 | events: 86 | - http: 87 | path: 'registry/-/package/{name}/dist-tags/{tag}' 88 | method: delete 89 | authorizer: authorizerGithub 90 | 91 | userPut: 92 | handler: userPut.default 93 | events: 94 | - http: 95 | path: 'registry/-/user/{id}' 96 | method: put 97 | 98 | userDelete: 99 | handler: userDelete.default 100 | events: 101 | - http: 102 | path: 'registry/-/user/token/{token}' 103 | method: delete 104 | authorizer: authorizerGithub 105 | 106 | whoamiGet: 107 | handler: whoamiGet.default 108 | events: 109 | - http: 110 | path: 'registry/-/whoami' 111 | method: get 112 | authorizer: authorizerGithub 113 | 114 | tarGet: 115 | handler: tarGet.default 116 | events: 117 | - http: 118 | integration: lambda 119 | authorizer: authorizerGithub 120 | path: 'registry/{name}/-/{tar}' 121 | method: get 122 | contentHandling: CONVERT_TO_BINARY 123 | request: 124 | template: 125 | application/json: > 126 | { 127 | "name": "$input.params('name')", 128 | "tar": "$input.params('tar')" 129 | } 130 | 131 | resources: 132 | Resources: 133 | PackageStorage: 134 | Type: AWS::S3::Bucket 135 | Properties: 136 | AccessControl: Private 137 | BucketName: ${self:provider.environment.bucket} 138 | PackageStoragePolicy: 139 | Type: "AWS::S3::BucketPolicy" 140 | DependsOn: "PackageStorage" 141 | Properties: 142 | Bucket: 143 | Ref: "PackageStorage" 144 | PolicyDocument: 145 | Statement: 146 | - Sid: DenyIncorrectEncryptionHeader 147 | Effect: Deny 148 | Principal: "*" 149 | Action: "s3:PutObject" 150 | Resource: "arn:aws:s3:::${self:provider.environment.bucket}/*" 151 | Condition: 152 | StringNotEquals: 153 | "s3:x-amz-server-side-encryption": AES256 154 | - Sid: DenyUnEncryptedObjectUploads 155 | Effect: Deny 156 | Principal: "*" 157 | Action: "s3:PutObject" 158 | Resource: "arn:aws:s3:::${self:provider.environment.bucket}/*" 159 | Condition: 160 | "Null": 161 | "s3:x-amz-server-side-encryption": true 162 | 163 | custom: 164 | webpackIncludeModules: true 165 | -------------------------------------------------------------------------------- /src/adapters/logger.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export default class Logger { 4 | constructor(cmd, namespace, credentials = {}) { 5 | this.command = cmd; 6 | this.namespace = namespace; 7 | this.credentials = credentials; 8 | } 9 | 10 | async publish(json) { 11 | if (this.credentials.clientId && this.credentials.secret) { 12 | await fetch('https://log.codebox.sh/v1/send', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | Authorization: `Bearer ${this.credentials.clientId}:${this.credentials.secret}`, 17 | }, 18 | body: JSON.stringify(json), 19 | }); 20 | } 21 | } 22 | 23 | async error(user, { stack, message }) { 24 | const json = { 25 | user, 26 | timestamp: new Date(), 27 | level: 'error', 28 | namespace: `error:${this.namespace}`, 29 | command: this.command, 30 | body: { 31 | message, 32 | stack, 33 | }, 34 | }; 35 | 36 | return this.publish(json); 37 | } 38 | 39 | async info(user, message) { 40 | const json = { 41 | user, 42 | timestamp: new Date(), 43 | level: 'info', 44 | namespace: `info:${this.namespace}`, 45 | command: this.command, 46 | body: message, 47 | }; 48 | 49 | return this.publish(json); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/adapters/npm.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export default { 4 | package: async (registry, name) => { 5 | const response = await fetch(`${registry}${name}`); 6 | 7 | if (!response.ok) { 8 | const error = new Error(`Could Not Find Package: ${registry}${name}`); 9 | error.status = response.status; 10 | throw error; 11 | } 12 | 13 | return response.json(); 14 | }, 15 | tar: async (registry, name) => { 16 | const response = await fetch(`${registry}${name}`); 17 | 18 | if (!response.ok) { 19 | const error = new Error(`Could Not Find Tar: ${registry}${name}`); 20 | error.status = response.status; 21 | throw error; 22 | } 23 | 24 | return response.buffer(); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/adapters/package.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftship/codebox-npm/fb281b8d99ca04ad6bfbd31475eed68be57b2b5e/src/adapters/package.js -------------------------------------------------------------------------------- /src/adapters/s3.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies 2 | 3 | export default class Storage { 4 | constructor({ region, bucket }) { 5 | this.S3 = new AWS.S3({ 6 | signatureVersion: 'v4', 7 | region, 8 | params: { 9 | Bucket: bucket, 10 | }, 11 | }); 12 | } 13 | 14 | async put(key, data, encoding) { 15 | return this.S3.putObject({ 16 | Key: key, 17 | Body: encoding === 'base64' ? new Buffer(data, 'base64') : data, 18 | ServerSideEncryption: 'AES256', 19 | }).promise(); 20 | } 21 | 22 | async get(key) { 23 | const meta = await this.S3.getObject({ 24 | Key: key, 25 | }).promise(); 26 | 27 | return meta.Body; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/authorizers/github.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import GitHub from '@octokit/rest'; 3 | 4 | const generatePolicy = ({ 5 | effect, 6 | methodArn, 7 | token, 8 | isAdmin, 9 | }) => { 10 | const methodParts = methodArn.split(':'); 11 | const region = methodParts[3]; 12 | const accountArn = methodParts[4]; 13 | const apiId = methodParts[5].split('/')[0]; 14 | const stage = methodParts[5].split('/')[1]; 15 | 16 | const authResponse = {}; 17 | authResponse.principalId = token; 18 | 19 | const policyDocument = {}; 20 | policyDocument.Version = '2012-10-17'; 21 | policyDocument.Statement = []; 22 | 23 | const statementOne = {}; 24 | statementOne.Action = 'execute-api:Invoke'; 25 | statementOne.Effect = effect; 26 | statementOne.Resource = `arn:aws:execute-api:${region}:${accountArn}:${apiId}/${stage}/GET/registry*`; 27 | policyDocument.Statement[0] = statementOne; 28 | 29 | const statementTwo = {}; 30 | statementTwo.Action = 'execute-api:Invoke'; 31 | statementTwo.Effect = isAdmin ? 'Allow' : 'Deny'; 32 | statementTwo.Resource = `arn:aws:execute-api:${region}:${accountArn}:${apiId}/${stage}/PUT/registry*`; 33 | policyDocument.Statement[1] = statementTwo; 34 | 35 | const statementThree = {}; 36 | statementThree.Action = 'execute-api:Invoke'; 37 | statementThree.Effect = isAdmin ? 'Allow' : 'Deny'; 38 | statementThree.Resource = `arn:aws:execute-api:${region}:${accountArn}:${apiId}/${stage}/DELETE/registry*`; 39 | policyDocument.Statement[2] = statementThree; 40 | 41 | authResponse.policyDocument = policyDocument; 42 | 43 | return authResponse; 44 | }; 45 | 46 | export default async ({ methodArn, authorizationToken }, context, callback) => { 47 | const tokenParts = authorizationToken.split('Bearer '); 48 | 49 | if (tokenParts.length <= 1) { 50 | return callback(null, generatePolicy({ 51 | token: authorizationToken, 52 | effect: 'Deny', 53 | methodArn, 54 | isAdmin: false, 55 | })); 56 | } 57 | 58 | const token = tokenParts[1]; 59 | 60 | const parsedUrl = url.parse(process.env.githubUrl); 61 | const github = new GitHub({ 62 | host: parsedUrl.host, 63 | protocol: 'https', 64 | pathPrefix: parsedUrl.path, 65 | }); 66 | 67 | github.authenticate({ 68 | type: 'basic', 69 | username: process.env.githubClientId, 70 | password: process.env.githubSecret, 71 | }); 72 | 73 | try { 74 | const { 75 | user, 76 | updated_at, 77 | created_at, 78 | } = await github.authorization.check({ 79 | client_id: process.env.githubClientId, 80 | access_token: token, 81 | }); 82 | 83 | let isAdmin = false; 84 | let effect = 'Allow'; 85 | let restrictedOrgs = []; 86 | 87 | if (process.env.restrictedOrgs) { 88 | restrictedOrgs = process.env.restrictedOrgs.split(','); 89 | } 90 | 91 | if (restrictedOrgs.length) { 92 | try { 93 | github.authenticate({ 94 | type: 'token', 95 | token, 96 | }); 97 | 98 | const orgs = await github.users.getOrgMemberships({ 99 | state: 'active', 100 | }); 101 | 102 | const usersOrgs = orgs.filter(org => restrictedOrgs.indexOf(org.organization.login) > -1); 103 | effect = usersOrgs.length ? 'Allow' : 'Deny'; 104 | } catch (githubError) { 105 | return callback(null, generatePolicy({ 106 | token: tokenParts[1], 107 | effect: 'Deny', 108 | methodArn, 109 | isAdmin: false, 110 | })); 111 | } 112 | } 113 | 114 | if (process.env.admins) { 115 | isAdmin = process.env.admins.split(',').indexOf(user.login) > -1; 116 | } 117 | 118 | const policy = generatePolicy({ 119 | effect, 120 | methodArn, 121 | token, 122 | isAdmin, 123 | }); 124 | 125 | policy.context = { 126 | username: user.login, 127 | avatar: user.avatar_url, 128 | updatedAt: updated_at, 129 | createdAt: created_at, 130 | }; 131 | 132 | return callback(null, policy); 133 | } catch (error) { 134 | return callback(null, generatePolicy({ 135 | token: tokenParts[1], 136 | effect: 'Deny', 137 | methodArn, 138 | isAdmin: false, 139 | })); 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /src/contextFactory.js: -------------------------------------------------------------------------------- 1 | import npm from './adapters/npm'; 2 | import S3 from './adapters/s3'; 3 | import Logger from './adapters/logger'; 4 | 5 | const user = authorizer => ({ 6 | name: authorizer.username, 7 | avatar: authorizer.avatar, 8 | }); 9 | 10 | const command = (headers) => { 11 | if (headers.Referer) { 12 | const refererParts = headers.Referer.split(' '); 13 | const name = refererParts[0]; 14 | 15 | return { 16 | name, 17 | args: refererParts.slice(1), 18 | }; 19 | } 20 | 21 | return { 22 | name: 'Unknown', 23 | args: [], 24 | }; 25 | }; 26 | 27 | const storage = (region, bucket) => 28 | new S3({ 29 | region, 30 | bucket, 31 | }); 32 | 33 | const log = (cmd, namespace, region, topic) => { 34 | if (process.env.clientId && process.env.secret) { 35 | return new Logger( 36 | cmd, 37 | namespace, 38 | { 39 | clientId: process.env.clientId, 40 | secret: process.env.secret, 41 | }, 42 | ); 43 | } 44 | 45 | return new Logger( 46 | cmd, 47 | namespace, { 48 | region, 49 | topic, 50 | }); 51 | }; 52 | 53 | export default (namespace, { headers, requestContext }) => { 54 | const { 55 | registry, 56 | bucket, 57 | region, 58 | logTopic, 59 | apiEndpoint, 60 | } = process.env; 61 | 62 | const cmd = command(headers); 63 | 64 | return { 65 | command: cmd, 66 | registry, 67 | apiEndpoint, 68 | user: user(requestContext.authorizer), 69 | storage: storage(region, bucket), 70 | log: log(cmd, namespace, region, logTopic), 71 | npm, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/dist-tags/delete.js: -------------------------------------------------------------------------------- 1 | import S3 from '../adapters/s3'; 2 | import Logger from '../adapters/logger'; 3 | 4 | export default async ({ 5 | requestContext, 6 | pathParameters, 7 | }, context, callback) => { 8 | const { bucket, region, logTopic } = process.env; 9 | const user = { 10 | name: requestContext.authorizer.username, 11 | avatar: requestContext.authorizer.avatar, 12 | }; 13 | const storage = new S3({ region, bucket }); 14 | const log = new Logger('dist-tags:delete', { region, topic: logTopic }); 15 | 16 | const name = `${decodeURIComponent(pathParameters.name)}`; 17 | 18 | try { 19 | const pkgBuffer = await storage.get(`${name}/index.json`); 20 | const json = JSON.parse(pkgBuffer.toString()); 21 | delete json['dist-tags'][pathParameters.tag]; 22 | 23 | await storage.put( 24 | `${name}/index.json`, 25 | JSON.stringify(json), 26 | ); 27 | 28 | await log.info(user, { 29 | name: json.name, 30 | tag: pathParameters.tag, 31 | 'dist-tags': json['dist-tags'], 32 | }); 33 | 34 | return callback(null, { 35 | statusCode: 200, 36 | body: JSON.stringify({ 37 | ok: true, 38 | id: pathParameters.name, 39 | 'dist-tags': json['dist-tags'], 40 | }), 41 | }); 42 | } catch (storageError) { 43 | await log.error(user, storageError); 44 | 45 | return callback(null, { 46 | statusCode: 500, 47 | body: JSON.stringify({ 48 | ok: false, 49 | error: storageError.message, 50 | }), 51 | }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/dist-tags/get.js: -------------------------------------------------------------------------------- 1 | import npm from '../adapters/npm'; 2 | import S3 from '../adapters/s3'; 3 | import Logger from '../adapters/logger'; 4 | 5 | export default async ({ 6 | requestContext, 7 | pathParameters, 8 | }, context, callback) => { 9 | const { registry, bucket, region, logTopic } = process.env; 10 | const user = { 11 | name: requestContext.authorizer.username, 12 | avatar: requestContext.authorizer.avatar, 13 | }; 14 | const storage = new S3({ region, bucket }); 15 | const log = new Logger('dist-tags:get', { region, topic: logTopic }); 16 | 17 | const name = `${decodeURIComponent(pathParameters.name)}`; 18 | 19 | try { 20 | const pkgBuffer = await storage.get(`${name}/index.json`); 21 | const json = JSON.parse(pkgBuffer.toString()); 22 | return callback(null, { 23 | statusCode: 200, 24 | body: JSON.stringify(json['dist-tags']), 25 | }); 26 | } catch (storageError) { 27 | if (storageError.code === 'NoSuchKey') { 28 | try { 29 | const data = await npm.package(registry, name); 30 | return callback(null, { 31 | statusCode: 200, 32 | body: JSON.stringify(data['dist-tags']), 33 | }); 34 | } catch ({ message }) { 35 | return callback(null, { 36 | statusCode: 404, 37 | body: JSON.stringify({ 38 | ok: false, 39 | error: message, 40 | }), 41 | }); 42 | } 43 | } 44 | 45 | await log.error(user, storageError); 46 | 47 | return callback(null, { 48 | statusCode: 500, 49 | body: JSON.stringify({ 50 | ok: false, 51 | error: storageError.message, 52 | }), 53 | }); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/dist-tags/put.js: -------------------------------------------------------------------------------- 1 | import S3 from '../adapters/s3'; 2 | import Logger from '../adapters/logger'; 3 | 4 | export default async ({ 5 | requestContext, 6 | body, 7 | pathParameters, 8 | }, context, callback) => { 9 | const { bucket, region, logTopic } = process.env; 10 | const user = { 11 | name: requestContext.authorizer.username, 12 | avatar: requestContext.authorizer.avatar, 13 | }; 14 | const storage = new S3({ region, bucket }); 15 | const log = new Logger('dist-tags:put', { region, topic: logTopic }); 16 | 17 | const name = `${decodeURIComponent(pathParameters.name)}`; 18 | 19 | try { 20 | const pkgBuffer = await storage.get(`${name}/index.json`); 21 | const json = JSON.parse(pkgBuffer.toString()); 22 | const version = body.replace(/"/g, ''); 23 | 24 | json['dist-tags'][pathParameters.tag] = version; 25 | 26 | await storage.put( 27 | `${name}/index.json`, 28 | JSON.stringify(json), 29 | ); 30 | 31 | await log.info(user, { 32 | name: json.name, 33 | tag: pathParameters.tag, 34 | version, 35 | 'dist-tags': json['dist-tags'], 36 | }); 37 | 38 | return callback(null, { 39 | statusCode: 200, 40 | body: JSON.stringify({ 41 | ok: true, 42 | id: pathParameters.name, 43 | 'dist-tags': json['dist-tags'], 44 | }), 45 | }); 46 | } catch (storageError) { 47 | await log.error(user, storageError); 48 | 49 | return callback(null, { 50 | statusCode: 500, 51 | body: JSON.stringify({ 52 | ok: false, 53 | error: storageError.message, 54 | }), 55 | }); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/get/index.js: -------------------------------------------------------------------------------- 1 | import lib from './lib'; 2 | import contextFactory from '../contextFactory'; 3 | 4 | export default async (event, _, callback) => { 5 | lib( 6 | event, 7 | contextFactory( 8 | 'package:get', 9 | event, 10 | ), 11 | callback, 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/get/lib.js: -------------------------------------------------------------------------------- 1 | export default async ({ pathParameters }, { 2 | registry, 3 | user, 4 | storage, 5 | npm, 6 | log, 7 | }, callback) => { 8 | const name = `${decodeURIComponent(pathParameters.name)}`; 9 | 10 | try { 11 | const pkgBuffer = await storage.get(`${name}/index.json`); 12 | const json = JSON.parse(pkgBuffer.toString()); 13 | json._attachments = {}; // eslint-disable-line no-underscore-dangle 14 | 15 | const version = json['dist-tags'].latest; 16 | 17 | await log.info(user, { 18 | name: json.name, 19 | version, 20 | }); 21 | 22 | return callback(null, { 23 | statusCode: 200, 24 | body: JSON.stringify(json), 25 | }); 26 | } catch (storageError) { 27 | if (storageError.code === 'NoSuchKey') { 28 | try { 29 | const json = await npm.package(registry, pathParameters.name); 30 | 31 | const version = json['dist-tags'].latest; 32 | 33 | await log.info(user, { 34 | name: json.name, 35 | version, 36 | }); 37 | 38 | return callback(null, { 39 | statusCode: 200, 40 | body: JSON.stringify(json), 41 | }); 42 | } catch (npmError) { 43 | if (npmError.status === 500) { 44 | await log.error(user, npmError); 45 | } 46 | 47 | return callback(null, { 48 | statusCode: npmError.status, 49 | body: JSON.stringify({ 50 | error: npmError.message, 51 | }), 52 | }); 53 | } 54 | } 55 | 56 | await log.error(user, storageError); 57 | 58 | return callback(null, { 59 | statusCode: 500, 60 | body: JSON.stringify({ 61 | error: storageError.message, 62 | }), 63 | }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/put/deprecate.js: -------------------------------------------------------------------------------- 1 | export default async ({ 2 | requestContext, 3 | pathParameters, 4 | body, 5 | }, { 6 | registry, 7 | user, 8 | storage, 9 | npm, 10 | log, 11 | }, callback) => { 12 | const name = `${decodeURIComponent(pathParameters.name)}`; 13 | 14 | try { 15 | await storage.put( 16 | `${name}/index.json`, 17 | body.toString(), 18 | ); 19 | 20 | return callback(null, { 21 | statusCode: 200, 22 | body: JSON.stringify({ 23 | success: true, 24 | }), 25 | }); 26 | } catch (putError) { 27 | await log.error(user, putError); 28 | 29 | return callback(null, { 30 | statusCode: 500, 31 | body: JSON.stringify({ 32 | success: false, 33 | error: putError.message, 34 | }), 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/put/index.js: -------------------------------------------------------------------------------- 1 | import deprecate from './deprecate'; 2 | import publish from './publish'; 3 | import contextFactory from '../contextFactory'; 4 | 5 | export default async (event, _, callback) => { 6 | const context = contextFactory( 7 | 'package:put', 8 | event, 9 | ); 10 | 11 | if (context.command.name === 'deprecate') { 12 | return deprecate( 13 | event, 14 | context, 15 | callback, 16 | ); 17 | } 18 | 19 | return publish( 20 | event, 21 | context, 22 | callback, 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/put/publish.js: -------------------------------------------------------------------------------- 1 | export default async ({ 2 | requestContext, 3 | pathParameters, 4 | body, 5 | }, { 6 | registry, 7 | apiEndpoint, 8 | user, 9 | storage, 10 | npm, 11 | log, 12 | }, callback) => { 13 | // Ensure package has unique name on npm 14 | try { 15 | const data = await npm.package( 16 | registry, 17 | pathParameters.name, 18 | ); 19 | 20 | if (data._id) { // eslint-disable-line no-underscore-dangle 21 | return callback(null, { 22 | statusCode: 403, 23 | body: JSON.stringify({ 24 | success: false, 25 | error: 'Your package name needs to be unique to the public npm registry.', 26 | }), 27 | }); 28 | } 29 | } catch (npmError) { 30 | if (npmError.status === 500) { 31 | await log.error(user, npmError); 32 | 33 | return callback(null, { 34 | statusCode: npmError.status, 35 | body: JSON.stringify({ 36 | success: false, 37 | error: npmError.message, 38 | }), 39 | }); 40 | } 41 | } 42 | 43 | const name = `${decodeURIComponent(pathParameters.name)}`; 44 | const pkg = JSON.parse(body); 45 | const tag = Object.keys(pkg['dist-tags'])[0]; 46 | const version = pkg['dist-tags'][tag]; 47 | const versionData = pkg.versions[version]; 48 | 49 | const tarballFilename = encodeURIComponent(versionData.dist.tarball.split('/-/')[1]); 50 | versionData.dist.tarball = `${apiEndpoint}/${pathParameters.name}/-/${tarballFilename}`; 51 | 52 | let json = {}; 53 | 54 | try { 55 | const pkgBuffer = await storage.get(`${name}/index.json`); 56 | json = JSON.parse(pkgBuffer.toString()); 57 | 58 | if (json.versions[version]) { 59 | return callback(null, { 60 | statusCode: 403, 61 | body: JSON.stringify({ 62 | success: false, 63 | error: `You cannot publish over the previously published version ${version}.`, 64 | }), 65 | }); 66 | } 67 | 68 | json['dist-tags'][tag] = version; 69 | json._attachments = {}; // eslint-disable-line no-underscore-dangle 70 | json._attachments[`${name}-${version}.tgz`] = pkg._attachments[`${name}-${version}.tgz`]; // eslint-disable-line no-underscore-dangle 71 | json.versions[version] = versionData; 72 | } catch (storageError) { 73 | if (storageError.code === 'NoSuchKey') { 74 | json = pkg; 75 | json['dist-tags'].latest = version; 76 | } 77 | } 78 | 79 | try { 80 | await storage.put( 81 | `${name}/${version}.tgz`, 82 | json._attachments[`${name}-${version}.tgz`].data, // eslint-disable-line no-underscore-dangle 83 | 'base64', 84 | ); 85 | 86 | await storage.put( 87 | `${name}/index.json`, 88 | JSON.stringify(json), 89 | ); 90 | 91 | await log.info(user, { 92 | name: versionData.name, 93 | description: versionData.description, 94 | version, 95 | keywords: versionData.keywords, 96 | license: versionData.license, 97 | contributors: versionData.contributors, 98 | dependencies: versionData.dependencies, 99 | homepage: versionData.homepage, 100 | repository: versionData.repository, 101 | 'dist-tags': json['dist-tags'], 102 | }); 103 | 104 | return callback(null, { 105 | statusCode: 200, 106 | body: JSON.stringify({ 107 | success: true, 108 | }), 109 | }); 110 | } catch (putError) { 111 | await log.error(user, putError); 112 | 113 | return callback(null, { 114 | statusCode: 500, 115 | body: JSON.stringify({ 116 | success: false, 117 | error: putError.message, 118 | }), 119 | }); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/tar/get.js: -------------------------------------------------------------------------------- 1 | import npm from '../adapters/npm'; 2 | import S3 from '../adapters/s3'; 3 | 4 | export default async (event, context, callback) => { 5 | const { registry, bucket, region } = process.env; 6 | const storage = new S3({ region, bucket }); 7 | 8 | const name = `${decodeURIComponent(event.name)}`; 9 | const tarName = `${decodeURIComponent(event.tar)}`; 10 | 11 | try { 12 | const fileName = tarName.replace(`${name}-`, ''); 13 | const pkgBuffer = await storage.get(`${name}/${fileName}`); 14 | 15 | return callback(null, pkgBuffer.toString('base64')); 16 | } catch (storageError) { 17 | if (storageError.code === 'NoSuchKey') { 18 | try { 19 | const npmPkgBuffer = await npm.tar(registry, `${event.name}/-/${event.tar}`); 20 | return callback(null, npmPkgBuffer.toString('base64')); 21 | } catch (npmError) { 22 | return callback(npmError); 23 | } 24 | } 25 | 26 | return callback(storageError); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/user/delete.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import GitHub from '@octokit/rest'; 3 | 4 | export default async ({ pathParameters }, context, callback) => { 5 | const { 6 | token, 7 | } = pathParameters; 8 | 9 | const parsedUrl = url.parse(process.env.githubUrl); 10 | const github = new GitHub({ 11 | host: parsedUrl.host, 12 | protocol: 'https', 13 | pathPrefix: parsedUrl.path, 14 | }); 15 | 16 | github.authenticate({ 17 | type: 'basic', 18 | username: process.env.githubClientId, 19 | password: process.env.githubSecret, 20 | }); 21 | 22 | try { 23 | await github.authorization.reset({ 24 | client_id: process.env.githubClientId, 25 | access_token: token, 26 | }); 27 | 28 | return callback(null, { 29 | statusCode: 200, 30 | body: JSON.stringify({ 31 | ok: true, 32 | }), 33 | }); 34 | } catch (err) { 35 | return callback(null, { 36 | statusCode: 500, 37 | body: JSON.stringify({ 38 | ok: false, 39 | error: err.message, 40 | }), 41 | }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/user/put.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import GitHub from '@octokit/rest'; 3 | 4 | export default async ({ body }, context, callback) => { 5 | const { 6 | name, 7 | password, 8 | } = JSON.parse(body); 9 | 10 | const scopes = ['user:email']; 11 | 12 | if (process.env.restrictedOrgs) { 13 | scopes.push('read:org'); 14 | } 15 | 16 | const nameParts = name.split('.'); 17 | const username = nameParts[0]; 18 | const otp = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; 19 | 20 | const parsedUrl = url.parse(process.env.githubUrl); 21 | const github = new GitHub({ 22 | host: parsedUrl.host, 23 | protocol: 'https', 24 | pathPrefix: parsedUrl.path, 25 | }); 26 | 27 | github.authenticate({ 28 | type: 'basic', 29 | username, 30 | password, 31 | }); 32 | 33 | let auth = {}; 34 | try { 35 | auth = await github.authorization.getOrCreateAuthorizationForApp({ 36 | scopes, 37 | client_id: process.env.githubClientId, 38 | client_secret: process.env.githubSecret, 39 | note: 'codebox private npm registry', 40 | headers: { 41 | 'X-GitHub-OTP': otp, 42 | }, 43 | }); 44 | 45 | if (!auth.token.length) { 46 | await github.authorization.delete({ 47 | id: auth.id, 48 | headers: { 49 | 'X-GitHub-OTP': otp, 50 | }, 51 | }); 52 | 53 | auth = await github.authorization.create({ 54 | scopes, 55 | client_id: process.env.githubClientId, 56 | client_secret: process.env.githubSecret, 57 | note: 'codebox private npm registry', 58 | headers: { 59 | 'X-GitHub-OTP': otp, 60 | }, 61 | }); 62 | } 63 | 64 | return callback(null, { 65 | statusCode: 201, 66 | body: JSON.stringify({ 67 | ok: true, 68 | token: auth.token, 69 | }), 70 | }); 71 | } catch (error) { 72 | return callback(null, { 73 | statusCode: 403, 74 | body: JSON.stringify({ 75 | ok: false, 76 | error: error.message, 77 | }), 78 | }); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/whoami/get.js: -------------------------------------------------------------------------------- 1 | export default async ({ requestContext }, _, callback) => 2 | callback(null, { 3 | statusCode: 200, 4 | body: JSON.stringify({ 5 | username: requestContext.authorizer.username, 6 | }), 7 | }); 8 | 9 | -------------------------------------------------------------------------------- /test/adapters/logger.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Subject from '../../src/adapters/logger'; 3 | 4 | describe('Logger', () => { 5 | let user; 6 | let clock; 7 | let fetchStub; 8 | 9 | beforeEach(() => { 10 | user = { 11 | name: 'foo', 12 | avatar: 'https://example.com', 13 | }; 14 | 15 | fetchStub = stub(); 16 | 17 | clock = useFakeTimers(); 18 | 19 | Subject.__Rewire__('fetch', fetchStub); 20 | }); 21 | 22 | describe('#info()', () => { 23 | it('should call insights logging endpoint with correct parameters', async () => { 24 | const subject = new Subject({ name: 'foo', args: [] }, 'foo:bar', { 25 | clientId: 'foo-client-id', 26 | secret: 'bar-secret', 27 | }); 28 | 29 | await subject.info(user, { foo: 'bar' }); 30 | 31 | assert(fetchStub.calledWithExactly('https://log.codebox.sh/v1/send', { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | Authorization: 'Bearer foo-client-id:bar-secret', 36 | }, 37 | body: '{"user":{"name":"foo","avatar":"https://example.com"},"timestamp":"1970-01-01T00:00:00.000Z","level":"info","namespace":"info:foo:bar","command":{"name":"foo","args":[]},"body":{"foo":"bar"}}', 38 | })); 39 | }); 40 | }); 41 | 42 | describe('#error()', () => { 43 | it('should call insights logging endpoint with correct parameters', async () => { 44 | const subject = new Subject({ name: 'foo', args: [] }, 'foo:bar', { 45 | clientId: 'foo-client-id', 46 | secret: 'bar-secret', 47 | }); 48 | 49 | const expectedError = new Error('Foo Bar'); 50 | expectedError.stack = 'foo bar stack'; 51 | 52 | await subject.error(user, expectedError); 53 | 54 | assert(fetchStub.calledWithExactly('https://log.codebox.sh/v1/send', { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/json', 58 | Authorization: 'Bearer foo-client-id:bar-secret', 59 | }, 60 | body: '{"user":{"name":"foo","avatar":"https://example.com"},"timestamp":"1970-01-01T00:00:00.000Z","level":"error","namespace":"error:foo:bar","command":{"name":"foo","args":[]},"body":{"message":"Foo Bar","stack":"foo bar stack"}}', 61 | })); 62 | }); 63 | }); 64 | 65 | afterEach(() => { 66 | clock.restore(); 67 | Subject.__ResetDependency__('fetch'); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/adapters/npm.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import subject from '../../src/adapters/npm'; 3 | 4 | describe('NPM', () => { 5 | describe('#tar()', () => { 6 | context('does not exist', () => { 7 | let fetchStub; 8 | 9 | beforeEach(() => { 10 | fetchStub = stub().returns({ 11 | ok: false, 12 | status: 404, 13 | }); 14 | 15 | subject.__Rewire__('fetch', fetchStub); 16 | }); 17 | 18 | it('throw correct error', async () => { 19 | try { 20 | await subject.tar( 21 | 'https://example.com/', 22 | 'foo-tar', 23 | ); 24 | } catch (error) { 25 | assert.equal(error.status, 404); 26 | assert.equal(error.message, 'Could Not Find Tar: https://example.com/foo-tar'); 27 | } 28 | }); 29 | 30 | afterEach(() => { 31 | subject.__ResetDependency__('fetch'); 32 | }); 33 | }); 34 | 35 | context('exists', () => { 36 | const expected = new Buffer('foo'); 37 | let fetchStub; 38 | 39 | beforeEach(() => { 40 | fetchStub = stub().returns({ 41 | ok: true, 42 | buffer: () => Promise.resolve(expected), 43 | }); 44 | 45 | subject.__Rewire__('fetch', fetchStub); 46 | }); 47 | 48 | it('should return buffer', async () => { 49 | const actual = await subject.tar( 50 | 'https://example.com', 51 | 'foo-package', 52 | ); 53 | 54 | assert.equal(actual, expected); 55 | }); 56 | 57 | afterEach(() => { 58 | subject.__ResetDependency__('fetch'); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('#package()', () => { 64 | context('does not exist', () => { 65 | let fetchStub; 66 | 67 | beforeEach(() => { 68 | fetchStub = stub().returns({ 69 | ok: false, 70 | status: 404, 71 | }); 72 | 73 | subject.__Rewire__('fetch', fetchStub); 74 | }); 75 | 76 | it('throw correct error', async () => { 77 | try { 78 | await subject.package( 79 | 'https://example.com/', 80 | 'foo-package', 81 | ); 82 | } catch (error) { 83 | assert.equal(error.status, 404); 84 | assert.equal(error.message, 'Could Not Find Package: https://example.com/foo-package'); 85 | } 86 | }); 87 | 88 | afterEach(() => { 89 | subject.__ResetDependency__('fetch'); 90 | }); 91 | }); 92 | 93 | context('exists', () => { 94 | const expected = { name: 'foo-package' }; 95 | let fetchStub; 96 | 97 | beforeEach(() => { 98 | fetchStub = stub().returns({ 99 | ok: true, 100 | json: () => Promise.resolve(expected), 101 | }); 102 | 103 | subject.__Rewire__('fetch', fetchStub); 104 | }); 105 | 106 | it('should return json', async () => { 107 | const actual = await subject.package( 108 | 'https://example.com', 109 | 'foo-package', 110 | ); 111 | 112 | assert.equal(actual, expected); 113 | }); 114 | 115 | afterEach(() => { 116 | subject.__ResetDependency__('fetch'); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/adapters/s3.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import AWS from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies 3 | import Subject from '../../src/adapters/s3'; 4 | 5 | describe('S3', () => { 6 | let awsSpy; 7 | let putObjectStub; 8 | let getObjectStub; 9 | 10 | beforeEach(() => { 11 | awsSpy = { 12 | S3: spy(() => { 13 | getObjectStub = stub().returns({ promise: () => Promise.resolve() }); 14 | putObjectStub = stub().returns({ promise: () => Promise.resolve() }); 15 | 16 | const awsS3Instance = createStubInstance(AWS.S3); 17 | awsS3Instance.putObject = putObjectStub; 18 | awsS3Instance.getObject = getObjectStub; 19 | 20 | return awsS3Instance; 21 | }), 22 | }; 23 | 24 | Subject.__Rewire__('AWS', awsSpy); 25 | }); 26 | 27 | describe('#put()', () => { 28 | context('base64', () => { 29 | it('should call AWS with correct parameters', async () => { 30 | const subject = new Subject({ 31 | region: 'foo-region', 32 | bucket: 'bar-bucket', 33 | }); 34 | 35 | await subject.put('foo-key', 'test', 'base64'); 36 | 37 | assert(putObjectStub.calledWithExactly({ 38 | Key: 'foo-key', 39 | Body: new Buffer('test', 'base64'), 40 | ServerSideEncryption: 'AES256', 41 | })); 42 | }); 43 | }); 44 | 45 | context('string', () => { 46 | it('should call AWS with correct parameters', async () => { 47 | const subject = new Subject({ 48 | region: 'foo-region', 49 | bucket: 'bar-bucket', 50 | }); 51 | 52 | await subject.put('foo-key', 'test'); 53 | 54 | assert(putObjectStub.calledWithExactly({ 55 | Key: 'foo-key', 56 | Body: 'test', 57 | ServerSideEncryption: 'AES256', 58 | })); 59 | }); 60 | }); 61 | }); 62 | 63 | afterEach(() => { 64 | Subject.__ResetDependency__('AWS'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/authorizers/github.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import GitHub from '@octokit/rest'; 3 | import subject from '../../src/authorizers/github'; 4 | 5 | describe('GitHub Authorizer', () => { 6 | let event; 7 | let callback; 8 | let gitHubSpy; 9 | let gitHubInstance; 10 | 11 | beforeEach(() => { 12 | const env = { 13 | githubClientId: 'foo-client-id', 14 | githubSecret: 'bar-secret', 15 | githubUrl: 'https://example.com', 16 | }; 17 | 18 | process.env = env; 19 | 20 | callback = stub(); 21 | }); 22 | 23 | describe('invalid access token', () => { 24 | context('with github application', () => { 25 | let checkAuthStub; 26 | 27 | beforeEach(() => { 28 | event = { 29 | authorizationToken: 'Bearer foo-invalid-github-token', 30 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 31 | }; 32 | 33 | gitHubSpy = spy(() => { 34 | gitHubInstance = createStubInstance(GitHub); 35 | checkAuthStub = stub().throws(new Error('Invalid token with GitHub app')); 36 | 37 | gitHubInstance.authorization = { 38 | check: checkAuthStub, 39 | }; 40 | gitHubInstance.authenticate = stub(); 41 | 42 | return gitHubInstance; 43 | }); 44 | 45 | subject.__Rewire__({ 46 | GitHub: gitHubSpy, 47 | }); 48 | }); 49 | 50 | it('should deny get, put and delete', async () => { 51 | await subject(event, stub(), callback); 52 | 53 | assert(callback.calledWithExactly(null, { 54 | principalId: 'foo-invalid-github-token', 55 | policyDocument: { 56 | Version: '2012-10-17', 57 | Statement: [ 58 | { 59 | Action: 'execute-api:Invoke', 60 | Effect: 'Deny', 61 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 62 | }, 63 | { 64 | Action: 'execute-api:Invoke', 65 | Effect: 'Deny', 66 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 67 | }, 68 | { 69 | Action: 'execute-api:Invoke', 70 | Effect: 'Deny', 71 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 72 | }, 73 | ], 74 | }, 75 | })); 76 | }); 77 | 78 | afterEach(() => { 79 | subject.__ResetDependency__('GitHub'); 80 | }); 81 | }); 82 | 83 | context('auhtorization header', () => { 84 | beforeEach(() => { 85 | event = { 86 | authorizationToken: 'foo-invalid-token', 87 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 88 | }; 89 | }); 90 | 91 | it('should deny get, put and delete', async () => { 92 | await subject(event, stub(), callback); 93 | 94 | assert(callback.calledWithExactly(null, { 95 | principalId: 'foo-invalid-token', 96 | policyDocument: { 97 | Version: '2012-10-17', 98 | Statement: [ 99 | { 100 | Action: 'execute-api:Invoke', 101 | Effect: 'Deny', 102 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 103 | }, 104 | { 105 | Action: 'execute-api:Invoke', 106 | Effect: 'Deny', 107 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 108 | }, 109 | { 110 | Action: 'execute-api:Invoke', 111 | Effect: 'Deny', 112 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 113 | }, 114 | ], 115 | }, 116 | })); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('valid access token', () => { 122 | context('is in restricted org', () => { 123 | let authStub; 124 | let getOrgMembershipsStub; 125 | 126 | beforeEach(() => { 127 | process.env.admins = ''; 128 | process.env.restrictedOrgs = 'foo-org'; 129 | 130 | event = { 131 | authorizationToken: 'Bearer foo-valid-token', 132 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 133 | }; 134 | 135 | gitHubSpy = spy(() => { 136 | gitHubInstance = createStubInstance(GitHub); 137 | authStub = stub(); 138 | getOrgMembershipsStub = stub().returns([{ 139 | organization: { 140 | login: 'foo-org', 141 | }, 142 | }]); 143 | 144 | const checkAuthStub = stub().returns({ 145 | user: { 146 | login: 'foo-user', 147 | avatar_url: 'https://example.com', 148 | }, 149 | created_at: '2001-01-01T00:00:00Z', 150 | updated_at: '2001-02-01T00:00:00Z', 151 | }); 152 | 153 | gitHubInstance.authenticate = authStub; 154 | gitHubInstance.authorization = { 155 | check: checkAuthStub, 156 | }; 157 | gitHubInstance.users = { 158 | getOrgMemberships: getOrgMembershipsStub, 159 | }; 160 | 161 | return gitHubInstance; 162 | }); 163 | 164 | subject.__Rewire__({ 165 | GitHub: gitHubSpy, 166 | }); 167 | }); 168 | 169 | it('should get users organizations', async () => { 170 | await subject(event, stub(), callback); 171 | 172 | assert(getOrgMembershipsStub.calledWithExactly({ 173 | state: 'active', 174 | })); 175 | }); 176 | 177 | it('should only allow get access', async () => { 178 | await subject(event, stub(), callback); 179 | 180 | assert(callback.calledWithExactly(null, { 181 | principalId: 'foo-valid-token', 182 | policyDocument: { 183 | Version: '2012-10-17', 184 | Statement: [ 185 | { 186 | Action: 'execute-api:Invoke', 187 | Effect: 'Allow', 188 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 189 | }, 190 | { 191 | Action: 'execute-api:Invoke', 192 | Effect: 'Deny', 193 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 194 | }, 195 | { 196 | Action: 'execute-api:Invoke', 197 | Effect: 'Deny', 198 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 199 | }, 200 | ], 201 | }, 202 | context: { 203 | username: 'foo-user', 204 | avatar: 'https://example.com', 205 | createdAt: '2001-01-01T00:00:00Z', 206 | updatedAt: '2001-02-01T00:00:00Z', 207 | }, 208 | })); 209 | }); 210 | 211 | afterEach(() => { 212 | subject.__ResetDependency__('GitHub'); 213 | }); 214 | }); 215 | 216 | context('not in restricted org', () => { 217 | let authStub; 218 | let getOrgMembershipsStub; 219 | 220 | beforeEach(() => { 221 | process.env.admins = ''; 222 | process.env.restrictedOrgs = 'foo-org'; 223 | 224 | event = { 225 | authorizationToken: 'Bearer foo-valid-token', 226 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 227 | }; 228 | 229 | gitHubSpy = spy(() => { 230 | gitHubInstance = createStubInstance(GitHub); 231 | authStub = stub(); 232 | getOrgMembershipsStub = stub().returns([]); 233 | 234 | const checkAuthStub = stub().returns({ 235 | user: { 236 | login: 'foo-user', 237 | avatar_url: 'https://example.com', 238 | }, 239 | created_at: '2001-01-01T00:00:00Z', 240 | updated_at: '2001-02-01T00:00:00Z', 241 | }); 242 | 243 | gitHubInstance.authenticate = authStub; 244 | gitHubInstance.authorization = { 245 | check: checkAuthStub, 246 | }; 247 | gitHubInstance.users = { 248 | getOrgMemberships: getOrgMembershipsStub, 249 | }; 250 | 251 | return gitHubInstance; 252 | }); 253 | 254 | subject.__Rewire__({ 255 | GitHub: gitHubSpy, 256 | }); 257 | }); 258 | 259 | it('should get users organizations', async () => { 260 | await subject(event, stub(), callback); 261 | 262 | assert(getOrgMembershipsStub.calledWithExactly({ 263 | state: 'active', 264 | })); 265 | }); 266 | 267 | it('should deny get, put and delete', async () => { 268 | await subject(event, stub(), callback); 269 | 270 | assert(callback.calledWithExactly(null, { 271 | principalId: 'foo-valid-token', 272 | policyDocument: { 273 | Version: '2012-10-17', 274 | Statement: [ 275 | { 276 | Action: 'execute-api:Invoke', 277 | Effect: 'Deny', 278 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 279 | }, 280 | { 281 | Action: 'execute-api:Invoke', 282 | Effect: 'Deny', 283 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 284 | }, 285 | { 286 | Action: 'execute-api:Invoke', 287 | Effect: 'Deny', 288 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 289 | }, 290 | ], 291 | }, 292 | context: { 293 | username: 'foo-user', 294 | avatar: 'https://example.com', 295 | createdAt: '2001-01-01T00:00:00Z', 296 | updatedAt: '2001-02-01T00:00:00Z', 297 | }, 298 | })); 299 | }); 300 | 301 | afterEach(() => { 302 | subject.__ResetDependency__('GitHub'); 303 | }); 304 | }); 305 | 306 | context('not an adminstrator', () => { 307 | let authStub; 308 | let checkAuthStub; 309 | 310 | beforeEach(() => { 311 | process.env.admins = ''; 312 | 313 | event = { 314 | authorizationToken: 'Bearer foo-valid-token', 315 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 316 | }; 317 | 318 | gitHubSpy = spy(() => { 319 | gitHubInstance = createStubInstance(GitHub); 320 | authStub = stub(); 321 | checkAuthStub = stub().returns({ 322 | user: { 323 | login: 'foo-user', 324 | avatar_url: 'https://example.com', 325 | }, 326 | created_at: '2001-01-01T00:00:00Z', 327 | updated_at: '2001-02-01T00:00:00Z', 328 | }); 329 | 330 | gitHubInstance.authenticate = authStub; 331 | gitHubInstance.authorization = { 332 | check: checkAuthStub, 333 | }; 334 | 335 | return gitHubInstance; 336 | }); 337 | 338 | subject.__Rewire__({ 339 | GitHub: gitHubSpy, 340 | }); 341 | }); 342 | 343 | it('should set credentials to authenticate with github api', async () => { 344 | await subject(event, stub(), callback); 345 | 346 | assert(authStub.calledWithExactly({ 347 | type: 'basic', 348 | username: 'foo-client-id', 349 | password: 'bar-secret', 350 | })); 351 | }); 352 | 353 | it('should check token with github', async () => { 354 | await subject(event, stub(), callback); 355 | 356 | assert(checkAuthStub.calledWithExactly({ 357 | client_id: 'foo-client-id', 358 | access_token: 'foo-valid-token', 359 | })); 360 | }); 361 | 362 | it('should only allow get access', async () => { 363 | await subject(event, stub(), callback); 364 | 365 | assert(callback.calledWithExactly(null, { 366 | principalId: 'foo-valid-token', 367 | policyDocument: { 368 | Version: '2012-10-17', 369 | Statement: [ 370 | { 371 | Action: 'execute-api:Invoke', 372 | Effect: 'Allow', 373 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 374 | }, 375 | { 376 | Action: 'execute-api:Invoke', 377 | Effect: 'Deny', 378 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 379 | }, 380 | { 381 | Action: 'execute-api:Invoke', 382 | Effect: 'Deny', 383 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 384 | }, 385 | ], 386 | }, 387 | context: { 388 | username: 'foo-user', 389 | avatar: 'https://example.com', 390 | createdAt: '2001-01-01T00:00:00Z', 391 | updatedAt: '2001-02-01T00:00:00Z', 392 | }, 393 | })); 394 | }); 395 | 396 | afterEach(() => { 397 | subject.__ResetDependency__('GitHub'); 398 | }); 399 | }); 400 | 401 | context('is an adminstrator', () => { 402 | let authStub; 403 | let checkAuthStub; 404 | 405 | beforeEach(() => { 406 | process.env.admins = 'foo-user'; 407 | 408 | event = { 409 | authorizationToken: 'Bearer foo-valid-token', 410 | methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', 411 | }; 412 | 413 | gitHubSpy = spy(() => { 414 | gitHubInstance = createStubInstance(GitHub); 415 | authStub = stub(); 416 | checkAuthStub = stub().returns({ 417 | user: { 418 | login: 'foo-user', 419 | avatar_url: 'https://example.com', 420 | }, 421 | created_at: '2001-01-01T00:00:00Z', 422 | updated_at: '2001-02-01T00:00:00Z', 423 | }); 424 | 425 | gitHubInstance.authenticate = authStub; 426 | gitHubInstance.authorization = { 427 | check: checkAuthStub, 428 | }; 429 | 430 | return gitHubInstance; 431 | }); 432 | 433 | subject.__Rewire__({ 434 | GitHub: gitHubSpy, 435 | }); 436 | }); 437 | 438 | it('should set credentials to authenticate with github api', async () => { 439 | await subject(event, stub(), callback); 440 | 441 | assert(authStub.calledWithExactly({ 442 | type: 'basic', 443 | username: 'foo-client-id', 444 | password: 'bar-secret', 445 | })); 446 | }); 447 | 448 | it('should check token with github', async () => { 449 | await subject(event, stub(), callback); 450 | 451 | assert(checkAuthStub.calledWithExactly({ 452 | client_id: 'foo-client-id', 453 | access_token: 'foo-valid-token', 454 | })); 455 | }); 456 | 457 | it('should allow get, put and delete access', async () => { 458 | await subject(event, stub(), callback); 459 | 460 | assert(callback.calledWithExactly(null, { 461 | principalId: 'foo-valid-token', 462 | policyDocument: { 463 | Version: '2012-10-17', 464 | Statement: [ 465 | { 466 | Action: 'execute-api:Invoke', 467 | Effect: 'Allow', 468 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', 469 | }, 470 | { 471 | Action: 'execute-api:Invoke', 472 | Effect: 'Allow', 473 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', 474 | }, 475 | { 476 | Action: 'execute-api:Invoke', 477 | Effect: 'Allow', 478 | Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', 479 | }, 480 | ], 481 | }, 482 | context: { 483 | username: 'foo-user', 484 | avatar: 'https://example.com', 485 | createdAt: '2001-01-01T00:00:00Z', 486 | updatedAt: '2001-02-01T00:00:00Z', 487 | }, 488 | })); 489 | }); 490 | 491 | afterEach(() => { 492 | subject.__ResetDependency__('GitHub'); 493 | }); 494 | }); 495 | }); 496 | }); 497 | -------------------------------------------------------------------------------- /test/dist-tags/delete.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Storage from '../../src/adapters/s3'; 3 | import Logger from '../../src/adapters/logger'; 4 | import pkg from '../fixtures/package'; 5 | 6 | import subject from '../../src/dist-tags/delete'; 7 | 8 | describe('DELETE registry/-/package/{name}/dist-tags/{tag}', () => { 9 | let event; 10 | let callback; 11 | let storageSpy; 12 | let storageInstance; 13 | let loggerSpy; 14 | let loggerInstance; 15 | 16 | beforeEach(() => { 17 | const env = { 18 | bucket: 'foo-bucket', 19 | region: 'bar-region', 20 | }; 21 | 22 | process.env = env; 23 | 24 | loggerSpy = spy(() => { 25 | loggerInstance = createStubInstance(Logger); 26 | 27 | loggerInstance.info = stub(); 28 | loggerInstance.error = stub(); 29 | 30 | return loggerInstance; 31 | }); 32 | 33 | event = { 34 | requestContext: { 35 | authorizer: { 36 | username: 'foo', 37 | avatar: 'https://example.com', 38 | }, 39 | }, 40 | pathParameters: { 41 | name: 'foo-bar-package', 42 | tag: 'alpha', 43 | }, 44 | }; 45 | 46 | callback = stub(); 47 | 48 | subject.__Rewire__('Logger', loggerSpy); 49 | }); 50 | 51 | describe('dist-tags rm', () => { 52 | context('package exists', () => { 53 | beforeEach(() => { 54 | storageSpy = spy(() => { 55 | storageInstance = createStubInstance(Storage); 56 | 57 | const pkgDistTags = JSON.parse(pkg.withAttachments({ 58 | major: 1, 59 | minor: 0, 60 | patch: 0, 61 | }).toString()); 62 | 63 | pkgDistTags['dist-tags'].alpha = '1.0.0'; 64 | 65 | storageInstance.get.returns( 66 | new Buffer(JSON.stringify(pkgDistTags)), 67 | ); 68 | 69 | return storageInstance; 70 | }); 71 | 72 | subject.__Rewire__('S3', storageSpy); 73 | }); 74 | 75 | it('should get from storage with correct key', async () => { 76 | await subject(event, stub(), callback); 77 | 78 | assert(storageInstance.get.calledWithExactly( 79 | 'foo-bar-package/index.json', 80 | )); 81 | }); 82 | 83 | it('should put updated package json into storage', async () => { 84 | await subject(event, stub(), callback); 85 | 86 | assert(storageInstance.put.calledWithExactly( 87 | 'foo-bar-package/index.json', 88 | pkg.withAttachments({ major: 1, minor: 0, patch: 0 }).toString(), 89 | )); 90 | }); 91 | 92 | it('should return correct updated package json response', async () => { 93 | await subject(event, stub(), callback); 94 | 95 | assert(callback.calledWithExactly(null, { 96 | statusCode: 200, 97 | body: '{"ok":true,"id":"foo-bar-package","dist-tags":{"latest":"1.0.0"}}', 98 | })); 99 | }); 100 | 101 | afterEach(() => { 102 | subject.__ResetDependency__('S3'); 103 | }); 104 | }); 105 | 106 | context('storage put errors', () => { 107 | beforeEach(() => { 108 | storageSpy = spy(() => { 109 | storageInstance = createStubInstance(Storage); 110 | 111 | storageInstance.get.returns(pkg.withoutAttachments({ 112 | major: 1, 113 | minor: 0, 114 | patch: 0, 115 | })); 116 | 117 | storageInstance.put.throws(new Error('Storage error.')); 118 | 119 | return storageInstance; 120 | }); 121 | 122 | subject.__Rewire__('S3', storageSpy); 123 | }); 124 | 125 | it('should return 500 response with error', async () => { 126 | await subject(event, stub(), callback); 127 | 128 | assert(callback.calledWithExactly(null, { 129 | statusCode: 500, 130 | body: '{"ok":false,"error":"Storage error."}', 131 | })); 132 | }); 133 | 134 | afterEach(() => { 135 | subject.__ResetDependency__('S3'); 136 | }); 137 | }); 138 | }); 139 | 140 | afterEach(() => { 141 | subject.__ResetDependency__('Logger'); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/dist-tags/get.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Storage from '../../src/adapters/s3'; 3 | import Logger from '../../src/adapters/logger'; 4 | import pkg from '../fixtures/package'; 5 | 6 | import subject from '../../src/dist-tags/get'; 7 | 8 | describe('GET /registry/-/package/{name}/dist-tags', () => { 9 | let event; 10 | let callback; 11 | let storageSpy; 12 | let storageInstance; 13 | let loggerInstance; 14 | let loggerSpy; 15 | 16 | beforeEach(() => { 17 | const env = { 18 | bucket: 'foo-bucket', 19 | region: 'bar-region', 20 | registry: 'https://example.com', 21 | }; 22 | 23 | process.env = env; 24 | 25 | loggerSpy = spy(() => { 26 | loggerInstance = createStubInstance(Logger); 27 | 28 | loggerInstance.info = stub(); 29 | loggerInstance.error = stub(); 30 | 31 | return loggerInstance; 32 | }); 33 | 34 | event = { 35 | requestContext: { 36 | authorizer: { 37 | username: 'foo', 38 | avatar: 'https://example.com', 39 | }, 40 | }, 41 | pathParameters: { 42 | name: 'foo-bar-package', 43 | }, 44 | }; 45 | 46 | callback = stub(); 47 | 48 | subject.__Rewire__('Logger', loggerSpy); 49 | }); 50 | 51 | describe('dist-tags ls', () => { 52 | context('package exists in private registry', () => { 53 | beforeEach(() => { 54 | storageSpy = spy(() => { 55 | storageInstance = createStubInstance(Storage); 56 | 57 | storageInstance.get.returns(pkg.withAttachments({ 58 | major: 1, 59 | minor: 0, 60 | patch: 0, 61 | })); 62 | 63 | return storageInstance; 64 | }); 65 | 66 | subject.__Rewire__('S3', storageSpy); 67 | }); 68 | 69 | it('should get from storage with correct key', async () => { 70 | await subject(event, stub(), callback); 71 | 72 | assert(storageInstance.get.calledWithExactly( 73 | 'foo-bar-package/index.json', 74 | )); 75 | }); 76 | 77 | it('should return dist tags response', async () => { 78 | await subject(event, stub(), callback); 79 | 80 | assert(callback.calledWithExactly(null, { 81 | statusCode: 200, 82 | body: '{"latest":"1.0.0"}', 83 | })); 84 | }); 85 | 86 | afterEach(() => { 87 | subject.__ResetDependency__('S3'); 88 | }); 89 | }); 90 | 91 | context('package does not exist in private registry or npm', () => { 92 | let npmPackageStub; 93 | 94 | beforeEach(() => { 95 | npmPackageStub = stub().throws(new Error('No package on npm.')); 96 | const mockNpm = { 97 | package: npmPackageStub, 98 | }; 99 | 100 | storageSpy = spy(() => { 101 | storageInstance = createStubInstance(Storage); 102 | 103 | const notFoundError = new Error('No such key.'); 104 | notFoundError.code = 'NoSuchKey'; 105 | 106 | storageInstance.get.throws(notFoundError); 107 | 108 | return storageInstance; 109 | }); 110 | 111 | subject.__Rewire__({ 112 | S3: storageSpy, 113 | npm: mockNpm, 114 | }); 115 | }); 116 | 117 | it('should return correct 404 response', async () => { 118 | await subject(event, stub(), callback); 119 | 120 | assert(callback.calledWithExactly(null, { 121 | statusCode: 404, 122 | body: '{"ok":false,"error":"No package on npm."}', 123 | })); 124 | }); 125 | 126 | afterEach(() => { 127 | subject.__ResetDependency__('npm'); 128 | subject.__ResetDependency__('S3'); 129 | }); 130 | }); 131 | 132 | context('package does not exist in private registry', () => { 133 | let npmPackageStub; 134 | 135 | beforeEach(() => { 136 | npmPackageStub = stub().returns( 137 | JSON.parse(pkg.withoutAttachments({ 138 | major: 1, 139 | minor: 0, 140 | patch: 0, 141 | }).toString())); 142 | 143 | const mockNpm = { 144 | package: npmPackageStub, 145 | }; 146 | 147 | storageSpy = spy(() => { 148 | storageInstance = createStubInstance(Storage); 149 | 150 | const notFoundError = new Error('No such key.'); 151 | notFoundError.code = 'NoSuchKey'; 152 | 153 | storageInstance.get.throws(notFoundError); 154 | 155 | return storageInstance; 156 | }); 157 | 158 | subject.__Rewire__({ 159 | S3: storageSpy, 160 | npm: mockNpm, 161 | }); 162 | }); 163 | 164 | it('should fetch package json from npm', async () => { 165 | await subject(event, stub(), callback); 166 | 167 | assert(npmPackageStub.calledWithExactly( 168 | 'https://example.com', 169 | 'foo-bar-package', 170 | )); 171 | }); 172 | 173 | it('should return dist-tags json response from npm', async () => { 174 | await subject(event, stub(), callback); 175 | 176 | assert(callback.calledWithExactly(null, { 177 | statusCode: 200, 178 | body: '{"latest":"1.0.0"}', 179 | })); 180 | }); 181 | 182 | afterEach(() => { 183 | subject.__ResetDependency__('npm'); 184 | subject.__ResetDependency__('S3'); 185 | }); 186 | }); 187 | 188 | context('storage get errors', () => { 189 | beforeEach(() => { 190 | storageSpy = spy(() => { 191 | storageInstance = createStubInstance(Storage); 192 | 193 | storageInstance.get.throws(new Error('Storage error.')); 194 | 195 | return storageInstance; 196 | }); 197 | 198 | subject.__Rewire__('S3', storageSpy); 199 | }); 200 | 201 | it('should return 500 response with error', async () => { 202 | await subject(event, stub(), callback); 203 | 204 | assert(callback.calledWithExactly(null, { 205 | statusCode: 500, 206 | body: '{"ok":false,"error":"Storage error."}', 207 | })); 208 | }); 209 | 210 | afterEach(() => { 211 | subject.__ResetDependency__('S3'); 212 | }); 213 | }); 214 | }); 215 | 216 | afterEach(() => { 217 | subject.__ResetDependency__('Logger'); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /test/dist-tags/put.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Storage from '../../src/adapters/s3'; 3 | import Logger from '../../src/adapters/logger'; 4 | import pkg from '../fixtures/package'; 5 | 6 | import subject from '../../src/dist-tags/put'; 7 | 8 | describe('PUT registry/-/package/{name}/dist-tags/{tag}', () => { 9 | let event; 10 | let callback; 11 | let storageSpy; 12 | let storageInstance; 13 | let loggerInstance; 14 | let loggerSpy; 15 | 16 | beforeEach(() => { 17 | const env = { 18 | bucket: 'foo-bucket', 19 | region: 'bar-region', 20 | }; 21 | 22 | process.env = env; 23 | 24 | loggerSpy = spy(() => { 25 | loggerInstance = createStubInstance(Logger); 26 | 27 | loggerInstance.info = stub(); 28 | loggerInstance.error = stub(); 29 | 30 | return loggerInstance; 31 | }); 32 | 33 | event = { 34 | requestContext: { 35 | authorizer: { 36 | username: 'foo', 37 | avatar: 'https://example.com', 38 | }, 39 | }, 40 | pathParameters: { 41 | name: 'foo-bar-package', 42 | tag: 'foo', 43 | // npm sends a quoted string for the version 44 | // in the body for this call. 45 | }, 46 | body: '"1.0.0"', 47 | }; 48 | 49 | callback = stub(); 50 | 51 | subject.__Rewire__('Logger', loggerSpy); 52 | }); 53 | 54 | describe('dist-tags add', () => { 55 | context('package exists', () => { 56 | beforeEach(() => { 57 | storageSpy = spy(() => { 58 | storageInstance = createStubInstance(Storage); 59 | 60 | const pkgDistTags = JSON.parse(pkg.withAttachments({ 61 | major: 1, 62 | minor: 0, 63 | patch: 0, 64 | }).toString()); 65 | 66 | storageInstance.get.returns( 67 | new Buffer(JSON.stringify(pkgDistTags)), 68 | ); 69 | 70 | return storageInstance; 71 | }); 72 | 73 | subject.__Rewire__('S3', storageSpy); 74 | }); 75 | 76 | it('should get from storage with correct key', async () => { 77 | await subject(event, stub(), callback); 78 | 79 | assert(storageInstance.get.calledWithExactly( 80 | 'foo-bar-package/index.json', 81 | )); 82 | }); 83 | 84 | it('should put updated package json into storage', async () => { 85 | await subject(event, stub(), callback); 86 | 87 | const pkgInitial = JSON.parse( 88 | pkg.withAttachments({ 89 | major: 1, 90 | minor: 0, 91 | patch: 0, 92 | }).toString(), 93 | ); 94 | 95 | pkgInitial['dist-tags'] = Object.assign( 96 | {}, 97 | pkgInitial['dist-tags'], 98 | { foo: '1.0.0' }, 99 | ); 100 | 101 | const expected = JSON.stringify(pkgInitial); 102 | 103 | assert(storageInstance.put.calledWithExactly( 104 | 'foo-bar-package/index.json', 105 | expected, 106 | )); 107 | }); 108 | 109 | it('should return correct updated package json response', async () => { 110 | await subject(event, stub(), callback); 111 | 112 | assert(callback.calledWithExactly(null, { 113 | statusCode: 200, 114 | body: '{"ok":true,"id":"foo-bar-package","dist-tags":{"latest":"1.0.0","foo":"1.0.0"}}', 115 | })); 116 | }); 117 | 118 | afterEach(() => { 119 | subject.__ResetDependency__('S3'); 120 | }); 121 | }); 122 | 123 | context('storage put errors', () => { 124 | beforeEach(() => { 125 | storageSpy = spy(() => { 126 | storageInstance = createStubInstance(Storage); 127 | 128 | storageInstance.get.returns(pkg.withoutAttachments({ 129 | major: 1, 130 | minor: 0, 131 | patch: 0, 132 | })); 133 | 134 | storageInstance.put.throws(new Error('Storage error.')); 135 | 136 | return storageInstance; 137 | }); 138 | 139 | subject.__Rewire__('S3', storageSpy); 140 | }); 141 | 142 | it('should return 500 response with error', async () => { 143 | await subject(event, stub(), callback); 144 | 145 | assert(callback.calledWithExactly(null, { 146 | statusCode: 500, 147 | body: '{"ok":false,"error":"Storage error."}', 148 | })); 149 | }); 150 | 151 | afterEach(() => { 152 | subject.__ResetDependency__('S3'); 153 | }); 154 | }); 155 | }); 156 | 157 | afterEach(() => { 158 | subject.__ResetDependency__('Logger'); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/fixtures/package.js: -------------------------------------------------------------------------------- 1 | export default { 2 | deprecate: (msg, { 3 | major, 4 | minor, 5 | patch, 6 | }) => new Buffer( 7 | JSON.stringify({ 8 | _id: 'foo-bar-package', 9 | name: 'foo-bar-package', 10 | 'dist-tags': { 11 | latest: `${major}.${minor}.${patch}`, 12 | }, 13 | versions: { 14 | [`${major}.${minor}.${patch}`]: { 15 | name: 'foo-bar-package', 16 | version: `${major}.${minor}.${patch}`, 17 | deprecated: msg, 18 | dist: { 19 | tarball: `https://example.com/prod/registry/foo-bar-package/-/foo-bar-package-${major}.${minor}.${patch}.tgz`, 20 | }, 21 | }, 22 | }, 23 | _attachments: { 24 | [`foo-bar-package-${major}.${minor}.${patch}.tgz`]: { 25 | data: 'foo-package-data', 26 | }, 27 | }, 28 | }), 29 | ), 30 | withAttachments: ({ 31 | major, 32 | minor, 33 | patch, 34 | }) => new Buffer( 35 | JSON.stringify({ 36 | _id: 'foo-bar-package', 37 | name: 'foo-bar-package', 38 | 'dist-tags': { 39 | latest: `${major}.${minor}.${patch}`, 40 | }, 41 | versions: { 42 | [`${major}.${minor}.${patch}`]: { 43 | name: 'foo-bar-package', 44 | version: `${major}.${minor}.${patch}`, 45 | dist: { 46 | tarball: `https://example.com/prod/registry/foo-bar-package/-/foo-bar-package-${major}.${minor}.${patch}.tgz`, 47 | }, 48 | }, 49 | }, 50 | _attachments: { 51 | [`foo-bar-package-${major}.${minor}.${patch}.tgz`]: { 52 | data: 'foo-package-data', 53 | }, 54 | }, 55 | }), 56 | ), 57 | withoutAttachments: ({ 58 | major, 59 | minor, 60 | patch, 61 | }) => new Buffer( 62 | JSON.stringify({ 63 | _id: 'foo-bar-package', 64 | name: 'foo-bar-package', 65 | 'dist-tags': { 66 | latest: `${major}.${minor}.${patch}`, 67 | }, 68 | versions: { 69 | [`${major}.${minor}.${patch}`]: { 70 | name: 'foo-bar-package', 71 | version: `${major}.${minor}.${patch}`, 72 | dist: { 73 | tarball: `https://example.com/prod/registry/foo-bar-package/-/foo-bar-package-${major}.${minor}.${patch}.tgz`, 74 | }, 75 | }, 76 | }, 77 | _attachments: {}, 78 | }), 79 | ), 80 | }; 81 | -------------------------------------------------------------------------------- /test/get/lib.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import pkg from '../fixtures/package'; 3 | 4 | import subject from '../../src/get/lib'; 5 | 6 | describe('GET /registry/{name}', () => { 7 | let event; 8 | let callback; 9 | 10 | beforeEach(() => { 11 | event = { 12 | requestContext: { 13 | authorizer: { 14 | username: 'foo', 15 | avatar: 'https://example.com', 16 | }, 17 | }, 18 | pathParameters: { 19 | name: 'foo-bar-package', 20 | }, 21 | }; 22 | 23 | callback = stub(); 24 | }); 25 | 26 | context('package does not exist in private registry', () => { 27 | let npmStub; 28 | let storageStub; 29 | 30 | beforeEach(() => { 31 | const npmPackageStub = stub().returns( 32 | JSON.parse(pkg.withoutAttachments({ 33 | major: 1, 34 | minor: 0, 35 | patch: 0, 36 | }).toString())); 37 | 38 | npmStub = { 39 | package: npmPackageStub, 40 | }; 41 | 42 | const notFoundError = new Error('Not Found'); 43 | notFoundError.code = 'NoSuchKey'; 44 | 45 | storageStub = { 46 | get: stub().throws(notFoundError), 47 | }; 48 | }); 49 | 50 | it('should fetch package json from npm', async () => { 51 | await subject(event, { 52 | registry: 'https://example.com', 53 | user: stub(), 54 | log: { 55 | error: stub(), 56 | }, 57 | npm: npmStub, 58 | storage: storageStub, 59 | }, callback); 60 | 61 | assert(npmStub.package.calledWithExactly( 62 | 'https://example.com', 63 | 'foo-bar-package', 64 | )); 65 | }); 66 | 67 | it('should return package json response from npm', async () => { 68 | await subject(event, { 69 | registry: 'https://example.com', 70 | user: stub(), 71 | log: { 72 | error: stub(), 73 | info: stub(), 74 | }, 75 | npm: npmStub, 76 | storage: storageStub, 77 | }, callback); 78 | 79 | assert(callback.calledWithExactly(null, { 80 | statusCode: 200, 81 | body: pkg.withoutAttachments({ 82 | major: 1, 83 | minor: 0, 84 | patch: 0, 85 | }).toString(), 86 | })); 87 | }); 88 | }); 89 | 90 | context('package exists in private registry', () => { 91 | let storageStub; 92 | 93 | beforeEach(() => { 94 | const getPackageStub = stub().returns( 95 | pkg.withoutAttachments({ 96 | major: 1, 97 | minor: 0, 98 | patch: 0, 99 | })); 100 | 101 | storageStub = { 102 | get: getPackageStub, 103 | }; 104 | }); 105 | 106 | it('should get package json from storage with correct key', async () => { 107 | await subject(event, { 108 | registry: 'https://example.com', 109 | user: stub(), 110 | log: { 111 | error: stub(), 112 | }, 113 | npm: stub(), 114 | storage: storageStub, 115 | }, callback); 116 | 117 | assert(storageStub.get.calledWithExactly( 118 | 'foo-bar-package/index.json', 119 | )); 120 | }); 121 | 122 | it('should return package json response', async () => { 123 | await subject(event, { 124 | registry: 'https://example.com', 125 | user: stub(), 126 | log: { 127 | error: stub(), 128 | info: stub(), 129 | }, 130 | npm: stub(), 131 | storage: storageStub, 132 | }, callback); 133 | 134 | assert(callback.calledWithExactly(null, { 135 | statusCode: 200, 136 | body: pkg.withoutAttachments({ 137 | major: 1, 138 | minor: 0, 139 | patch: 0, 140 | }).toString(), 141 | })); 142 | }); 143 | }); 144 | 145 | context('npm errors', () => { 146 | let npmStub; 147 | let storageStub; 148 | 149 | beforeEach(() => { 150 | const npmError = new Error('npm Error'); 151 | npmError.status = 500; 152 | 153 | npmStub = { 154 | package: stub().throws(npmError), 155 | }; 156 | 157 | const notFoundError = new Error('Not Found'); 158 | notFoundError.code = 'NoSuchKey'; 159 | 160 | storageStub = { 161 | get: stub().throws(notFoundError), 162 | }; 163 | }); 164 | 165 | it('should return correct status code and response with error', async () => { 166 | await subject(event, { 167 | registry: 'https://example.com', 168 | user: stub(), 169 | log: { 170 | error: stub(), 171 | }, 172 | npm: npmStub, 173 | storage: storageStub, 174 | }, callback); 175 | 176 | assert(callback.calledWithExactly(null, { 177 | statusCode: 500, 178 | body: '{"error":"npm Error"}', 179 | })); 180 | }); 181 | }); 182 | 183 | context('storage get errors', () => { 184 | let storageStub; 185 | 186 | beforeEach(() => { 187 | storageStub = { 188 | get: stub().throws(new Error('Storage Error')), 189 | }; 190 | }); 191 | 192 | it('should return 500 response with error', async () => { 193 | await subject(event, { 194 | registry: 'https://example.com', 195 | user: stub(), 196 | log: { 197 | error: stub(), 198 | }, 199 | npm: stub(), 200 | storage: storageStub, 201 | }, callback); 202 | 203 | assert(callback.calledWithExactly(null, { 204 | statusCode: 500, 205 | body: '{"error":"Storage Error"}', 206 | })); 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /test/globals.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | 4 | global.assert = assert; 5 | global.stub = sinon.stub; 6 | global.spy = sinon.spy; 7 | global.useFakeTimers = sinon.useFakeTimers; 8 | global.createStubInstance = sinon.createStubInstance; 9 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-polyfill 2 | --require ./test/globals.js 3 | --compilers js:babel-register 4 | --recursive 5 | -------------------------------------------------------------------------------- /test/put/deprecate.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import pkg from '../fixtures/package'; 3 | 4 | import subject from '../../src/put/deprecate'; 5 | 6 | describe('PUT /registry/{name}', () => { 7 | let event; 8 | let callback; 9 | let storageStub; 10 | 11 | beforeEach(() => { 12 | const env = { 13 | bucket: 'foo-bucket', 14 | region: 'bar-region', 15 | }; 16 | 17 | process.env = env; 18 | 19 | event = (msg, version) => ({ 20 | requestContext: { 21 | authorizer: { 22 | username: 'foo', 23 | avatar: 'https://example.com', 24 | }, 25 | }, 26 | body: pkg.deprecate(msg, version), 27 | pathParameters: { 28 | name: 'foo-bar-package', 29 | }, 30 | }); 31 | 32 | callback = stub(); 33 | }); 34 | 35 | describe('deprecate', () => { 36 | beforeEach(() => { 37 | storageStub = { 38 | put: stub(), 39 | }; 40 | }); 41 | 42 | it('should store package json', async () => { 43 | await subject( 44 | event('This package is deprecated', { 45 | major: 1, 46 | minor: 0, 47 | patch: 0, 48 | }), { 49 | registry: 'https://example.com', 50 | user: stub(), 51 | log: { 52 | info: stub(), 53 | error: stub(), 54 | }, 55 | npm: stub(), 56 | storage: storageStub, 57 | command: { 58 | name: 'deprecate', 59 | message: 'This package is deprecated', 60 | }, 61 | }, 62 | callback, 63 | ); 64 | 65 | assert(storageStub.put.calledWithExactly( 66 | 'foo-bar-package/index.json', 67 | pkg.deprecate('This package is deprecated', { 68 | major: 1, 69 | minor: 0, 70 | patch: 0, 71 | }).toString(), 72 | )); 73 | }); 74 | 75 | it('should return correct response', async () => { 76 | await subject( 77 | event('This package is deprecated', { 78 | major: 1, 79 | minor: 0, 80 | patch: 0, 81 | }), { 82 | registry: 'https://example.com', 83 | user: stub(), 84 | log: { 85 | info: stub(), 86 | error: stub(), 87 | }, 88 | npm: stub(), 89 | storage: storageStub, 90 | }, 91 | callback, 92 | ); 93 | 94 | assert(callback.calledWithExactly(null, { 95 | statusCode: 200, 96 | body: '{"success":true}', 97 | })); 98 | }); 99 | 100 | context('storage put error', () => { 101 | beforeEach(() => { 102 | storageStub = { 103 | get: stub().returns(pkg.deprecate('This package is deprecated', { 104 | major: 1, 105 | minor: 0, 106 | patch: 0, 107 | })), 108 | put: stub().throws(new Error('Failed to put')), 109 | }; 110 | }); 111 | 112 | it('should return 500 response', async () => { 113 | await subject( 114 | event('This package is deprecated', { 115 | major: 2, 116 | minor: 0, 117 | patch: 0, 118 | }), { 119 | registry: 'https://example.com', 120 | user: stub(), 121 | log: { 122 | error: stub(), 123 | info: stub(), 124 | }, 125 | npm: stub(), 126 | storage: storageStub, 127 | }, 128 | callback, 129 | ); 130 | 131 | assert(callback.calledWithExactly(null, { 132 | statusCode: 500, 133 | body: '{"success":false,"error":"Failed to put"}', 134 | })); 135 | }); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/put/publish.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import pkg from '../fixtures/package'; 3 | 4 | import subject from '../../src/put/publish'; 5 | 6 | describe('PUT /registry/{name}', () => { 7 | let event; 8 | let callback; 9 | let storageStub; 10 | 11 | beforeEach(() => { 12 | const env = { 13 | bucket: 'foo-bucket', 14 | region: 'bar-region', 15 | }; 16 | 17 | process.env = env; 18 | 19 | event = version => ({ 20 | requestContext: { 21 | authorizer: { 22 | username: 'foo', 23 | avatar: 'https://example.com', 24 | }, 25 | }, 26 | body: pkg.withAttachments(version), 27 | pathParameters: { 28 | name: 'foo-bar-package', 29 | }, 30 | }); 31 | 32 | callback = stub(); 33 | }); 34 | 35 | describe('publish', () => { 36 | context('new package', () => { 37 | beforeEach(() => { 38 | const notFoundError = new Error('No such key.'); 39 | notFoundError.code = 'NoSuchKey'; 40 | 41 | storageStub = { 42 | get: stub().throws(notFoundError), 43 | put: stub(), 44 | }; 45 | }); 46 | 47 | it('should attempt to get package json', async () => { 48 | await subject( 49 | event({ 50 | major: 1, 51 | minor: 0, 52 | patch: 0, 53 | }), { 54 | registry: 'https://example.com', 55 | apiEndpoint: 'https://example.com/prod/registry', 56 | user: stub(), 57 | log: { 58 | info: stub(), 59 | error: stub(), 60 | }, 61 | npm: stub(), 62 | storage: storageStub, 63 | }, 64 | callback, 65 | ); 66 | 67 | assert(storageStub.get.calledWithExactly( 68 | 'foo-bar-package/index.json', 69 | )); 70 | }); 71 | 72 | it('should store tar file', async () => { 73 | await subject( 74 | event({ 75 | major: 1, 76 | minor: 0, 77 | patch: 0, 78 | }), { 79 | registry: 'https://example.com', 80 | apiEndpoint: 'https://example.com/prod/registry', 81 | user: stub(), 82 | log: { 83 | error: stub(), 84 | }, 85 | npm: stub(), 86 | storage: storageStub, 87 | }, 88 | callback, 89 | ); 90 | 91 | assert(storageStub.put.calledWithExactly( 92 | 'foo-bar-package/1.0.0.tgz', 93 | 'foo-package-data', 94 | 'base64', 95 | )); 96 | }); 97 | 98 | it('should store package json', async () => { 99 | await subject( 100 | event({ 101 | major: 1, 102 | minor: 0, 103 | patch: 0, 104 | }), { 105 | registry: 'https://example.com', 106 | apiEndpoint: 'https://example.com/prod/registry', 107 | user: stub(), 108 | log: { 109 | info: stub(), 110 | error: stub(), 111 | }, 112 | npm: stub(), 113 | storage: storageStub, 114 | }, 115 | callback, 116 | ); 117 | 118 | assert(storageStub.put.calledWithExactly( 119 | 'foo-bar-package/index.json', 120 | pkg.withAttachments({ 121 | major: 1, 122 | minor: 0, 123 | patch: 0, 124 | }).toString(), 125 | )); 126 | }); 127 | 128 | it('should return correct response', async () => { 129 | await subject( 130 | event({ 131 | major: 1, 132 | minor: 0, 133 | patch: 0, 134 | }), { 135 | registry: 'https://example.com', 136 | apiEndpoint: 'https://example.com/prod/registry', 137 | user: stub(), 138 | log: { 139 | info: stub(), 140 | error: stub(), 141 | }, 142 | npm: stub(), 143 | storage: storageStub, 144 | }, 145 | callback, 146 | ); 147 | 148 | assert(callback.calledWithExactly(null, { 149 | statusCode: 200, 150 | body: '{"success":true}', 151 | })); 152 | }); 153 | }); 154 | 155 | context('existing package', () => { 156 | beforeEach(() => { 157 | storageStub = { 158 | get: stub().returns(pkg.withAttachments({ 159 | major: 1, 160 | minor: 0, 161 | patch: 0, 162 | })), 163 | put: stub(), 164 | }; 165 | }); 166 | 167 | it('should get package json', async () => { 168 | await subject( 169 | event({ 170 | major: 2, 171 | minor: 0, 172 | patch: 0, 173 | }), { 174 | registry: 'https://example.com', 175 | apiEndpoint: 'https://example.com/prod/registry', 176 | user: stub(), 177 | log: { 178 | info: stub(), 179 | error: stub(), 180 | }, 181 | npm: stub(), 182 | storage: storageStub, 183 | }, 184 | callback, 185 | ); 186 | 187 | assert(storageStub.get.calledWithExactly( 188 | 'foo-bar-package/index.json', 189 | )); 190 | }); 191 | 192 | it('should store tar file', async () => { 193 | await subject( 194 | event({ 195 | major: 2, 196 | minor: 0, 197 | patch: 0, 198 | }), { 199 | registry: 'https://example.com', 200 | apiEndpoint: 'https://example.com/prod/registry', 201 | user: stub(), 202 | log: { 203 | error: stub(), 204 | info: stub(), 205 | }, 206 | npm: stub(), 207 | storage: storageStub, 208 | }, 209 | callback, 210 | ); 211 | 212 | assert(storageStub.put.calledWithExactly( 213 | 'foo-bar-package/2.0.0.tgz', 214 | 'foo-package-data', 215 | 'base64', 216 | )); 217 | }); 218 | 219 | it('should store package json', async () => { 220 | await subject( 221 | event({ 222 | major: 2, 223 | minor: 0, 224 | patch: 0, 225 | }), { 226 | registry: 'https://example.com', 227 | apiEndpoint: 'https://example.com/prod/registry', 228 | user: stub(), 229 | log: { 230 | error: stub(), 231 | info: stub(), 232 | }, 233 | npm: stub(), 234 | storage: storageStub, 235 | }, 236 | callback, 237 | ); 238 | 239 | const pkg1 = JSON.parse(pkg.withAttachments({ major: 1, minor: 0, patch: 0 }).toString()); 240 | const pkg2 = JSON.parse(pkg.withAttachments({ major: 2, minor: 0, patch: 0 }).toString()); 241 | 242 | const versions = Object.assign(pkg1.versions, pkg2.versions); 243 | 244 | const updatedPackage = Object.assign({}, pkg1, pkg2); 245 | updatedPackage.versions = versions; 246 | updatedPackage._attachments = pkg2._attachments; 247 | 248 | const expected = JSON.stringify(updatedPackage); 249 | 250 | assert(storageStub.put.calledWithExactly( 251 | 'foo-bar-package/index.json', 252 | expected, 253 | )); 254 | }); 255 | 256 | it('should return correct response', async () => { 257 | await subject( 258 | event({ 259 | major: 2, 260 | minor: 0, 261 | patch: 0, 262 | }), { 263 | registry: 'https://example.com', 264 | apiEndpoint: 'https://example.com/prod/registry', 265 | user: stub(), 266 | log: { 267 | error: stub(), 268 | info: stub(), 269 | }, 270 | npm: stub(), 271 | storage: storageStub, 272 | }, 273 | callback, 274 | ); 275 | 276 | assert(callback.calledWithExactly(null, { 277 | statusCode: 200, 278 | body: '{"success":true}', 279 | })); 280 | }); 281 | }); 282 | 283 | context('package id that exists on npm', () => { 284 | let npmStub; 285 | 286 | beforeEach(() => { 287 | npmStub = { 288 | package: stub().returns( 289 | JSON.parse( 290 | pkg.withAttachments({ 291 | major: 1, 292 | minor: 0, 293 | patch: 0, 294 | }).toString(), 295 | ), 296 | ), 297 | }; 298 | }); 299 | 300 | it('should return 403 informing you require a unqiue package name', async () => { 301 | await subject( 302 | event({ 303 | major: 1, 304 | minor: 0, 305 | patch: 0, 306 | }), { 307 | registry: 'https://example.com', 308 | apiEndpoint: 'https://example.com/prod/registry', 309 | user: stub(), 310 | log: { 311 | error: stub(), 312 | info: stub(), 313 | }, 314 | npm: npmStub, 315 | storage: storageStub, 316 | }, 317 | callback, 318 | ); 319 | 320 | assert(callback.calledWithExactly(null, { 321 | statusCode: 403, 322 | body: '{"success":false,"error":"Your package name needs to be unique to the public npm registry."}', 323 | })); 324 | }); 325 | }); 326 | 327 | context('publishing an existing version', () => { 328 | beforeEach(() => { 329 | storageStub = { 330 | get: stub().returns(pkg.withAttachments({ 331 | major: 1, 332 | minor: 0, 333 | patch: 0, 334 | })), 335 | }; 336 | }); 337 | 338 | it('should return 403 informing you cannot re-publish previous versions', async () => { 339 | await subject( 340 | event({ 341 | major: 1, 342 | minor: 0, 343 | patch: 0, 344 | }), { 345 | registry: 'https://example.com', 346 | apiEndpoint: 'https://example.com/prod/registry', 347 | user: stub(), 348 | log: { 349 | error: stub(), 350 | info: stub(), 351 | }, 352 | npm: stub(), 353 | storage: storageStub, 354 | }, 355 | callback, 356 | ); 357 | 358 | assert(callback.calledWithExactly(null, { 359 | statusCode: 403, 360 | body: '{"success":false,"error":"You cannot publish over the previously published version 1.0.0."}', 361 | })); 362 | }); 363 | }); 364 | 365 | context('storage put error', () => { 366 | beforeEach(() => { 367 | storageStub = { 368 | get: stub().returns(pkg.withAttachments({ 369 | major: 1, 370 | minor: 0, 371 | patch: 0, 372 | })), 373 | put: stub().throws(new Error('Failed to put')), 374 | }; 375 | }); 376 | 377 | it('should return 500 response', async () => { 378 | await subject( 379 | event({ 380 | major: 2, 381 | minor: 0, 382 | patch: 0, 383 | }), { 384 | registry: 'https://example.com', 385 | user: stub(), 386 | log: { 387 | error: stub(), 388 | info: stub(), 389 | }, 390 | npm: stub(), 391 | storage: storageStub, 392 | }, 393 | callback, 394 | ); 395 | 396 | assert(callback.calledWithExactly(null, { 397 | statusCode: 500, 398 | body: '{"success":false,"error":"Failed to put"}', 399 | })); 400 | }); 401 | }); 402 | }); 403 | }); 404 | -------------------------------------------------------------------------------- /test/serverless_plugins/codebox-tools/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import AWS from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies 3 | import CodeboxTools from '../../../.serverless_plugins/codebox-tools'; 4 | 5 | describe('Plugin: CodeboxTools', () => { 6 | const createServerlessStub = (S3, Lambda, log) => ({ 7 | getProvider: () => ({ 8 | sdk: { 9 | S3, 10 | Lambda, 11 | }, 12 | }), 13 | cli: { 14 | log, 15 | }, 16 | config: { 17 | serverless: { 18 | service: { 19 | service: 'foo-service', 20 | }, 21 | }, 22 | }, 23 | service: { 24 | resources: { 25 | Resources: { 26 | PackageStorage: { 27 | Properties: { 28 | BucketName: 'foo-bucket', 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | describe('#index()', () => { 37 | context('has no keys', () => { 38 | let subject; 39 | let serverlessStub; 40 | let serverlessLogStub; 41 | let listObjectsStub; 42 | 43 | beforeEach(() => { 44 | serverlessLogStub = stub(); 45 | serverlessStub = createServerlessStub( 46 | spy(() => { 47 | listObjectsStub = stub().returns({ 48 | promise: () => Promise.resolve({ 49 | IsTruncated: false, 50 | Contents: [], 51 | }), 52 | }); 53 | 54 | const awsS3Instance = createStubInstance(AWS.S3); 55 | awsS3Instance.listObjectsV2 = listObjectsStub; 56 | 57 | return awsS3Instance; 58 | }), stub(), serverlessLogStub); 59 | 60 | subject = new CodeboxTools(serverlessStub, { host: 'bar' }); 61 | }); 62 | 63 | it('should request keys correctly', async () => { 64 | await subject.index(); 65 | 66 | assert(listObjectsStub.calledWithExactly({ 67 | Bucket: 'foo-bucket', 68 | ContinuationToken: undefined, 69 | })); 70 | }); 71 | }); 72 | 73 | context('has keys', () => { 74 | let subject; 75 | let serverlessStub; 76 | let serverlessLogStub; 77 | let listObjectsStub; 78 | let getObjectStub; 79 | let fetchStub; 80 | let clock; 81 | 82 | beforeEach(() => { 83 | clock = useFakeTimers(); 84 | fetchStub = stub(); 85 | serverlessLogStub = stub(); 86 | serverlessStub = createServerlessStub( 87 | spy(() => { 88 | listObjectsStub = stub().returns({ 89 | promise: () => Promise.resolve({ 90 | IsTruncated: false, 91 | Contents: [{ 92 | Key: 'foo/index.json', 93 | }], 94 | }), 95 | }); 96 | 97 | getObjectStub = stub().returns({ 98 | promise: () => Promise.resolve({ 99 | Body: new Buffer('{"dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"foo", "dist":{"tarball":"http://old-host/registry/foo/-/bar-1.0.0.tgz"}}}}'), 100 | }), 101 | }); 102 | 103 | const awsS3Instance = createStubInstance(AWS.S3); 104 | awsS3Instance.listObjectsV2 = listObjectsStub; 105 | awsS3Instance.getObject = getObjectStub; 106 | 107 | return awsS3Instance; 108 | }), stub(), serverlessLogStub); 109 | 110 | subject = new CodeboxTools(serverlessStub, { host: 'example.com' }); 111 | 112 | CodeboxTools.__Rewire__('fetch', fetchStub); 113 | }); 114 | 115 | it('should call index endpoint correctly', async () => { 116 | await subject.index(); 117 | 118 | assert(fetchStub.calledWithExactly('https://log.codebox.sh/v1/send', { 119 | method: 'POST', 120 | headers: { 121 | 'Content-Type': 'application/json', 122 | }, 123 | body: '{"timestamp":"1970-01-01T00:00:00.000Z","namespace":"info:package:put","level":"info","user":{"name":"Codebox","avatar":"https://s3-eu-west-1.amazonaws.com/codebox-assets/logo.png"},"credentials":{},"body":{"name":"foo","dist-tags":{"latest":"1.0.0"}}}', 124 | })); 125 | }); 126 | 127 | afterEach(() => { 128 | clock.restore(); 129 | CodeboxTools.__ResetDependency__('fetch'); 130 | }); 131 | }); 132 | }); 133 | describe('#encrypt()', () => { 134 | context('keys', () => { 135 | let subject; 136 | let serverlessStub; 137 | let serverlessLogStub; 138 | let putObjectStub; 139 | let listObjectsStub; 140 | let getObjectStub; 141 | let mockData; 142 | 143 | beforeEach(() => { 144 | mockData = new Buffer('{"versions":{"1.0.0":{"name":"foo", "dist":{"tarball":"http://old-host/registry/foo/-/bar-1.0.0.tgz"}}}}'); 145 | serverlessLogStub = stub(); 146 | serverlessStub = createServerlessStub( 147 | spy(() => { 148 | putObjectStub = stub().returns({ 149 | promise: () => Promise.resolve(), 150 | }); 151 | 152 | listObjectsStub = stub().returns({ 153 | promise: () => Promise.resolve({ 154 | IsTruncated: false, 155 | Contents: [{ 156 | Key: 'foo/index.json', 157 | }], 158 | }), 159 | }); 160 | 161 | getObjectStub = stub().returns({ 162 | promise: () => Promise.resolve({ 163 | Body: mockData, 164 | }), 165 | }); 166 | 167 | const awsS3Instance = createStubInstance(AWS.S3); 168 | awsS3Instance.listObjectsV2 = listObjectsStub; 169 | awsS3Instance.putObject = putObjectStub; 170 | awsS3Instance.getObject = getObjectStub; 171 | 172 | return awsS3Instance; 173 | }), 174 | stub(), 175 | serverlessLogStub, 176 | ); 177 | 178 | subject = new CodeboxTools(serverlessStub, { 179 | host: 'example.com', 180 | stage: 'test', 181 | path: '/foo', 182 | }); 183 | }); 184 | 185 | it('should store packages encrypted correctly', async () => { 186 | await subject.encrypt(); 187 | 188 | assert(putObjectStub.calledWithExactly({ 189 | Bucket: 'foo-bucket', 190 | Key: 'foo/index.json', 191 | Body: mockData, 192 | ServerSideEncryption: 'AES256', 193 | })); 194 | }); 195 | }); 196 | 197 | context('error', () => { 198 | let subject; 199 | let serverlessStub; 200 | let serverlessLogStub; 201 | let listObjectsStub; 202 | 203 | beforeEach(() => { 204 | serverlessLogStub = stub(); 205 | serverlessStub = createServerlessStub( 206 | spy(() => { 207 | listObjectsStub = stub().returns({ 208 | promise: () => Promise.reject(new Error('Foo')), 209 | }); 210 | 211 | const awsS3Instance = createStubInstance(AWS.S3); 212 | awsS3Instance.listObjectsV2 = listObjectsStub; 213 | 214 | return awsS3Instance; 215 | }), stub(), serverlessLogStub); 216 | 217 | subject = new CodeboxTools(serverlessStub, { host: 'example.com' }); 218 | }); 219 | 220 | it('should log error correctly', async () => { 221 | try { 222 | await subject.encrypt(); 223 | } catch (err) { 224 | assert(serverlessLogStub.calledWithExactly('Failed file encryption migration Foo')); 225 | } 226 | }); 227 | }); 228 | }); 229 | 230 | describe('#migrate()', () => { 231 | context('has no keys', () => { 232 | let subject; 233 | let serverlessStub; 234 | let serverlessLogStub; 235 | let listObjectsStub; 236 | let getFunctionConfigurationStub; 237 | 238 | beforeEach(() => { 239 | serverlessLogStub = stub(); 240 | serverlessStub = createServerlessStub( 241 | spy(() => { 242 | listObjectsStub = stub().returns({ 243 | promise: () => Promise.resolve({ 244 | IsTruncated: false, 245 | Contents: [], 246 | }), 247 | }); 248 | 249 | const awsS3Instance = createStubInstance(AWS.S3); 250 | awsS3Instance.listObjectsV2 = listObjectsStub; 251 | 252 | return awsS3Instance; 253 | }), spy(() => { 254 | getFunctionConfigurationStub = stub().returns({ 255 | promise: () => Promise.resolve({ 256 | Environment: { 257 | Variables: { 258 | apiEndpoint: 'https://example.com/test/registry', 259 | }, 260 | }, 261 | }), 262 | }); 263 | 264 | const updateFunctionConfigurationStub = stub().returns({ 265 | promise: () => Promise.resolve({}), 266 | }); 267 | 268 | const lambdaInstance = createStubInstance(AWS.Lambda); 269 | lambdaInstance.getFunctionConfiguration = getFunctionConfigurationStub; 270 | lambdaInstance.updateFunctionConfiguration = updateFunctionConfigurationStub; 271 | 272 | return lambdaInstance; 273 | }), serverlessLogStub); 274 | 275 | subject = new CodeboxTools(serverlessStub, { host: 'bar', stage: 'test' }); 276 | }); 277 | 278 | it('should request keys correctly', async () => { 279 | await subject.migrate(); 280 | 281 | assert(listObjectsStub.calledWithExactly({ 282 | Bucket: 'foo-bucket', 283 | ContinuationToken: undefined, 284 | })); 285 | }); 286 | }); 287 | 288 | context('has keys', () => { 289 | let subject; 290 | let serverlessStub; 291 | let serverlessLogStub; 292 | let putObjectStub; 293 | let listObjectsStub; 294 | let getObjectStub; 295 | let getFunctionConfigurationStub; 296 | 297 | beforeEach(() => { 298 | serverlessLogStub = stub(); 299 | serverlessStub = createServerlessStub( 300 | spy(() => { 301 | putObjectStub = stub().returns({ 302 | promise: () => Promise.resolve(), 303 | }); 304 | 305 | listObjectsStub = stub().returns({ 306 | promise: () => Promise.resolve({ 307 | IsTruncated: false, 308 | Contents: [{ 309 | Key: 'foo/index.json', 310 | }], 311 | }), 312 | }); 313 | 314 | getObjectStub = stub().returns({ 315 | promise: () => Promise.resolve({ 316 | Body: new Buffer('{"versions":{"1.0.0":{"name":"foo", "dist":{"tarball":"http://old-host/registry/foo/-/bar-1.0.0.tgz"}}}}'), 317 | }), 318 | }); 319 | 320 | const awsS3Instance = createStubInstance(AWS.S3); 321 | awsS3Instance.listObjectsV2 = listObjectsStub; 322 | awsS3Instance.putObject = putObjectStub; 323 | awsS3Instance.getObject = getObjectStub; 324 | 325 | return awsS3Instance; 326 | }), spy(() => { 327 | getFunctionConfigurationStub = stub().returns({ 328 | promise: () => Promise.resolve({ 329 | Environment: { 330 | Variables: { 331 | apiEndpoint: 'https://example.com/test/registry', 332 | }, 333 | }, 334 | }), 335 | }); 336 | 337 | const updateFunctionConfigurationStub = stub().returns({ 338 | promise: () => Promise.resolve({}), 339 | }); 340 | 341 | const lambdaInstance = createStubInstance(AWS.Lambda); 342 | lambdaInstance.getFunctionConfiguration = getFunctionConfigurationStub; 343 | lambdaInstance.updateFunctionConfiguration = updateFunctionConfigurationStub; 344 | 345 | return lambdaInstance; 346 | }), serverlessLogStub); 347 | 348 | subject = new CodeboxTools(serverlessStub, { 349 | host: 'example.com', 350 | stage: 'test', 351 | path: '/foo', 352 | }); 353 | }); 354 | 355 | it('should store updated packages correctly', async () => { 356 | await subject.migrate(); 357 | 358 | assert(putObjectStub.calledWithExactly({ 359 | Bucket: 'foo-bucket', 360 | Key: 'foo/index.json', 361 | Body: '{"versions":{"1.0.0":{"name":"foo","dist":{"tarball":"https://example.com/registry/foo/-/bar-1.0.0.tgz"}}}}', 362 | })); 363 | }); 364 | 365 | it('should request keys correctly', async () => { 366 | await subject.migrate(); 367 | 368 | assert(listObjectsStub.calledWithExactly({ 369 | Bucket: 'foo-bucket', 370 | ContinuationToken: undefined, 371 | })); 372 | }); 373 | }); 374 | 375 | context('error', () => { 376 | let subject; 377 | let serverlessStub; 378 | let serverlessLogStub; 379 | let listObjectsStub; 380 | 381 | beforeEach(() => { 382 | serverlessLogStub = stub(); 383 | serverlessStub = createServerlessStub( 384 | spy(() => { 385 | listObjectsStub = stub().returns({ 386 | promise: () => Promise.reject(new Error('Domain Migration Error')), 387 | }); 388 | 389 | const awsS3Instance = createStubInstance(AWS.S3); 390 | awsS3Instance.listObjectsV2 = listObjectsStub; 391 | 392 | return awsS3Instance; 393 | }), stub(), serverlessLogStub); 394 | 395 | subject = new CodeboxTools(serverlessStub, { host: 'example.com' }); 396 | }); 397 | 398 | it('should log error correctly', async () => { 399 | try { 400 | await subject.migrate(); 401 | } catch (err) { 402 | assert(serverlessLogStub.calledWithExactly('Domain update failed for example.com')); 403 | } 404 | }); 405 | }); 406 | }); 407 | }); 408 | -------------------------------------------------------------------------------- /test/serverless_plugins/remove-storage/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import AWS from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies 3 | import RemoveStorageBucket from '../../../.serverless_plugins/remove-storage'; 4 | 5 | describe('Plugin: RemoveStorageBucket', () => { 6 | const createServerlessStub = ( 7 | SharedIniFileCredentials, 8 | S3, 9 | log, 10 | ) => ({ 11 | config: { 12 | serverless: { 13 | service: { 14 | provider: { 15 | profile: 'foo', 16 | }, 17 | }, 18 | }, 19 | }, 20 | getProvider: () => ({ 21 | sdk: { 22 | S3, 23 | SharedIniFileCredentials, 24 | config: {}, 25 | }, 26 | }), 27 | cli: { 28 | log, 29 | }, 30 | service: { 31 | resources: { 32 | Resources: { 33 | PackageStorage: { 34 | Properties: { 35 | BucketName: 'foo-bucket', 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }); 42 | 43 | describe('#beforeRemove()', () => { 44 | context('has no keys', () => { 45 | let subject; 46 | let serverlessStub; 47 | let serverlessLogStub; 48 | let deleteBucketStub; 49 | let deleteObjectsStub; 50 | let listObjectsStub; 51 | 52 | beforeEach(() => { 53 | serverlessLogStub = stub(); 54 | serverlessStub = createServerlessStub( 55 | stub(), 56 | spy(() => { 57 | deleteBucketStub = stub().returns({ 58 | promise: () => Promise.resolve(), 59 | }); 60 | 61 | listObjectsStub = stub().returns({ 62 | promise: () => Promise.resolve({ 63 | IsTruncated: false, 64 | Contents: [], 65 | }), 66 | }); 67 | 68 | deleteObjectsStub = stub().returns({ 69 | promise: () => Promise.resolve(), 70 | }); 71 | 72 | const awsS3Instance = createStubInstance(AWS.S3); 73 | awsS3Instance.deleteBucket = deleteBucketStub; 74 | awsS3Instance.listObjectsV2 = listObjectsStub; 75 | awsS3Instance.deleteObjects = deleteObjectsStub; 76 | 77 | return awsS3Instance; 78 | }), serverlessLogStub); 79 | 80 | subject = new RemoveStorageBucket(serverlessStub); 81 | }); 82 | 83 | it('should list keys correctly', async () => { 84 | await subject.beforeRemove(); 85 | 86 | assert(listObjectsStub.calledWithExactly({ 87 | Bucket: 'foo-bucket', 88 | ContinuationToken: undefined, 89 | })); 90 | }); 91 | 92 | it('should not call delete objects', async () => { 93 | await subject.beforeRemove(); 94 | 95 | assert(!deleteObjectsStub.called); 96 | }); 97 | }); 98 | 99 | context('has keys', () => { 100 | let subject; 101 | let serverlessStub; 102 | let serverlessLogStub; 103 | let deleteBucketStub; 104 | let deleteObjectsStub; 105 | let listObjectsStub; 106 | let fileCredentialsStub; 107 | 108 | beforeEach(() => { 109 | serverlessLogStub = stub(); 110 | fileCredentialsStub = stub(); 111 | serverlessStub = createServerlessStub( 112 | fileCredentialsStub, 113 | spy(() => { 114 | deleteBucketStub = stub().returns({ 115 | promise: () => Promise.resolve(), 116 | }); 117 | 118 | listObjectsStub = stub().returns({ 119 | promise: () => Promise.resolve({ 120 | IsTruncated: false, 121 | Contents: [{ 122 | Key: 'foo', 123 | }, 124 | { 125 | Key: 'bar', 126 | }], 127 | }), 128 | }); 129 | 130 | deleteObjectsStub = stub().returns({ 131 | promise: () => Promise.resolve(), 132 | }); 133 | 134 | const awsS3Instance = createStubInstance(AWS.S3); 135 | awsS3Instance.deleteBucket = deleteBucketStub; 136 | awsS3Instance.listObjectsV2 = listObjectsStub; 137 | awsS3Instance.deleteObjects = deleteObjectsStub; 138 | 139 | return awsS3Instance; 140 | }), serverlessLogStub); 141 | 142 | subject = new RemoveStorageBucket(serverlessStub); 143 | }); 144 | 145 | it('should set credentials correctly', async () => { 146 | await subject.beforeRemove(); 147 | 148 | assert(fileCredentialsStub.calledWithExactly({ 149 | profile: 'foo', 150 | })); 151 | }); 152 | 153 | it('should list keys correctly', async () => { 154 | await subject.beforeRemove(); 155 | 156 | assert(listObjectsStub.calledWithExactly({ 157 | Bucket: 'foo-bucket', 158 | ContinuationToken: undefined, 159 | })); 160 | }); 161 | 162 | it('should delete objects correctly', async () => { 163 | await subject.beforeRemove(); 164 | 165 | assert(deleteObjectsStub.calledWithExactly({ 166 | Bucket: 'foo-bucket', 167 | Delete: { 168 | Objects: [{ 169 | Key: 'foo', 170 | }, 171 | { 172 | Key: 'bar', 173 | }], 174 | }, 175 | })); 176 | }); 177 | 178 | it('should call aws delete bucket correctly', async () => { 179 | await subject.beforeRemove(); 180 | 181 | assert(deleteBucketStub.calledWithExactly({ 182 | Bucket: 'foo-bucket', 183 | })); 184 | }); 185 | 186 | it('should log it was a success', async () => { 187 | await subject.beforeRemove(); 188 | 189 | assert(serverlessLogStub.calledWithExactly('AWS Package Storage Removed')); 190 | }); 191 | }); 192 | 193 | context('error', () => { 194 | let subject; 195 | let serverlessStub; 196 | let serverlessLogStub; 197 | let listObjectsStub; 198 | 199 | beforeEach(() => { 200 | serverlessLogStub = stub(); 201 | serverlessStub = createServerlessStub( 202 | stub(), 203 | spy(() => { 204 | listObjectsStub = stub().returns({ 205 | promise: () => Promise.reject(new Error('Removal Error')), 206 | }); 207 | 208 | const awsS3Instance = createStubInstance(AWS.S3); 209 | awsS3Instance.listObjectsV2 = listObjectsStub; 210 | 211 | return awsS3Instance; 212 | }), serverlessLogStub); 213 | 214 | subject = new RemoveStorageBucket(serverlessStub); 215 | }); 216 | 217 | it('should log error correctly', async () => { 218 | try { 219 | await subject.beforeRemove(); 220 | } catch (err) { 221 | assert(serverlessLogStub.calledWithExactly('Could not remove AWS package storage: Removal Error')); 222 | } 223 | }); 224 | }); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /test/tar/get.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Storage from '../../src/adapters/s3'; 3 | import Logger from '../../src/adapters/logger'; 4 | import subject from '../../src/tar/get'; 5 | 6 | describe('GET /registry/{name}/-/{tar}', () => { 7 | let event; 8 | let callback; 9 | let storageSpy; 10 | let storageInstance; 11 | let loggerInstance; 12 | let loggerSpy; 13 | 14 | beforeEach(() => { 15 | const env = { 16 | bucket: 'foo-bucket', 17 | region: 'bar-region', 18 | registry: 'https://example.com', 19 | }; 20 | 21 | process.env = env; 22 | 23 | loggerSpy = spy(() => { 24 | loggerInstance = createStubInstance(Logger); 25 | 26 | loggerInstance.info = stub(); 27 | loggerInstance.error = stub(); 28 | 29 | return loggerInstance; 30 | }); 31 | 32 | event = { 33 | name: 'foo-bar-package', 34 | tar: 'foo-bar-package-1.0.0.tgz', 35 | }; 36 | 37 | callback = stub(); 38 | 39 | subject.__Rewire__('Logger', loggerSpy); 40 | }); 41 | 42 | context('tar exists in private registry', () => { 43 | beforeEach(() => { 44 | storageSpy = spy(() => { 45 | storageInstance = createStubInstance(Storage); 46 | 47 | storageInstance.get.returns(new Buffer('bar')); 48 | 49 | return storageInstance; 50 | }); 51 | 52 | subject.__Rewire__('S3', storageSpy); 53 | }); 54 | 55 | it('should get package json from storage with correct key', async () => { 56 | await subject(event, stub(), callback); 57 | 58 | assert(storageInstance.get.calledWithExactly( 59 | 'foo-bar-package/1.0.0.tgz', 60 | )); 61 | }); 62 | 63 | it('should return base64 response', async () => { 64 | await subject(event, stub(), callback); 65 | 66 | assert(callback.calledWithExactly( 67 | null, 68 | 'YmFy', 69 | )); 70 | }); 71 | 72 | afterEach(() => { 73 | subject.__ResetDependency__('S3'); 74 | }); 75 | }); 76 | 77 | context('tar does not exist in private registry', () => { 78 | let npmTarStub; 79 | 80 | beforeEach(() => { 81 | npmTarStub = stub().returns( 82 | new Buffer('YmFy', 'base64'), 83 | ); 84 | 85 | const mockNpm = { 86 | tar: npmTarStub, 87 | }; 88 | 89 | storageSpy = spy(() => { 90 | storageInstance = createStubInstance(Storage); 91 | 92 | const notFoundError = new Error('No such key.'); 93 | notFoundError.code = 'NoSuchKey'; 94 | 95 | storageInstance.get.throws(notFoundError); 96 | 97 | return storageInstance; 98 | }); 99 | 100 | subject.__Rewire__({ 101 | S3: storageSpy, 102 | npm: mockNpm, 103 | }); 104 | }); 105 | 106 | it('should fetch package json from npm', async () => { 107 | await subject(event, stub(), callback); 108 | 109 | assert(npmTarStub.calledWithExactly( 110 | 'https://example.com', 111 | 'foo-bar-package/-/foo-bar-package-1.0.0.tgz', 112 | )); 113 | }); 114 | 115 | it('should return base64 response', async () => { 116 | await subject(event, stub(), callback); 117 | 118 | assert(callback.calledWithExactly( 119 | null, 120 | 'YmFy', 121 | )); 122 | }); 123 | 124 | afterEach(() => { 125 | subject.__ResetDependency__('npm'); 126 | subject.__ResetDependency__('S3'); 127 | }); 128 | }); 129 | 130 | context('storage get errors', () => { 131 | beforeEach(() => { 132 | storageSpy = spy(() => { 133 | storageInstance = createStubInstance(Storage); 134 | 135 | storageInstance.get.throws(new Error('Storage error.')); 136 | 137 | return storageInstance; 138 | }); 139 | 140 | subject.__Rewire__('S3', storageSpy); 141 | }); 142 | 143 | it('should return 500 response with error', async () => { 144 | await subject(event, stub(), callback); 145 | 146 | assert(callback.calledWithExactly( 147 | new Error('Storage error.'), 148 | )); 149 | }); 150 | 151 | afterEach(() => { 152 | subject.__ResetDependency__('S3'); 153 | }); 154 | }); 155 | 156 | afterEach(() => { 157 | subject.__ResetDependency__('Logger'); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/user/delete.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import GitHub from '@octokit/rest'; 3 | import subject from '../../src/user/delete'; 4 | 5 | describe('DELETE /registry/-/user/token/{token}', () => { 6 | let event; 7 | let callback; 8 | let gitHubSpy; 9 | let gitHubInstance; 10 | 11 | beforeEach(() => { 12 | const env = { 13 | githubClientId: 'foo-client-id', 14 | githubSecret: 'bar-secret', 15 | githubUrl: 'https://example.com', 16 | restrictedOrgs: 'foo-org', 17 | }; 18 | 19 | process.env = env; 20 | 21 | callback = stub(); 22 | }); 23 | 24 | describe('logout', () => { 25 | context('with valid token', () => { 26 | let authStub; 27 | let resetAuthStub; 28 | 29 | beforeEach(() => { 30 | event = { 31 | pathParameters: { 32 | token: 'foo-token', 33 | }, 34 | }; 35 | 36 | gitHubSpy = spy(() => { 37 | gitHubInstance = createStubInstance(GitHub); 38 | authStub = stub(); 39 | resetAuthStub = stub(); 40 | 41 | gitHubInstance.authenticate = authStub; 42 | gitHubInstance.authorization = { 43 | reset: resetAuthStub, 44 | }; 45 | 46 | return gitHubInstance; 47 | }); 48 | 49 | subject.__Rewire__({ 50 | GitHub: gitHubSpy, 51 | }); 52 | }); 53 | 54 | it('should authenticate using app credentials with github', async () => { 55 | await subject(event, stub(), callback); 56 | 57 | assert(authStub.calledWithExactly({ 58 | type: 'basic', 59 | username: 'foo-client-id', 60 | password: 'bar-secret', 61 | })); 62 | }); 63 | 64 | it('should reset token with github', async () => { 65 | await subject(event, stub(), callback); 66 | 67 | assert(resetAuthStub.calledWithExactly({ 68 | client_id: 'foo-client-id', 69 | access_token: 'foo-token', 70 | })); 71 | }); 72 | 73 | it('should return 200 response', async () => { 74 | await subject(event, stub(), callback); 75 | 76 | assert(callback.calledWithExactly(null, { 77 | statusCode: 200, 78 | body: '{"ok":true}', 79 | })); 80 | }); 81 | 82 | afterEach(() => { 83 | subject.__ResetDependency__('GitHub'); 84 | }); 85 | }); 86 | 87 | context('with invalid token', () => { 88 | let authStub; 89 | let resetAuthStub; 90 | 91 | beforeEach(() => { 92 | event = { 93 | pathParameters: { 94 | token: 'foo-bad-token', 95 | }, 96 | }; 97 | 98 | gitHubSpy = spy(() => { 99 | gitHubInstance = createStubInstance(GitHub); 100 | authStub = stub(); 101 | resetAuthStub = stub().throws(new Error('Invalid token')); 102 | 103 | gitHubInstance.authenticate = authStub; 104 | gitHubInstance.authorization = { 105 | reset: resetAuthStub, 106 | }; 107 | 108 | return gitHubInstance; 109 | }); 110 | 111 | subject.__Rewire__({ 112 | GitHub: gitHubSpy, 113 | }); 114 | }); 115 | 116 | it('should authenticate using app credentials with github', async () => { 117 | await subject(event, stub(), callback); 118 | 119 | assert(authStub.calledWithExactly({ 120 | type: 'basic', 121 | username: 'foo-client-id', 122 | password: 'bar-secret', 123 | })); 124 | }); 125 | 126 | it('should return a 500 error', async () => { 127 | await subject(event, stub(), callback); 128 | 129 | assert(callback.calledWithExactly(null, { 130 | statusCode: 500, 131 | body: '{"ok":false,"error":"Invalid token"}', 132 | })); 133 | }); 134 | 135 | afterEach(() => { 136 | subject.__ResetDependency__('GitHub'); 137 | }); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/user/put.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import GitHub from '@octokit/rest'; 3 | import subject from '../../src/user/put'; 4 | 5 | describe('PUT /registry/-/user/{id}', () => { 6 | let event; 7 | let callback; 8 | let gitHubSpy; 9 | let gitHubInstance; 10 | 11 | beforeEach(() => { 12 | const env = { 13 | githubClientId: 'foo-client-id', 14 | githubSecret: 'bar-secret', 15 | githubUrl: 'https://example.com', 16 | restrictedOrgs: 'foo-org', 17 | }; 18 | 19 | process.env = env; 20 | 21 | callback = stub(); 22 | }); 23 | 24 | describe('login', () => { 25 | context('with 2FA', () => { 26 | let authStub; 27 | let getCreateAuthStub; 28 | 29 | beforeEach(() => { 30 | event = { 31 | pathParameters: { 32 | id: 'foo-user.123456', 33 | }, 34 | body: '{"name":"foo-user.123456","password":"bar-password"}', 35 | }; 36 | 37 | gitHubSpy = spy(() => { 38 | gitHubInstance = createStubInstance(GitHub); 39 | getCreateAuthStub = stub().returns({ token: 'foo-token' }); 40 | authStub = stub(); 41 | 42 | gitHubInstance.authenticate = authStub; 43 | gitHubInstance.authorization = { 44 | getOrCreateAuthorizationForApp: getCreateAuthStub, 45 | }; 46 | 47 | return gitHubInstance; 48 | }); 49 | 50 | subject.__Rewire__({ 51 | GitHub: gitHubSpy, 52 | }); 53 | }); 54 | 55 | it('should set credentials to authenticate with github api', async () => { 56 | await subject(event, stub(), callback); 57 | 58 | assert(authStub.calledWithExactly({ 59 | type: 'basic', 60 | username: 'foo-user', 61 | password: 'bar-password', 62 | })); 63 | }); 64 | 65 | it('should get or create authorization for app correctly with otp', async () => { 66 | await subject(event, stub(), callback); 67 | 68 | assert(getCreateAuthStub.calledWithExactly({ 69 | scopes: ['user:email', 'read:org'], 70 | client_id: 'foo-client-id', 71 | client_secret: 'bar-secret', 72 | note: 'codebox private npm registry', 73 | headers: { 74 | 'X-GitHub-OTP': '123456', 75 | }, 76 | })); 77 | }); 78 | 79 | afterEach(() => { 80 | subject.__ResetDependency__('GitHub'); 81 | }); 82 | }); 83 | 84 | context('first time without 2FA', () => { 85 | let authStub; 86 | let getCreateAuthStub; 87 | 88 | beforeEach(() => { 89 | event = { 90 | pathParameters: { 91 | id: 'foo-user', 92 | }, 93 | body: '{"name":"foo-user","password":"bar-password"}', 94 | }; 95 | 96 | gitHubSpy = spy(() => { 97 | gitHubInstance = createStubInstance(GitHub); 98 | getCreateAuthStub = stub().returns({ token: 'foo-token' }); 99 | authStub = stub(); 100 | 101 | gitHubInstance.authenticate = authStub; 102 | gitHubInstance.authorization = { 103 | getOrCreateAuthorizationForApp: getCreateAuthStub, 104 | }; 105 | 106 | return gitHubInstance; 107 | }); 108 | 109 | subject.__Rewire__({ 110 | GitHub: gitHubSpy, 111 | }); 112 | }); 113 | 114 | it('should set credentials to authenticate with github api', async () => { 115 | await subject(event, stub(), callback); 116 | 117 | assert(authStub.calledWithExactly({ 118 | type: 'basic', 119 | username: 'foo-user', 120 | password: 'bar-password', 121 | })); 122 | }); 123 | 124 | it('should get or create authorization for app correctly', async () => { 125 | await subject(event, stub(), callback); 126 | 127 | assert(getCreateAuthStub.calledWithExactly({ 128 | scopes: ['user:email', 'read:org'], 129 | client_id: 'foo-client-id', 130 | client_secret: 'bar-secret', 131 | note: 'codebox private npm registry', 132 | headers: { 133 | 'X-GitHub-OTP': '', 134 | }, 135 | })); 136 | }); 137 | 138 | it('should return correct status code and token response', async () => { 139 | await subject(event, stub(), callback); 140 | 141 | assert(callback.calledWithExactly(null, { 142 | statusCode: 201, 143 | body: '{"ok":true,"token":"foo-token"}', 144 | })); 145 | }); 146 | 147 | afterEach(() => { 148 | subject.__ResetDependency__('GitHub'); 149 | }); 150 | }); 151 | 152 | context('logged in previously without 2FA', () => { 153 | let authStub; 154 | let getCreateAuthStub; 155 | let createAuthStub; 156 | let deleteAuthStub; 157 | 158 | beforeEach(() => { 159 | event = { 160 | pathParameters: { 161 | id: 'foo-user', 162 | }, 163 | body: '{"name":"foo-user","password":"bar-password"}', 164 | }; 165 | 166 | gitHubSpy = spy(() => { 167 | gitHubInstance = createStubInstance(GitHub); 168 | 169 | // GitHub does not return a token 170 | // if you already have one assigned 171 | getCreateAuthStub = stub().returns({ id: 'foo-user', token: '' }); 172 | 173 | authStub = stub(); 174 | deleteAuthStub = stub(); 175 | createAuthStub = stub().returns({ id: 'foo-user', token: 'new-foo-token' }); 176 | 177 | gitHubInstance.authenticate = authStub; 178 | gitHubInstance.authorization = { 179 | getOrCreateAuthorizationForApp: getCreateAuthStub, 180 | delete: deleteAuthStub, 181 | create: createAuthStub, 182 | }; 183 | 184 | return gitHubInstance; 185 | }); 186 | 187 | subject.__Rewire__({ 188 | GitHub: gitHubSpy, 189 | }); 190 | }); 191 | 192 | it('should set credentials to authenticate with github api', async () => { 193 | await subject(event, stub(), callback); 194 | 195 | assert(authStub.calledWithExactly({ 196 | type: 'basic', 197 | username: 'foo-user', 198 | password: 'bar-password', 199 | })); 200 | }); 201 | 202 | it('should delete current token from github', async () => { 203 | await subject(event, stub(), callback); 204 | 205 | assert(deleteAuthStub.calledWithExactly({ 206 | id: 'foo-user', 207 | headers: { 208 | 'X-GitHub-OTP': '', 209 | }, 210 | })); 211 | }); 212 | 213 | it('should create a new token against github app', async () => { 214 | await subject(event, stub(), callback); 215 | 216 | assert(createAuthStub.calledWithExactly({ 217 | scopes: ['user:email', 'read:org'], 218 | client_id: 'foo-client-id', 219 | client_secret: 'bar-secret', 220 | note: 'codebox private npm registry', 221 | headers: { 222 | 'X-GitHub-OTP': '', 223 | }, 224 | })); 225 | }); 226 | 227 | it('should attempts to get or create authorization for app correctly', async () => { 228 | await subject(event, stub(), callback); 229 | 230 | assert(getCreateAuthStub.calledWithExactly({ 231 | scopes: ['user:email', 'read:org'], 232 | client_id: 'foo-client-id', 233 | client_secret: 'bar-secret', 234 | note: 'codebox private npm registry', 235 | headers: { 236 | 'X-GitHub-OTP': '', 237 | }, 238 | })); 239 | }); 240 | 241 | it('should return correct status code and token response', async () => { 242 | await subject(event, stub(), callback); 243 | 244 | assert(callback.calledWithExactly(null, { 245 | statusCode: 201, 246 | body: '{"ok":true,"token":"new-foo-token"}', 247 | })); 248 | }); 249 | 250 | afterEach(() => { 251 | subject.__ResetDependency__('GitHub'); 252 | }); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /test/whoami/get.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import subject from '../../src/whoami/get'; 3 | 4 | describe('GET /registry/-/whoami', () => { 5 | let event; 6 | let callback; 7 | 8 | beforeEach(() => { 9 | callback = stub(); 10 | }); 11 | 12 | describe('whoami', () => { 13 | beforeEach(() => { 14 | event = { 15 | requestContext: { 16 | authorizer: { 17 | username: 'foobar', 18 | }, 19 | }, 20 | }; 21 | }); 22 | 23 | it('should return correct username', async () => { 24 | await subject(event, stub(), callback); 25 | 26 | assert(callback.calledWithExactly(null, { 27 | statusCode: 200, 28 | body: '{"username":"foobar"}', 29 | })); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | target: 'node', 6 | externals: [nodeExternals()], 7 | entry: { 8 | authorizerGithub: ['./bootstrap', './src/authorizers/github.js'], 9 | put: ['./bootstrap', './src/put/index.js'], 10 | get: ['./bootstrap', './src/get/index.js'], 11 | distTagsGet: ['./bootstrap', './src/dist-tags/get.js'], 12 | distTagsPut: ['./bootstrap', './src/dist-tags/put.js'], 13 | distTagsDelete: ['./bootstrap', './src/dist-tags/delete.js'], 14 | userPut: ['./bootstrap', './src/user/put.js'], 15 | userDelete: ['./bootstrap', './src/user/delete.js'], 16 | whoamiGet: ['./bootstrap', './src/whoami/get.js'], 17 | tarGet: ['./bootstrap', './src/tar/get.js'], 18 | }, 19 | output: { 20 | libraryTarget: 'commonjs', 21 | path: path.join(__dirname, '.webpack'), 22 | filename: '[name].js', 23 | }, 24 | module: { 25 | loaders: [{ 26 | test: /\.js$/, 27 | loader: 'babel', 28 | include: /src/, 29 | exclude: /node_modules/, 30 | }, 31 | { 32 | test: /\.json$/, 33 | loader: 'json-loader', 34 | }], 35 | }, 36 | }; 37 | --------------------------------------------------------------------------------