├── .gitignore ├── README.md ├── functions └── npm │ ├── lib │ └── Storage.js │ ├── package.json │ ├── package │ ├── event.json │ ├── handler.js │ └── s-function.json │ ├── ping │ ├── event.json │ ├── handler.js │ └── s-function.json │ └── user │ ├── event.json │ ├── handler.js │ └── s-function.json ├── package.json ├── s-project.json ├── s-resources-cf.json └── s-templates.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | node_modules 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | dist 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 32 | node_modules 33 | 34 | #IDE 35 | **/.idea 36 | 37 | #OS 38 | .DS_Store 39 | .tmp 40 | 41 | #SERVERLESS 42 | admin.env 43 | .env 44 | 45 | #Ignore _meta folder 46 | _meta 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless NPM Registry 2 | 3 | _Will not be continuing, nor maintaning this project._ 4 | 5 | NPM Registry server implemented with [Serverless](http://serverless.com/) framework. Sole purpose of this experiment was to explore the capabilities of Serverless framework. This project is working with limited functionality, but everything here is working like the official registry. 6 | 7 | ### Missing features 8 | - Teams 9 | - Package deprecation 10 | - Test units 11 | -------------------------------------------------------------------------------- /functions/npm/lib/Storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var Crypto = require('crypto'); 5 | var Path = require('path'); 6 | 7 | module.exports = function () { 8 | 9 | var DynamoDB = new AWS.DynamoDB({ 10 | apiVersion: '2012-08-10', 11 | region: process.env.AWS_REGION 12 | }); 13 | 14 | var S3 = new AWS.S3({ 15 | apiVersion: '2006-03-01', 16 | region: process.env.AWS_REGION 17 | }); 18 | 19 | function encryptString (string) { 20 | var cipher = Crypto.createCipher('aes192', process.env.NPM_SECRET); 21 | return cipher.update(string, 'utf8', 'hex') + cipher.final('hex'); 22 | } 23 | 24 | function decryptString (string) { 25 | var decipher = Crypto.createDecipher('aes192', process.env.NPM_SECRET); 26 | return decipher.update(string, 'hex', 'utf8') + decipher.final('utf8'); 27 | } 28 | 29 | class User { 30 | 31 | constructor (data) { 32 | Object.assign(this, { 33 | name: '', 34 | password: '', 35 | expire: '', 36 | permission: [], 37 | access: [], 38 | owner: [] 39 | }, data); 40 | } 41 | 42 | get token () { 43 | return encryptString(`${this.name}:${this.password}:${new Date().getTime()}`); 44 | } 45 | 46 | matchPassword (password) { 47 | return encryptString(password) == this.password; 48 | } 49 | 50 | canRead (pkg) { 51 | // TODO is expired ? 52 | return this.access.indexOf(pkg) > -1 || this.canWrite(pkg); 53 | } 54 | 55 | canWrite (pkg) { 56 | // TODO is expired ? 57 | return this.canPerform('publish') && this.owner.indexOf(pkg) > -1; 58 | } 59 | 60 | canPerform (action) { 61 | // TODO is expired ? 62 | return this.permission.indexOf(action) > -1; 63 | } 64 | 65 | grantRead (pkg) { 66 | if (!this.canRead(pkg)) { 67 | this.access.push(pkg); 68 | } 69 | return this.update(); 70 | } 71 | 72 | grantWrite (pkg) { 73 | if (!this.canWrite(pkg)) { 74 | this.owner.push(pkg); 75 | } 76 | return this.update(); 77 | } 78 | 79 | update () { 80 | return new Promise((resolve, reject) => { 81 | DynamoDB.updateItem({ 82 | TableName: process.env.NPM_USER_TABLE, 83 | Key: { 84 | name: { 85 | S: this.name 86 | } 87 | }, 88 | UpdateExpression: 'SET #password = :password, #expire = :expire, #permission = :permission, #access = :access, #owner = :owner', 89 | ExpressionAttributeNames: { 90 | '#password': 'password', 91 | '#expire': 'expire', 92 | '#permission': 'permission', 93 | '#access': 'access', 94 | '#owner': 'owner' 95 | }, 96 | ExpressionAttributeValues: { 97 | ':password': {S: this.password}, 98 | ':expire': {S: this.expire}, 99 | ':permission': {L: this.permission.map(pkg => { return {S: pkg}; })}, 100 | ':access': {L: this.access.map(pkg => { return {S: pkg}; })}, 101 | ':owner': {L: this.owner.map(pkg => { return {S: pkg}; })} 102 | } 103 | }, (err, data) => { 104 | if (err) { 105 | return reject(err); 106 | } 107 | return resolve(this); 108 | }); 109 | }); 110 | } 111 | 112 | static fetchByToken (token) { 113 | try { 114 | token = token.match(/^(Bearer) (.*)$/)[2]; 115 | var decryptedToken = decryptString(token).split(':'); 116 | var name = decryptedToken[0]; 117 | var pass = decryptedToken[1]; 118 | return this.fetchByName(name).then(user => { 119 | if (user.password != pass) { 120 | throw new Error('Invalid token.'); 121 | } 122 | return user; 123 | }); 124 | } catch (e) { 125 | return Promise.reject(e); 126 | } 127 | } 128 | 129 | static fetchByName (name) { 130 | return new Promise((resolve, reject) => { 131 | DynamoDB.getItem({ 132 | TableName: process.env.NPM_USER_TABLE, 133 | Key: { 134 | name: { 135 | S: name 136 | } 137 | } 138 | }, (err, data) => { 139 | if (err || !data || typeof data.Item === 'undefined') { 140 | return reject(new Error(`User "${name}" not found.`)); 141 | } 142 | return resolve(new User({ 143 | name: data.Item.name.S || '', 144 | password: data.Item.password.S || '', 145 | email: data.Item.email.S, 146 | expire: data.Item.expire ? data.Item.expire.S : '', 147 | permission: data.Item.permission ? data.Item.permission.L.map(pkg => pkg.S) : [], 148 | access: data.Item.access ? data.Item.access.L.map(pkg => pkg.S) : [], 149 | owner: data.Item.owner ? data.Item.owner.L.map(pkg => pkg.S) : [] 150 | })); 151 | }); 152 | }); 153 | } 154 | 155 | static create (info) { 156 | info = Object.assign({}, { 157 | name: '', 158 | password: '', 159 | expire: '', 160 | permission: [], 161 | access: [], 162 | owner: [] 163 | }, info); 164 | 165 | info.password = encryptString(info.password); 166 | 167 | return new Promise((resolve, reject) => { 168 | DynamoDB.putItem({ 169 | TableName: process.env.NPM_USER_TABLE, 170 | Item: { 171 | name: {S: info.name}, 172 | password: {S: info.password}, 173 | email: {S: info.email}, 174 | expire: {S: (info.expire || 'never')}, 175 | permission: {L: (info.permission || []).map(pkg => { return {S: pkg}; })}, 176 | access: {L: (info.access || []).map(pkg => { return {S: pkg}; })}, 177 | owner: {L: (info.owner || []).map(pkg => { return {S: pkg}; })} 178 | } 179 | }, (err, data) => { 180 | if (err) { 181 | return reject(err); 182 | } 183 | return resolve(new User(info)); 184 | }); 185 | }); 186 | } 187 | } 188 | 189 | class Package { 190 | constructor (data) { 191 | Object.assign(this, { 192 | name: '', 193 | info: '' 194 | }, data); 195 | 196 | try { 197 | this.info = JSON.parse(this.info); 198 | } catch (e) { 199 | this.info = {}; 200 | } 201 | } 202 | 203 | unpublish (revision) { 204 | var tasks = []; 205 | Object.keys(this.info.versions).forEach(ver => { 206 | if (revision && revision != ver) { 207 | return; 208 | } 209 | 210 | var meta = this.info.versions[ver]; 211 | 212 | if (meta.dist) { 213 | tasks.push(new Promise((resolve, reject) => { 214 | var file = Path.basename(meta.dist.tarball); 215 | S3.deleteObject({ 216 | Bucket: process.env.NPM_PACKAGE_BUCKET, 217 | Key: file 218 | }, (err, data) => { 219 | if (err) { 220 | return reject(err); 221 | } 222 | return resolve(); 223 | }) 224 | })); 225 | } 226 | }); 227 | 228 | if (revision) { 229 | delete this.info.versions[revision]; 230 | tasks.push(this.update()); 231 | } else { 232 | tasks.push(new Promise((resolve, reject) => { 233 | DynamoDB.deleteItem({ 234 | TableName: process.env.NPM_PACKAGE_TABLE, 235 | Key: { 236 | name: {S: this.name} 237 | } 238 | }, (err, data) => { 239 | if (err) { 240 | return reject(err); 241 | } 242 | resolve(this); 243 | }) 244 | })); 245 | } 246 | 247 | return Promise.all(tasks); 248 | } 249 | 250 | update () { 251 | return new Promise((resolve, reject) => { 252 | DynamoDB.updateItem({ 253 | TableName: process.env.NPM_PACKAGE_TABLE, 254 | Key: { 255 | name: { 256 | S: this.name 257 | } 258 | }, 259 | UpdateExpression: 'SET #info = :info', 260 | ExpressionAttributeNames: { 261 | '#info': 'info' 262 | }, 263 | ExpressionAttributeValues: { 264 | ':info': {S: JSON.stringify(this.info)} 265 | } 266 | }, (err, data) => { 267 | if (err) { 268 | return reject(err); 269 | } 270 | return resolve(this); 271 | }); 272 | }); 273 | } 274 | 275 | static fetchByName (name) { 276 | return new Promise((resolve, reject) => { 277 | DynamoDB.getItem({ 278 | TableName: process.env.NPM_PACKAGE_TABLE, 279 | Key: { 280 | name: { 281 | S: name 282 | } 283 | } 284 | }, (err, data) => { 285 | if (err || !data || typeof data.Item === 'undefined') { 286 | return reject(new Error(`Package "${name}" not found.`)); 287 | } 288 | return resolve(new Package({ 289 | name: data.Item.name.S || '', 290 | info: data.Item.info.S || '' 291 | })); 292 | }); 293 | }); 294 | } 295 | 296 | static fetchByNames (names) { 297 | return new Promise((resolve, reject) => { 298 | var packages = []; 299 | var keyExpr = names.map((name, i) => `#name = :name${i}`).join(' or '); 300 | var attrValues = {}; 301 | names.forEach((name, i) => { 302 | attrValues[`:name${i}`] = {S: name}; 303 | }); 304 | 305 | var fetchNext = after => { 306 | var query = { 307 | TableName: process.env.NPM_PACKAGE_TABLE, 308 | KeyConditionExpression: keyExpr, 309 | ExpressionAttributeNames: { 310 | '#name': 'name' 311 | }, 312 | ExpressionAttributeValues: attrValues 313 | }; 314 | if (after) { 315 | query.ExclusiveStartKey = {name: {S: after}}; 316 | } 317 | DynamoDB.query(query, (err, data) => { 318 | if (err) { 319 | return resolve(packages); 320 | } 321 | 322 | data.Items.forEach(Item => { 323 | packages.push(new Package({ 324 | name: Item.name.S || '', 325 | info: Item.info.S || '' 326 | })); 327 | }); 328 | 329 | if (typeof data.LastEvaluatedKey !== 'undefined' && data.LastEvaluatedKey !== null) { 330 | fetchNext(data.LastEvaluatedKey.S); 331 | } else { 332 | resolve(packages); 333 | } 334 | }); 335 | } 336 | 337 | fetchNext(); 338 | }); 339 | } 340 | 341 | static create (info, attachments) { 342 | info = Object.assign({}, { 343 | name: '', 344 | info: '' 345 | }, info); 346 | 347 | info.info = JSON.stringify(info.info); 348 | 349 | attachments = attachments || []; 350 | 351 | return new Promise((resolve, reject) => { 352 | DynamoDB.putItem({ 353 | TableName: process.env.NPM_PACKAGE_TABLE, 354 | Item: { 355 | name: {S: info.name}, 356 | info: {S: info.info} 357 | } 358 | }, (err, data) => { 359 | if (err) { 360 | return reject(err); 361 | } 362 | 363 | var uploads = []; 364 | Object.keys(attachments).forEach(file => { 365 | var meta = attachments[file]; 366 | 367 | uploads.push(new Promise((resolve, reject) => { 368 | S3.upload({ 369 | Bucket: process.env.NPM_PACKAGE_BUCKET, 370 | Key: file, 371 | Body: new Buffer(meta.data, 'base64'), 372 | ACL: 'private', 373 | ContentType: meta.content_type 374 | }, { 375 | // options ? 376 | }, (err, data) => { 377 | if (err) { 378 | return reject(err); 379 | } 380 | return resolve(); 381 | }) 382 | })); 383 | }); 384 | 385 | Promise.all(uploads) 386 | .then(results => { 387 | resolve(new Package({ 388 | name: info.name, 389 | info: info.info 390 | })); 391 | }) 392 | .catch(err => { 393 | reject(err); 394 | }) 395 | }); 396 | }); 397 | } 398 | 399 | static getDownloadUrl (file) { 400 | return new Promise((resolve, reject) => { 401 | 402 | S3.getSignedUrl('getObject', { 403 | Bucket: process.env.NPM_PACKAGE_BUCKET, 404 | Key: file, 405 | Expires: 60 406 | }, (err, url) => { 407 | if (err) { 408 | return reject(err); 409 | } 410 | return resolve(url); 411 | }); 412 | }); 413 | } 414 | } 415 | 416 | return { 417 | User: User, 418 | Package: Package 419 | }; 420 | }; 421 | -------------------------------------------------------------------------------- /functions/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-private-npm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Michael Grenier ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "aws-sdk": "^2.3.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /functions/npm/package/event.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /functions/npm/package/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Storage = require('../lib/Storage')(); 4 | 5 | module.exports.handler = function(event, context) { 6 | console.log('Request', event); 7 | 8 | if (event.authorization) { 9 | Storage.User.fetchByToken(event.authorization) 10 | .then(user => { 11 | 12 | var url = `${event.path}~${event.method}`; 13 | switch (url) { 14 | // npm search {package} 15 | case '/-/all/since~GET': 16 | context.fail(`https://registry.npmjs.org/-/all/since?stale=${event.stale}&startkey=${event.startkey}`); 17 | break; 18 | 19 | // npm search {package} 20 | case '/-/all~GET': 21 | context.fail(`https://registry.npmjs.org/-/all`); 22 | break; 23 | 24 | // npm info {package} 25 | case '/{package}~GET': 26 | Storage.Package.fetchByName(event.package) 27 | .then(pkg => { 28 | if (user.canRead(pkg.name)) { 29 | context.done(null, pkg.info); 30 | } 31 | else { 32 | context.fail(JSON.stringify({ 33 | error: `You need to login to access this access this package.` 34 | })); 35 | } 36 | }) 37 | .catch(err => { 38 | context.fail(`https://registry.npmjs.org/${event.package}`); 39 | }) 40 | break; 41 | 42 | // npm publish 43 | case '/{package}~PUT': 44 | var info = event.body; 45 | var attachments = info._attachments; 46 | delete info._attachments; 47 | 48 | info.maintainers = [{ name: user.name, email: user.email }]; 49 | info.time = { modified: (new Date()).toISOString() }; 50 | 51 | Storage.Package.fetchByName(event.package) 52 | .catch(err => { 53 | return Promise.resolve(null); 54 | }) 55 | .then(pkg => { 56 | if ( 57 | (pkg && user.canWrite(pkg.name) == false) || 58 | (!pkg && user.canPerform('publish') == false) 59 | ) { 60 | context.fail(JSON.stringify({ 61 | error: `You need to login to access this access this package.` 62 | })); 63 | } 64 | else { 65 | if (pkg) { 66 | info.versions = Object.assign({}, pkg.info.versions, info.versions); 67 | info['dist-tags'] = Object.assign({}, pkg.info['dist-tags'], info['dist-tags']); 68 | } 69 | 70 | return Promise.all([ 71 | Storage.Package.create({name: info.name, info: info}, attachments), 72 | user.grantWrite(info.name) 73 | ]) 74 | .then(results => { 75 | context.done(null, { 76 | ok: `Package published.` 77 | }); 78 | }) 79 | .catch(err => { 80 | context.fail(JSON.stringify({ 81 | error: `Bad request.` 82 | })); 83 | }); 84 | } 85 | }) 86 | .catch(err => { 87 | context.fail(JSON.stringify({ 88 | error: `Bad request.` 89 | })); 90 | }); 91 | 92 | break; 93 | 94 | // TODO npm publish [...] --tag {tag} https://docs.npmjs.com/cli/publish && https://github.com/rlidwka/sinopia/blob/master/lib/index-api.js#L279 95 | // TODO npm publish tarball.gz [...] https://docs.npmjs.com/cli/publish 96 | 97 | // npm install {package} 98 | case '/{package}/-/{tarball}~GET': 99 | Storage.Package.fetchByName(event.package) 100 | .then(pkg => { 101 | if (user.canRead(pkg.name) == false) { 102 | throw new Error(`User can not access this package.`); 103 | } 104 | 105 | Storage.Package.getDownloadUrl(event.tarball) 106 | .then(url => { 107 | return context.fail(url); 108 | }) 109 | .catch(err => { 110 | return context.fail(JSON.stringify({ 111 | error: `Bad request.` 112 | })); 113 | }); 114 | }) 115 | .catch(err => { 116 | context.fail(JSON.stringify({ 117 | error: `You need to login to access this access this package.` 118 | })); 119 | }); 120 | break; 121 | 122 | // npm dist-tag ls {package} 123 | case '/-/package/{package}/dist-tags~GET': 124 | Storage.Package.fetchByName(event.package) 125 | .then(pkg => { 126 | if (user.canWrite(pkg.name) == false) { 127 | throw new Error(`User can not access this package.`); 128 | } 129 | 130 | return context.done(null, pkg.info['dist-tags']); 131 | }) 132 | .catch(err => { 133 | context.fail(JSON.stringify({ 134 | error: `You need to login to access this access this package.` 135 | })); 136 | }); 137 | break; 138 | 139 | // npm dist-tag add {package}@{version} {tag} 140 | case '/-/package/{package}/dist-tags/{tag}~PUT': 141 | Storage.Package.fetchByName(event.package) 142 | .then(pkg => { 143 | if (user.canWrite(pkg.name) == false) { 144 | throw new Error(`User can not access this package.`); 145 | } 146 | 147 | pkg.info['dist-tags'][event.tag] = event.version; 148 | 149 | return pkg.update(); 150 | }) 151 | .then(pkg => { 152 | return context.done(null, { ok: 'Tags updated.' }); 153 | }) 154 | .catch(err => { 155 | context.fail(JSON.stringify({ 156 | error: `You need to login to access this access this package.` 157 | })); 158 | }); 159 | break; 160 | 161 | // npm dist-tag rm {package} {tag} 162 | case '/-/package/{package}/dist-tags/{tag}~DELETE': 163 | Storage.Package.fetchByName(event.package) 164 | .then(pkg => { 165 | if (user.canWrite(pkg.name) == false) { 166 | throw new Error(`User can not access this package.`); 167 | } 168 | 169 | delete pkg.info['dist-tags'][event.tag]; 170 | 171 | return pkg.update(); 172 | }) 173 | .then(pkg => { 174 | return context.done(null, { ok: 'Tags removed.' }); 175 | }) 176 | .catch(err => { 177 | context.fail(JSON.stringify({ 178 | error: `You need to login to access this access this package.` 179 | })); 180 | }); 181 | break; 182 | 183 | // npm unpublish {package} 184 | // npm unpublish {package}@{version} 185 | case '/{package}/-rev/{revision}~DELETE': 186 | Storage.Package.fetchByName(event.package) 187 | .then(pkg => { 188 | if (user.canWrite(pkg.name) == false) { 189 | throw new Error(`User can not access this package.`); 190 | } 191 | 192 | pkg.unpublish(event.revision == 'undefined' ? undefined : event.revision) 193 | .then(url => { 194 | return context.done(null, { 195 | ok: `Package removed.` 196 | }); 197 | }) 198 | .catch(err => { 199 | return context.fail(JSON.stringify({ 200 | error: `Bad request.` 201 | })); 202 | }); 203 | }) 204 | .catch(err => { 205 | context.fail(JSON.stringify({ 206 | error: `You need to login to access this access this package.` 207 | })); 208 | }); 209 | break; 210 | 211 | // TODO npm deprecate https://docs.npmjs.com/cli/deprecate 212 | 213 | default: 214 | return context.done(null, { 215 | event: event 216 | }); 217 | } 218 | 219 | }) 220 | .catch(err => { 221 | context.fail(JSON.stringify({ 222 | error: `You need to login to access this registry.` 223 | })); 224 | }); 225 | } 226 | else { 227 | context.fail(JSON.stringify({ 228 | error: `You need to login to access this registry.` 229 | })); 230 | } 231 | }; 232 | -------------------------------------------------------------------------------- /functions/npm/package/s-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package", 3 | "runtime": "nodejs4.3", 4 | "description": "Serverless Lambda function for project: serverless-private-npm", 5 | "customName": false, 6 | "customRole": false, 7 | "handler": "npm/package/handler.handler", 8 | "timeout": 6, 9 | "memorySize": 1024, 10 | "authorizer": {}, 11 | "custom": { 12 | "excludePatterns": [] 13 | }, 14 | "endpoints": [ 15 | { 16 | "path": "/-/all/since", 17 | "method": "GET", 18 | "type": "AWS", 19 | "authorizationType": "none", 20 | "authorizerFunction": false, 21 | "apiKeyRequired": false, 22 | "requestParameters": {}, 23 | "requestTemplates": { 24 | "application/json": { 25 | "method": "$context.httpMethod", 26 | "path": "$context.resourcePath", 27 | "authorization": "$input.params().header.Authorization", 28 | "stale": "$input.params().querystring.get('stale')", 29 | "startkey": "$input.params().querystring.get('startkey')" 30 | } 31 | }, 32 | "responses": { 33 | ".*You need to login.*": { 34 | "statusCode": "401", 35 | "responseTemplates": { 36 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 37 | } 38 | }, 39 | "https?://.*": { 40 | "statusCode": "301", 41 | "responseParameters": { 42 | "method.response.header.Location": "integration.response.body.errorMessage" 43 | }, 44 | "responseTemplates": { 45 | "application/json": "" 46 | } 47 | }, 48 | "default": { 49 | "statusCode": "200", 50 | "responseParameters": {}, 51 | "responseModels": {}, 52 | "responseTemplates": { 53 | "application/json": "" 54 | } 55 | } 56 | } 57 | }, 58 | { 59 | "path": "/-/all", 60 | "method": "GET", 61 | "type": "AWS", 62 | "authorizationType": "none", 63 | "authorizerFunction": false, 64 | "apiKeyRequired": false, 65 | "requestParameters": {}, 66 | "requestTemplates": { 67 | "application/json": { 68 | "method": "$context.httpMethod", 69 | "path": "$context.resourcePath", 70 | "authorization": "$input.params().header.Authorization" 71 | } 72 | }, 73 | "responses": { 74 | ".*You need to login.*": { 75 | "statusCode": "401", 76 | "responseTemplates": { 77 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 78 | } 79 | }, 80 | "https?://.*": { 81 | "statusCode": "301", 82 | "responseParameters": { 83 | "method.response.header.Location": "integration.response.body.errorMessage" 84 | }, 85 | "responseTemplates": { 86 | "application/json": "" 87 | } 88 | }, 89 | "default": { 90 | "statusCode": "200", 91 | "responseModels": {}, 92 | "responseTemplates": { 93 | "application/json": "" 94 | } 95 | } 96 | } 97 | }, 98 | { 99 | "path": "/{package}", 100 | "method": "GET", 101 | "type": "AWS", 102 | "authorizationType": "none", 103 | "authorizerFunction": false, 104 | "apiKeyRequired": false, 105 | "requestParameters": {}, 106 | "requestTemplates": { 107 | "application/json": { 108 | "method": "$context.httpMethod", 109 | "path": "$context.resourcePath", 110 | "authorization": "$input.params().header.Authorization", 111 | "package": "$input.params('package')", 112 | "authorization": "$input.params('Authorization')" 113 | } 114 | }, 115 | "responses": { 116 | ".*You need to login.*": { 117 | "statusCode": "401", 118 | "responseTemplates": { 119 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 120 | } 121 | }, 122 | "https?://.*": { 123 | "statusCode": "301", 124 | "responseParameters": { 125 | "method.response.header.Location": "integration.response.body.errorMessage" 126 | }, 127 | "responseTemplates": { 128 | "application/json": "" 129 | } 130 | }, 131 | "default": { 132 | "statusCode": "200", 133 | "responseParameters": {}, 134 | "responseModels": {}, 135 | "responseTemplates": { 136 | "application/json": "" 137 | } 138 | } 139 | } 140 | }, 141 | { 142 | "path": "/{package}", 143 | "method": "PUT", 144 | "type": "AWS", 145 | "authorizationType": "none", 146 | "authorizerFunction": false, 147 | "apiKeyRequired": false, 148 | "requestParameters": {}, 149 | "requestTemplates": { 150 | "application/json": { 151 | "method": "$context.httpMethod", 152 | "path": "$context.resourcePath", 153 | "authorization": "$input.params().header.Authorization", 154 | "package": "$input.params('package')", 155 | "body" : "$input.json('$')" 156 | } 157 | }, 158 | "responses": { 159 | ".*You need to login.*": { 160 | "statusCode": "401", 161 | "responseTemplates": { 162 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 163 | } 164 | }, 165 | ".*Bad request.*": { 166 | "statusCode": "400", 167 | "responseTemplates": { 168 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 169 | } 170 | }, 171 | "default": { 172 | "statusCode": "200", 173 | "responseParameters": {}, 174 | "responseModels": {}, 175 | "responseTemplates": { 176 | "application/json": "" 177 | } 178 | } 179 | } 180 | }, 181 | { 182 | "path": "/{package}/-/{tarball}", 183 | "method": "GET", 184 | "type": "AWS", 185 | "authorizationType": "none", 186 | "authorizerFunction": false, 187 | "apiKeyRequired": false, 188 | "requestParameters": {}, 189 | "requestTemplates": { 190 | "application/json": { 191 | "method": "$context.httpMethod", 192 | "path": "$context.resourcePath", 193 | "authorization": "$input.params().header.Authorization", 194 | "package": "$input.params('package')", 195 | "tarball": "$input.params('tarball')" 196 | } 197 | }, 198 | "responses": { 199 | ".*You need to login.*": { 200 | "statusCode": "401", 201 | "responseTemplates": { 202 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 203 | } 204 | }, 205 | ".*Bad request.*": { 206 | "statusCode": "400", 207 | "responseTemplates": { 208 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 209 | } 210 | }, 211 | "https?://.*": { 212 | "statusCode": "301", 213 | "responseParameters": { 214 | "method.response.header.Location": "integration.response.body.errorMessage" 215 | }, 216 | "responseTemplates": { 217 | "application/json": "" 218 | } 219 | }, 220 | "default": { 221 | "statusCode": "200", 222 | "responseParameters": {}, 223 | "responseModels": {}, 224 | "responseTemplates": { 225 | "application/json": "" 226 | } 227 | } 228 | } 229 | }, 230 | { 231 | "path": "/{package}/-rev/{revision}", 232 | "method": "PUT", 233 | "type": "AWS", 234 | "authorizationType": "none", 235 | "authorizerFunction": false, 236 | "apiKeyRequired": false, 237 | "requestParameters": {}, 238 | "requestTemplates": { 239 | "application/json": { 240 | "method": "$context.httpMethod", 241 | "path": "$context.resourcePath", 242 | "authorization": "$input.params().header.Authorization", 243 | "package": "$input.params('package')", 244 | "revision": "$input.params('revision')" 245 | } 246 | }, 247 | "responses": { 248 | ".*You need to login.*": { 249 | "statusCode": "401", 250 | "responseTemplates": { 251 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 252 | } 253 | }, 254 | ".*Bad request.*": { 255 | "statusCode": "400", 256 | "responseTemplates": { 257 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 258 | } 259 | }, 260 | "default": { 261 | "statusCode": "200", 262 | "responseParameters": {}, 263 | "responseModels": {}, 264 | "responseTemplates": { 265 | "application/json": "" 266 | } 267 | } 268 | } 269 | }, 270 | { 271 | "path": "/{package}/-rev/{revision}", 272 | "method": "DELETE", 273 | "type": "AWS", 274 | "authorizationType": "none", 275 | "authorizerFunction": false, 276 | "apiKeyRequired": false, 277 | "requestParameters": {}, 278 | "requestTemplates": { 279 | "application/json": { 280 | "method": "$context.httpMethod", 281 | "path": "$context.resourcePath", 282 | "authorization": "$input.params().header.Authorization", 283 | "package": "$input.params('package')", 284 | "revision": "$input.params('revision')" 285 | } 286 | }, 287 | "responses": { 288 | ".*You need to login.*": { 289 | "statusCode": "401", 290 | "responseTemplates": { 291 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 292 | } 293 | }, 294 | ".*Bad request.*": { 295 | "statusCode": "400", 296 | "responseTemplates": { 297 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 298 | } 299 | }, 300 | "default": { 301 | "statusCode": "200", 302 | "responseParameters": {}, 303 | "responseModels": {}, 304 | "responseTemplates": { 305 | "application/json": "" 306 | } 307 | } 308 | } 309 | }, 310 | { 311 | "path": "/-/package/{package}/dist-tags", 312 | "method": "GET", 313 | "type": "AWS", 314 | "authorizationType": "none", 315 | "authorizerFunction": false, 316 | "apiKeyRequired": false, 317 | "requestParameters": {}, 318 | "requestTemplates": { 319 | "application/json": { 320 | "method": "$context.httpMethod", 321 | "path": "$context.resourcePath", 322 | "authorization": "$input.params().header.Authorization", 323 | "package": "$input.params('package')" 324 | } 325 | }, 326 | "responses": { 327 | ".*You need to login.*": { 328 | "statusCode": "401", 329 | "responseTemplates": { 330 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 331 | } 332 | }, 333 | ".*Bad request.*": { 334 | "statusCode": "400", 335 | "responseTemplates": { 336 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 337 | } 338 | }, 339 | "default": { 340 | "statusCode": "200", 341 | "responseParameters": {}, 342 | "responseModels": {}, 343 | "responseTemplates": { 344 | "application/json": "" 345 | } 346 | } 347 | } 348 | }, 349 | { 350 | "path": "/-/package/{package}/dist-tags/{tag}", 351 | "method": "PUT", 352 | "type": "AWS", 353 | "authorizationType": "none", 354 | "authorizerFunction": false, 355 | "apiKeyRequired": false, 356 | "requestParameters": {}, 357 | "requestTemplates": { 358 | "application/json": { 359 | "method": "$context.httpMethod", 360 | "path": "$context.resourcePath", 361 | "authorization": "$input.params().header.Authorization", 362 | "package": "$input.params('package')", 363 | "tag": "$input.params('tag')", 364 | "version" : "$input.json('$')" 365 | } 366 | }, 367 | "responses": { 368 | ".*You need to login.*": { 369 | "statusCode": "401", 370 | "responseTemplates": { 371 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 372 | } 373 | }, 374 | ".*Bad request.*": { 375 | "statusCode": "400", 376 | "responseTemplates": { 377 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 378 | } 379 | }, 380 | "default": { 381 | "statusCode": "200", 382 | "responseParameters": {}, 383 | "responseModels": {}, 384 | "responseTemplates": { 385 | "application/json": "" 386 | } 387 | } 388 | } 389 | }, 390 | { 391 | "path": "/-/package/{package}/dist-tags/{tag}", 392 | "method": "DELETE", 393 | "type": "AWS", 394 | "authorizationType": "none", 395 | "authorizerFunction": false, 396 | "apiKeyRequired": false, 397 | "requestParameters": {}, 398 | "requestTemplates": { 399 | "application/json": { 400 | "method": "$context.httpMethod", 401 | "path": "$context.resourcePath", 402 | "authorization": "$input.params().header.Authorization", 403 | "package": "$input.params('package')", 404 | "tag": "$input.params('tag')" 405 | } 406 | }, 407 | "responses": { 408 | ".*You need to login.*": { 409 | "statusCode": "401", 410 | "responseTemplates": { 411 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 412 | } 413 | }, 414 | ".*Bad request.*": { 415 | "statusCode": "400", 416 | "responseTemplates": { 417 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 418 | } 419 | }, 420 | "default": { 421 | "statusCode": "200", 422 | "responseParameters": {}, 423 | "responseModels": {}, 424 | "responseTemplates": { 425 | "application/json": "" 426 | } 427 | } 428 | } 429 | } 430 | ], 431 | "events": [], 432 | "environment": "$${npmEnvironments}", 433 | "vpc": { 434 | "securityGroupIds": [], 435 | "subnetIds": [] 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /functions/npm/ping/event.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /functions/npm/ping/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.handler = function(event, context) { 4 | return context.done(null, {}); 5 | }; 6 | -------------------------------------------------------------------------------- /functions/npm/ping/s-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ping", 3 | "runtime": "nodejs4.3", 4 | "description": "Serverless Lambda function for project: serverless-private-npm", 5 | "customName": false, 6 | "customRole": false, 7 | "handler": "handler.handler", 8 | "timeout": 6, 9 | "memorySize": 1024, 10 | "authorizer": {}, 11 | "custom": { 12 | "excludePatterns": [] 13 | }, 14 | "endpoints": [ 15 | { 16 | "path": "/-/ping", 17 | "method": "GET", 18 | "type": "AWS", 19 | "authorizationType": "none", 20 | "authorizerFunction": false, 21 | "apiKeyRequired": false, 22 | "requestParameters": {}, 23 | "requestTemplates": { 24 | "application/json": "" 25 | }, 26 | "responses": { 27 | "default": { 28 | "statusCode": "200", 29 | "responseParameters": {}, 30 | "responseModels": {}, 31 | "responseTemplates": { 32 | "application/json": "" 33 | } 34 | } 35 | } 36 | } 37 | ], 38 | "events": [], 39 | "environment": "$${npmEnvironments}", 40 | "vpc": { 41 | "securityGroupIds": [], 42 | "subnetIds": [] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /functions/npm/user/event.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /functions/npm/user/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Storage = require('../lib/Storage')(); 4 | 5 | module.exports.handler = function(event, context) { 6 | 7 | var url = `${event.path}~${event.method}`; 8 | switch (url) { 9 | 10 | // npm whoami 11 | case '/-/whoami~GET': 12 | 13 | if (event.authorization) { 14 | Storage.User.fetchByToken(event.authorization) 15 | .then(user => { 16 | context.done(null, { 17 | username: user.name 18 | }); 19 | }) 20 | .catch(err => { 21 | context.fail(JSON.stringify({ 22 | error: `You need to login to access this registry.` 23 | })); 24 | }); 25 | } 26 | else { 27 | context.fail(JSON.stringify({ 28 | error: `You need to login to access this registry.` 29 | })); 30 | } 31 | 32 | break; 33 | 34 | // npm ??? 35 | case '/-/user/{name}~GET': 36 | if (event.name.match(/^org\.couchdb\.user:/)) { 37 | var name = event.name.replace(/^org\.couchdb\.user:/, ''); 38 | 39 | Storage.User.fetchByName(name) 40 | .catch(err => { 41 | context.fail(JSON.stringify({ 42 | error: `User ${name} does not exists.` 43 | })); 44 | }) 45 | .then(user => { 46 | context.done(null, { 47 | _id: `org.couchdb.user:${user.name}`, 48 | email: user.email, 49 | name: user.name 50 | }); 51 | }); 52 | } 53 | else { 54 | context.fail(JSON.stringify({ 55 | error: `Bad request.` 56 | })); 57 | } 58 | 59 | break; 60 | 61 | // npm login 62 | case '/-/user/{name}/-rev/{revision}~PUT': 63 | case '/-/user/{name}~PUT': 64 | if (event.name.match(/^org\.couchdb\.user:/)) { 65 | var name = event.name.replace(/^org\.couchdb\.user:/, ''); 66 | 67 | Storage.User.fetchByName(name) 68 | .catch(err => { 69 | return Storage.User.create({ 70 | name: event.body.name, 71 | password: event.body.password, 72 | email: event.body.email, 73 | expire: 'never' 74 | }) 75 | }) 76 | .then(user => { 77 | if (user.matchPassword(event.body.password)) { 78 | context.done(null, { 79 | ok: `User '${name}' created.`, 80 | token: user.token 81 | }); 82 | } 83 | else { 84 | context.fail(JSON.stringify({ 85 | error: `User ${name} does not exists or password mismatch.` 86 | })); 87 | } 88 | }); 89 | } 90 | else { 91 | context.fail(JSON.stringify({ 92 | error: `User was not provided.` 93 | })); 94 | } 95 | 96 | break; 97 | 98 | // npm logout 99 | case '/-/user/token/{token}~DELETE': 100 | return context.done(null, { 101 | ok: 'logged out' 102 | }); 103 | 104 | default: 105 | return context.done(null, { 106 | event: event 107 | }); 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /functions/npm/user/s-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "runtime": "nodejs4.3", 4 | "description": "Serverless Lambda function for project: serverless-private-npm", 5 | "customName": false, 6 | "customRole": false, 7 | "handler": "npm/user/handler.handler", 8 | "timeout": 60, 9 | "memorySize": 1024, 10 | "authorizer": {}, 11 | "custom": { 12 | "excludePatterns": [] 13 | }, 14 | "endpoints": [ 15 | { 16 | "path": "/-/whoami", 17 | "method": "GET", 18 | "type": "AWS", 19 | "authorizationType": "none", 20 | "authorizerFunction": false, 21 | "apiKeyRequired": false, 22 | "requestParameters": {}, 23 | "requestTemplates": { 24 | "application/json": { 25 | "method": "$context.httpMethod", 26 | "path": "$context.resourcePath", 27 | "authorization": "$input.params().header.Authorization" 28 | } 29 | }, 30 | "responses": { 31 | ".*You need to login.*": { 32 | "statusCode": "401", 33 | "responseTemplates": { 34 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 35 | } 36 | }, 37 | "default": { 38 | "statusCode": "200", 39 | "responseParameters": {}, 40 | "responseModels": {}, 41 | "responseTemplates": { 42 | "application/json": "" 43 | } 44 | } 45 | } 46 | }, 47 | { 48 | "path": "/-/user/{name}", 49 | "method": "GET", 50 | "type": "AWS", 51 | "authorizationType": "none", 52 | "authorizerFunction": false, 53 | "apiKeyRequired": false, 54 | "requestParameters": {}, 55 | "requestTemplates": { 56 | "application/json": { 57 | "method": "$context.httpMethod", 58 | "path": "$context.resourcePath", 59 | "name": "$input.params('name')" 60 | } 61 | }, 62 | "responses": { 63 | "User .* does not exists.": { 64 | "statusCode": "404", 65 | "responseTemplates": { 66 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 67 | } 68 | }, 69 | "default": { 70 | "statusCode": "200", 71 | "responseParameters": {}, 72 | "responseModels": {}, 73 | "responseTemplates": { 74 | "application/json": "" 75 | } 76 | } 77 | } 78 | }, 79 | { 80 | "path": "/-/user/{name}", 81 | "method": "PUT", 82 | "type": "AWS", 83 | "authorizationType": "none", 84 | "authorizerFunction": false, 85 | "apiKeyRequired": false, 86 | "requestParameters": {}, 87 | "requestTemplates": { 88 | "application/json": { 89 | "method": "$context.httpMethod", 90 | "path": "$context.resourcePath", 91 | "name": "$input.params('name')", 92 | "body" : "$input.json('$')" 93 | } 94 | }, 95 | "responses": { 96 | ".*User was not provided.*": { 97 | "statusCode": "400", 98 | "responseTemplates": { 99 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 100 | } 101 | }, 102 | ".*does not exists or password mismatch.*": { 103 | "statusCode": "409", 104 | "responseTemplates": { 105 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 106 | } 107 | }, 108 | "default": { 109 | "statusCode": "201", 110 | "responseParameters": {}, 111 | "responseModels": {}, 112 | "responseTemplates": { 113 | "application/json": "" 114 | } 115 | } 116 | } 117 | }, 118 | { 119 | "path": "/-/user/{name}/-rev/{revision}", 120 | "method": "PUT", 121 | "type": "AWS", 122 | "authorizationType": "none", 123 | "authorizerFunction": false, 124 | "apiKeyRequired": false, 125 | "requestParameters": {}, 126 | "requestTemplates": { 127 | "application/json": { 128 | "method": "$context.httpMethod", 129 | "path": "$context.resourcePath", 130 | "name": "$input.params('name')", 131 | "revision": "$input.params('revision')", 132 | "body" : "$input.json('$')" 133 | } 134 | }, 135 | "responses": { 136 | ".*User was not provided.*": { 137 | "statusCode": "400", 138 | "responseTemplates": { 139 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 140 | } 141 | }, 142 | ".*does not exists or password mismatch.*": { 143 | "statusCode": "409", 144 | "responseTemplates": { 145 | "application/json": "#set ($error = $util.parseJson($input.path('$.errorMessage'))) { \"error\": \"$util.escapeJavaScript($error.error)\" }" 146 | } 147 | }, 148 | "default": { 149 | "statusCode": "201", 150 | "responseParameters": {}, 151 | "responseModels": {}, 152 | "responseTemplates": { 153 | "application/json": "" 154 | } 155 | } 156 | } 157 | }, 158 | { 159 | "path": "/-/user/token/{token}", 160 | "method": "DELETE", 161 | "type": "AWS", 162 | "authorizationType": "none", 163 | "authorizerFunction": false, 164 | "apiKeyRequired": false, 165 | "requestParameters": {}, 166 | "requestTemplates": { 167 | "application/json": { 168 | "method": "$context.httpMethod", 169 | "path": "$context.resourcePath", 170 | "token": "$input.params('token')" 171 | } 172 | }, 173 | "responses": { 174 | "default": { 175 | "statusCode": "200", 176 | "responseParameters": {}, 177 | "responseModels": {}, 178 | "responseTemplates": { 179 | "application/json": "" 180 | } 181 | } 182 | } 183 | } 184 | ], 185 | "events": [], 186 | "environment": "$${npmEnvironments}", 187 | "vpc": { 188 | "securityGroupIds": [], 189 | "subnetIds": [] 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-private-npm", 3 | "version": "0.0.1", 4 | "description": "A Serverless Project and its Serverless Plugin dependencies.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://bitbucket.org/sinapsio/serverless-private-npm.git" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "npm", 12 | "nodejs" 13 | ], 14 | "author": "Michael Grenier ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://bitbucket.org/sinapsio/serverless-private-npm/issues" 18 | }, 19 | "homepage": "https://bitbucket.org/sinapsio/serverless-private-npm", 20 | "dependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /s-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-private-npm", 3 | "custom": {}, 4 | "plugins": [] 5 | } -------------------------------------------------------------------------------- /s-resources-cf.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway", 4 | "Resources": { 5 | "IamRoleLambda": { 6 | "Type": "AWS::IAM::Role", 7 | "Properties": { 8 | "AssumeRolePolicyDocument": { 9 | "Version": "2012-10-17", 10 | "Statement": [ 11 | { 12 | "Effect": "Allow", 13 | "Principal": { 14 | "Service": [ 15 | "lambda.amazonaws.com" 16 | ] 17 | }, 18 | "Action": [ 19 | "sts:AssumeRole" 20 | ] 21 | } 22 | ] 23 | }, 24 | "Path": "/" 25 | } 26 | }, 27 | "IamPolicyLambda": { 28 | "Type": "AWS::IAM::Policy", 29 | "Properties": { 30 | "PolicyName": "${stage}-${project}-lambda", 31 | "PolicyDocument": { 32 | "Version": "2012-10-17", 33 | "Statement": [ 34 | { 35 | "Effect": "Allow", 36 | "Action": [ 37 | "logs:CreateLogGroup", 38 | "logs:CreateLogStream", 39 | "logs:PutLogEvents" 40 | ], 41 | "Resource": "arn:aws:logs:${region}:*:*" 42 | }, 43 | { 44 | "Effect": "Allow", 45 | "Action": [ 46 | "dynamodb:GetItem", 47 | "dynamodb:PutItem", 48 | "dynamodb:Query", 49 | "dynamodb:UpdateItem", 50 | "dynamodb:DeleteItem" 51 | ], 52 | "Resource": "arn:aws:dynamodb:${region}:*:table/${project}-users-${stage}" 53 | }, 54 | { 55 | "Effect": "Allow", 56 | "Action": [ 57 | "dynamodb:GetItem", 58 | "dynamodb:PutItem", 59 | "dynamodb:Query", 60 | "dynamodb:UpdateItem", 61 | "dynamodb:DeleteItem" 62 | ], 63 | "Resource": "arn:aws:dynamodb:${region}:*:table/${project}-packages-${stage}" 64 | }, 65 | { 66 | "Effect": "Allow", 67 | "Action": [ 68 | "s3:DeleteObject", 69 | "s3:GetObject", 70 | "s3:PutObject", 71 | "s3:AbortMultipartUpload", 72 | "s3:ListMultipartUploadParts" 73 | ], 74 | "Resource": "arn:aws:s3:::${project}-packages-${stage}/*" 75 | } 76 | ] 77 | }, 78 | "Roles": [ 79 | { 80 | "Ref": "IamRoleLambda" 81 | } 82 | ] 83 | } 84 | }, 85 | "DynamoDBTableUser": { 86 | "Type": "AWS::DynamoDB::Table", 87 | "DeletionPolicy": "Retain", 88 | "Properties": { 89 | "TableName": "${project}-users-${stage}", 90 | "AttributeDefinitions": [ 91 | { 92 | "AttributeName": "name", 93 | "AttributeType": "S" 94 | } 95 | ], 96 | "KeySchema": [ 97 | { 98 | "AttributeName": "name", 99 | "KeyType": "HASH" 100 | } 101 | ], 102 | "ProvisionedThroughput": { 103 | "ReadCapacityUnits": 1, 104 | "WriteCapacityUnits": 1 105 | } 106 | } 107 | }, 108 | "DynamoDBTablePackage": { 109 | "Type": "AWS::DynamoDB::Table", 110 | "DeletionPolicy": "Retain", 111 | "Properties": { 112 | "TableName": "${project}-packages-${stage}", 113 | "AttributeDefinitions": [ 114 | { 115 | "AttributeName": "name", 116 | "AttributeType": "S" 117 | } 118 | ], 119 | "KeySchema": [ 120 | { 121 | "AttributeName": "name", 122 | "KeyType": "HASH" 123 | } 124 | ], 125 | "ProvisionedThroughput": { 126 | "ReadCapacityUnits": 1, 127 | "WriteCapacityUnits": 1 128 | } 129 | } 130 | }, 131 | "S3BucketPackage": { 132 | "Type": "AWS::S3::Bucket", 133 | "DeletionPolicy": "Retain", 134 | "Properties": { 135 | "BucketName": "${project}-packages-${stage}", 136 | "AccessControl": "Private" 137 | } 138 | } 139 | }, 140 | "Outputs": { 141 | "IamRoleArnLambda": { 142 | "Description": "ARN of the lambda IAM role", 143 | "Value": { 144 | "Fn::GetAtt": [ 145 | "IamRoleLambda", 146 | "Arn" 147 | ] 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /s-templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmEnvironments": { 3 | "NPM_SECRET": "${secret}", 4 | "NPM_USER_TABLE": "${project}-users-${stage}", 5 | "NPM_PACKAGE_TABLE": "${project}-packages-${stage}", 6 | "NPM_PACKAGE_BUCKET": "${project}-packages-${stage}" 7 | } 8 | } 9 | --------------------------------------------------------------------------------