{"_id":"sprucebot-skills-kit-server","_rev":"75-b7ba8bcbd1039c86d321b9d0ec160c0a","name":"sprucebot-skills-kit-server","dist-tags":{"latest":"6.18.1","prerelease":"6.19.0-beta.2b0bbf7","beta":"6.18.1"},"versions":{"0.0.5":{"name":"sprucebot-skills-kit-server","version":"0.0.5","scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"ebdaf461353fc7e5aa675e6375adf9a016b4e9d3","_id":"sprucebot-skills-kit-server@0.0.5","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-zR++ReLxu8ekMnsXKCzpvuZcmgwzY3LovGOA6siI+9ksY+0b2/0GJFSZ4sh1ttuKfVQSnV7z0LFRf0P9xml+HQ==","shasum":"1543cdabc40c4873b1578caa86b997966eaca184","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.0.5.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIEuIe9o9bR15Ma6lprMXUmejY0x6VbS+uwfbSY0SiHSMAiAgUkbCzGp85Lm8Kv6XUW5CWOtAAyHekIK6QzBNUjEVZQ=="}]},"maintainers":[{"name":"liquidg3","email":"npm@taylorrome.ro"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.0.5.tgz_1510162005879_0.5806755921803415"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.0.6":{"name":"sprucebot-skills-kit-server","version":"0.0.6","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"403d2fe005b96b264d6a4337df4debd60b1ba286","_id":"sprucebot-skills-kit-server@0.0.6","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-0zu8RJ/gkwLM8ogIhltOT9lJz0KZTCpni1tpnNlqN83yILPCgKMYLLtH4Idhlwprs43JK5tqzLoX9rzAobZJ5w==","shasum":"a43fcf804c780f43cc7f466f4f87c635e2c07378","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.0.6.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQC0Ga89crntlkvnJR3tkHHAqUO0biHb4d8W/kuSYssb8gIgc1FnhPrpBMGTWjnTfFTmCGajKhYu+U4gj8D/UzWyyRE="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.0.6.tgz_1510168599300_0.8135508047416806"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.0.7":{"name":"sprucebot-skills-kit-server","version":"0.0.7","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"5c906720505f0dcde85bbfb38eb613cd91b8c268","_id":"sprucebot-skills-kit-server@0.0.7","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-YkuTpRM8Q6ik7k4+GZeXtDfNnrEuzQsRvefNdnNH2fVTrGDx87jjvPDpDQOe6uLqV0KlL+oRFGQpEmz8iB8Oow==","shasum":"2678c4b5baee378730d5c737764b356c53467806","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.0.7.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD2NQ7QQadTv40lqHVt4vFnQgU9fd+NSME9GzfkGcAjUAIhANQEj29x++bg56r6cTCo0jqLmN34ISFgMS4Gr8OCyoDW"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.0.7.tgz_1510186277648_0.6971494383178651"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.0.8":{"name":"sprucebot-skills-kit-server","version":"0.0.8","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"7c0461d3d664db8f6f4eae14334f48d74f0c8a3a","_id":"sprucebot-skills-kit-server@0.0.8","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-mXg206qEO2k9WxNEYrNbHYLYapzvLo9qDvgL5VI3JDttVAYWVxzGsqrctog+lmqmDkCYNFy5e4wiCaAqhMFddw==","shasum":"b375084ce98c6caa158dc6c460bf42e7b4af25c6","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.0.8.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCDkWp1/WmK8PG+uDXIM22raLBzln13S0mwjeQg+7/ECgIgQPGJFlFARDsQJbOry8dywiWd/+AoXD0NQzlGy7J30C8="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.0.8.tgz_1510197258575_0.02003646595403552"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.1.0":{"name":"sprucebot-skills-kit-server","version":"0.1.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"ec7e5d34bbb48e1b93dc1973c66ce025798c75e4","_id":"sprucebot-skills-kit-server@0.1.0","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-XY6MJRTonh4AA0Kt++GAL3os1L4lcuZHttZb4j3gYzBmPHehGJiWWDkmvVsmUIk+GAly0JGC5BGCTZoTdPi33Q==","shasum":"a33006e9d6211effe247f9bdd9a28d27909d4450","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.1.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD4GAGRsow3peRtqOIOZFgcfcOSxLrcL6FK2mmr7PMIBQIhAOOaWE7Zbz9/s9AjEbzLJN7zVK3aq8FEdP/WaUlUfDgN"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.1.0.tgz_1510263584751_0.33711789594963193"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.2.0":{"name":"sprucebot-skills-kit-server","version":"0.2.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"553496960cf7f6b61075cd51ce59ba0a31150ab9","_id":"sprucebot-skills-kit-server@0.2.0","_npmVersion":"5.4.2","_nodeVersion":"8.4.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-RxLBIq9JcM6luC1+Q7EXrh2r+Ob7IjFjp7B4WtDxXpPd7+VoDXI7HzzfXz/S0I6KncELxBbJk5h4VoPx/cvoOA==","shasum":"390de48b6fc10955d34d4e929572add6f6f27eb3","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.2.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDKHDhBFFUVpznQQzJAtna67kFC9Z4dQ/YYIQZSfdCQlAIgdVDntwwNDP6U6mz2md+ai0VOvgMcHQKsSxnlncmGpq0="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.2.0.tgz_1510603686366_0.5599142725113779"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.2.1":{"name":"sprucebot-skills-kit-server","version":"0.2.1","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"a05f4ff36f7f6f09cabc23cb4ba42af9219571f6","_id":"sprucebot-skills-kit-server@0.2.1","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-gzzxKqh9vDeb6Qy7q1rZvNyEBRqkuHMju1MNI1P/m8sBWhmxS7VAPV7dnmmquCOqYgvyMlCoY4ZH5GjqaR3WgQ==","shasum":"2c80b221f1956a694cec22a30f93088e41e36541","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.2.1.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDi6uLV0KTcm+CKEftroF9pldW9WqGkPsbh2ZUvAFyLfQIhAJVh/uWFlYEBvhsyonyWdUmndBd1CYVByuHxNswJORB9"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.2.1.tgz_1510691915179_0.6151114476379007"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.2.2":{"name":"sprucebot-skills-kit-server","version":"0.2.2","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"009787c3c16092e0ce87ab171aa345171d15265f","_id":"sprucebot-skills-kit-server@0.2.2","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-QbluwdQ5YVKX7boERse01IggGbtO6Jk/xifnOC5+UlfOKSdmSACdVSKDUhsOeMyszLBfatQ0SxEW1q/Cxm+v9Q==","shasum":"032c6d7bf7c8985d101ab84992e27f21081219ca","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.2.2.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIA32/hO1TkRwCToPmRZrM85HuhkfNWRoAPyBwDJ8zLuRAiEAuztvu8xP8jfGtYjGEp/hZP3J2gW5OYuOrQ9kB5/PfXU="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.2.2.tgz_1510698966000_0.3032329985871911"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.3.0":{"name":"sprucebot-skills-kit-server","version":"0.3.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"49ce67055f0d202c2442020a2771be9b65ff0570","_id":"sprucebot-skills-kit-server@0.3.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-fFsnjqmJ2TJOiwRNdg2wTJUFx+IMvOqSXXlzhr5rxD0ROv3seMfevOK5Bief/ucmWwfynQbDLiToClOHR5WvjA==","shasum":"9305f43ad18d040d06f02c845539cbdd19af797d","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.3.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC2b4G/7ov3+h1u8R9sCcZrqwghc0ju0UpAdJ7J2r5g2wIhALn6XAq57cZvuF+a4nGiLgxwUAJ/4tIquNFarM7mo9/I"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.3.0.tgz_1510702027542_0.26463565812446177"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.3.1":{"name":"sprucebot-skills-kit-server","version":"0.3.1","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"edf4d6b77987d02e7b6ae93ddc6a928896d34935","_id":"sprucebot-skills-kit-server@0.3.1","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-YCMPlcBt8tEvGeZZDQff7EuzuvB1qkj31CPGrI0yPRTHN64vLMemf3ynbju9TCWnPj+XU3D/jkhwZAwRYkc4gA==","shasum":"4b63df85f23f48b35ab4ec8196e5c8cd4f1d4e9f","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.3.1.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDKYo0wIXOZwrxgxua8WeosDD3p8WKWgxc42ccV9d+eggIgUlSx/gACEtSHuKp+dRgsz3+RqQ2ep7s12LSCSbkmPUA="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.3.1.tgz_1510781990538_0.1746704475954175"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.3.2":{"name":"sprucebot-skills-kit-server","version":"0.3.2","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"f5c5bc3f4eac151dde8cba92480efae66893f8ec","_id":"sprucebot-skills-kit-server@0.3.2","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-gmirfm/nQn5ItlBSh/vo9bbUd0a0Aox/i1RMBsq+IddmdBMUOtZNCsYdg2xXoHif9cI/NZLWqHficaj4qVyxYw==","shasum":"e92af74caabc17f5adf80f59e4ff2bf49c8b4ff9","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.3.2.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQD+pRQm22GE5Zi5o7SZgwlor5japRUkqNEkENfrsKNuDgIgQesq8ZrAI+jtCKU0qs0DCx/z6aDsA8kIQsDA3/Rsm4o="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.3.2.tgz_1510856473885_0.9420632487162948"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.4.0":{"name":"sprucebot-skills-kit-server","version":"0.4.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"03adf3d34a1d417c500b3ba17c24031f05893792","_id":"sprucebot-skills-kit-server@0.4.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-IKFnuERcDo4bbJyMZD1GV4hYZ7HGFLrhlAJZv9eQQSSFl1xsKWgn0ZdW5tcMqv44ggCJdVC5+6nuYzAxbljc3A==","shasum":"a3d2ab228d4c457da73e806bcd96c9441e2ee7bf","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.4.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCt6/zvqCZCAY5tPo/i/OC7SBdLeUzwRZWjQ7vdIItxUQIgWdF0epl/SyARfi6SNP9owUN53OY3QkQxa675z9yNSjM="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.4.0.tgz_1510963575869_0.4697545198723674"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.5.0":{"name":"sprucebot-skills-kit-server","version":"0.5.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"634dab597a8f8ae817d0b0f9b1c6be9e5434294e","_id":"sprucebot-skills-kit-server@0.5.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-j7ri+TceL5v6asR/0ARHf6HOSEkncuS6RO8cKHcpbFXeLvcb698hb6zd67bTrQ4MlCWnDohDpybE7GtUaRY/Lw==","shasum":"85b921966ed75e1399b48ac31c663217e2563aad","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.5.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDRw1pxnNDth4RARjr8qIzFAlk2DgRtgRh3WHEEIsM3jwIhAM5LglG0vs1GRn71YMwnf4qrBesg3BbJGByKoRmVvpPE"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.5.0.tgz_1511115683387_0.737853343365714"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.5.1":{"name":"sprucebot-skills-kit-server","version":"0.5.1","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"decaef18c8eabb34754ba4ae6d1da509bc7dd607","_id":"sprucebot-skills-kit-server@0.5.1","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-AMAhDF8kqMRGcsgiv44NyIEMKq0tvzOG4vvm113Q3aqlRdiU8RWGzLuiZ/hjwGqtRzlakP7TNsV0gVFCRxllLQ==","shasum":"a752f4c2d21b4f6202eecff7ed403c4857ca9e5f","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.5.1.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAQQxcKfMcnXoHArZ6DMklDowrUlhSjztm83d74iAXpkAiALOotI5f1Xnpj4wMA5J4Wsuu6pz22H25AGl0icBduqtA=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.5.1.tgz_1511212893474_0.6787661211565137"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.5.2":{"name":"sprucebot-skills-kit-server","version":"0.5.2","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"8ff3f030e29e51b1f642f3c9b6d8753b043a5273","_id":"sprucebot-skills-kit-server@0.5.2","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-ORtsmU+GQDDmvc/Jg9o9fzs7JYvtGxgB3SL/mgxWLkI6u9j9MHwHUEp3XPDc5CJs8uv3ImA7/ChArKmeSTnF9Q==","shasum":"a096c04dcc1822d6a7ac8f5dd504f4274fb237f2","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.5.2.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCnXymNVDDAb+lGvoQzv8vSorhk1L6j2eIycl13+KroKgIhAO5+QFhg+sGkXN+9qcGYVV/IPODGPlJFOcAQy/1KtZfN"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.5.2.tgz_1511372536661_0.7647830289788544"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.6.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! 🌲🤖","version":"0.6.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"1c2146a9b66c4f2d3d874348078be8c7233a1c9d","_id":"sprucebot-skills-kit-server@0.6.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-ftwNyJRfRD8hqIv/HQXsZiqrQwZVjgaL7Dqw2mgsLQW5DR6Ef3VFwGJgrplPmUindnjduglpvXItWzFKaB+hrQ==","shasum":"ec24b206cc8b2e9ad1eb8df67cb33bb2550a3f20","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.6.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFpY3QYUgEcxZqYcEWwz5ZyGH8f8k378K4awKs651CmdAiEAiwYBIPS00ZBzBawQ5Tc8CvKcKobX1efgXqBCdiHMXuw="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.6.0.tgz_1511800265681_0.9625952057540417"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.6.1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! 🌲🤖","version":"0.6.1","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"eb381e6d840871728fa252d6feecdb3960e9233a","_id":"sprucebot-skills-kit-server@0.6.1","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-rOqZo4xzyKx3TT0NLzqivqcC7dZ1ZZPJfG/MkTyKe10sGemyzukBB6WZlze0Bu5ZTpho+4DwqxhJoI3yWqk/nQ==","shasum":"883af66663051418339fffb75e8b56bf050834e0","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.6.1.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQClg3gE4aclUCv71gIEcFhTvZhllamluJPjR9D7SS7TAgIhAKpMSLwwsfVYNewgpg5EilymJ5KXbhDoJTe2oTf4e62r"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.6.1.tgz_1513200603568_0.014027288416400552"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"0.6.2":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! 🌲🤖","version":"0.6.2","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"ad4b6dd7e28da14de8ac2c1399e90c993bdc83b3","_id":"sprucebot-skills-kit-server@0.6.2","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-Pa7V1rrSTp5lcBwtJj+r3QlyP9rbGa3d7VCcKGYY3G1dTScgqr7W9eSJWe6HlF68PxGvbVH2ajvMRHBW1RrEqQ==","shasum":"3b4c67913c62d3c7e9fd614cfaa73372999e2497","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-0.6.2.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBMurVx/ROQ7C4LggXPsAbEWoz42+PmHCVhpxv4sc+uoAiEAvO/9r9UFfVUD1jMBRzpH+8pZCoDkpnZ10Dtfm8ANBHE="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-0.6.2.tgz_1515187161566_0.8647495836485177"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! 🌲🤖","version":"1.0.0","scripts":{"lint":"eslint --fix '**/**/*{.js,.scss}'","test":"jest --coverage","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^21.2.1","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"coverage"},"gitHead":"d1828efd367706d8bf243ea12fe834babc7f3cf2","_id":"sprucebot-skills-kit-server@1.0.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-Tfw3wC/hrcbRbqzC4dwjLXjdZJbc0J6jLw5s6mIRtdt6YRrMSSb1GAUCE29SQqsyBfmlPmcintKd4shIDehVLg==","shasum":"70f6c04333fc47e25f6b8af3dda4f7c814cf2a7a","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.0.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCbV9GxPmPo8rxrI/WZ+UXoBbl/gA4PsFscWeF3dBBfBgIhAPfsZOsF5Iqhz3lE+ZkoTuskrJ48Sp//ih4KJzhwv90q"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-1.0.0.tgz_1516577347089_0.8413017003331333"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.1","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2e7896b80b6482a8d0242c53ef10dccc1c1ec087","_id":"sprucebot-skills-kit-server@1.0.1","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-tWDVgWfPyW9s6B4zxNV7vAp5A2MoYythxTf25NDAFpo1DroTAdbC4qTpgky4HVay/OvsBSuJ/AQ5CzU7i+eL/Q==","shasum":"6dc29b32ba5c1eb6897ca763e6b00340cd226e34","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.1.tgz","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCaDxGgxQJsOoF3+1y+SQz4gUulCoJGar72cC/pcDe2nwIhAKZgr9FP1R64YWk26aOUYoiBQjEpDYrLBNI0TeEI+g1p"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"shmcdermed@gmail.com","name":"mcdermed"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server-1.0.1.tgz_1517614333980_0.993979184422642"},"directories":{},"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.2":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.2","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"107eda3c42016da68746af3a4094527d9b5b75fe","_id":"sprucebot-skills-kit-server@1.0.2","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-7vlsJiHqy4Wt+5eJzkk+q1kel92vIQZGfyixSBicT2cCLsGvp4bvnaDSqldCUeD1eHPoc2oTzEaJmxLQmI3VRA==","shasum":"a66fc907fa8b2678685a3649694aa5c34a2a3844","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.2.tgz","fileCount":22,"unpackedSize":246976,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIE4pmebsdVqXls805mTZED7thEgr7KNLH2LwW6VB6pYvAiEA4rxmzHAFOIVCivlM5vT0GOXTlwySl5GEkquVb9cz3DA="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.2_1518318720978_0.43003521079319795"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.3":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.3","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2a3d29f1719f2575ba3a4e99502b46f3e8b29c46","_id":"sprucebot-skills-kit-server@1.0.3","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-C1n7Y9yT9wCvgyn4B3MMc/tK+X04S3jF0lMvHWNvk0ff+vTZLc5lGhqWpOixOIXGDrr//sfNa1sSo0Rw+SUJpQ==","shasum":"eed578ef40fc7d0157ef5aa43d8c96db6063717d","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.3.tgz","fileCount":22,"unpackedSize":246975,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBGaYcA73FLrNGmCQsMcQ4thTevcg4gKJy3bMzC0wh4AAiEAkI83onawNgaVtNNfnJaEJuv//etOQy5408PIt4z523A="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.3_1518725369706_0.5565258074777637"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.4":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.4","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"d7132069fc59a52d065b417b89df1ebf2d834b60","_id":"sprucebot-skills-kit-server@1.0.4","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-KueEK6gUPZyW98E+tLDCTN8GfDP7LLdARjUVeI6ciRolKv3qvGeJqD6SmzY2fMkMLRL9VcT+JanYb9I+EFdxJw==","shasum":"5cbd4e45dba85e48ccceaf9f5b73b325b0023197","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.4.tgz","fileCount":22,"unpackedSize":247279,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCSc2g0phPtmFR4cQbdA0NMGafha2YktDPpq3Pr0BA90wIhAMq04vQ7Wq/5xY4tMgKp9eYdm6Zf2AxsXatTCxT5X2So"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.4_1518730538216_0.30650524234923804"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.5":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.5","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"9d23762daea4affe832e4083e11bdb826f857792","_id":"sprucebot-skills-kit-server@1.0.5","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-wn2ai4915y77Z/legW6FPHnwQSOr2HLxvdBgmD/Hddfmdx/ftGOjB9nGwqAdJ86IB0eOP8q8geqj0qMOLn6WSA==","shasum":"5305add7b6a32b7703ec7c839df438968a0226e4","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.5.tgz","fileCount":22,"unpackedSize":247303,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCDuUNIUTeS/T/rX+IwTP/ko9t4X6DZarn9v5lMalTmxQIgaI2N8PpNvcsYHB2490T6XljFRGvQz/qObcWiAi71CPM="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.5_1518877128911_0.11997795208364725"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.6":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.6","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2dfd474bdb732d9f5d76745f4452ccadd8929c11","_id":"sprucebot-skills-kit-server@1.0.6","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-iD73cQdPHp7evIHm3iNwOHTZ+vPT0fo4MqFnh9IZhrhNZG+vMKD+86JKooGg3xkRsDLC/3qxOQ9hdvanTeEwQA==","shasum":"d4062efd5594a58bb0e7e6f5a31a4e308d061312","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.6.tgz","fileCount":22,"unpackedSize":247359,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDQ6o7glwbozCYpgcmu600ctwMzu3owpGwFIWh6XvoEbwIgcpb7/1nhnZY4YyD3qRDhNnL+/7IUGE7WvdH6QDiGDkY="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.6_1519070087540_0.2903468864953527"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.7":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.7","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.0.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"30bfcc660aa0880b624daf5346b829591b9b9999","_id":"sprucebot-skills-kit-server@1.0.7","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-rd4taebwqoXLA4woBcPbMkCOGV0WkzGRI1m+SaKc+zpVQWjCwof57aLyWSdxhCI8hxar+uJMAKIVqqe1gWHaaA==","shasum":"b182382ab3f199e7f7ed219b64ca4b89ea4dd157","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.7.tgz","fileCount":27,"unpackedSize":272762,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIE+dhoHvr+v4Ab/yQMDkQHZdvj5JdLs2ojbzwo0QhszqAiAXVXirtKwibvSoJNJDNlLkwVtFkBhrrVzwLgCMhhna8Q=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.7_1519859804978_0.41387373890254087"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.0.8":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.0.8","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","react":"^16.0.0","react-dom":"^16.0.0"},"devDependencies":{"add":"^2.0.6","eslint":"^4.10.0","aws-sdk":"^2.153.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"5153d0b99148916127f0de2e40acf399b0b6a697","_id":"sprucebot-skills-kit-server@1.0.8","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"liquidg3","email":"npm@taylorrome.ro"},"dist":{"integrity":"sha512-AvCxIFIb/PvcDpJ+49Nf3WM4gGGcCGYCer5bPszGKwbQKYcfh6hHuLc0+FiSqPypHWuuBY1GmGUkCvJIbnN5RA==","shasum":"5a8e3ff4422ac390a0945d54442cc308ef2f0f09","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.0.8.tgz","fileCount":22,"unpackedSize":247390,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIEWeD+bAWp/banNrUikalP+gft3vMZco5Q+Je+YEOdnmAiAa//b1MLx7nizb7plQDQ7K4wQolOmjpDV0JLfU7BqHrQ=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.0.8_1519859916460_0.3340903684446499"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.1.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.1.0","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.0.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"0279e3b5bce6d4a5af5fd6eb0eb10404e5154d6c","_id":"sprucebot-skills-kit-server@1.1.0","_npmVersion":"5.3.0","_nodeVersion":"8.5.0","_npmUser":{"name":"lkkadiri","email":"lkkadiri@gmail.com"},"dist":{"integrity":"sha512-qMF0HoDuxtwvSrt2B1fa3dJdUYK0wIZjroeVvTlM5DzqGDgxCzq2y2WCeKw8rKFec5kLMb58oUHIiy7fDltOSQ==","shasum":"eae4cc641b591f24428fd0f19007eaa2b5bccd96","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.1.0.tgz","fileCount":28,"unpackedSize":272815,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAOWuAddVLiuiUj5FB3Szc3/3qsRIh8H7l9ciw9H82X+AiAY1w786tyeaZhviCmeNwBZA4KJuJ7S05+7af5/VBUv1w=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.1.0_1520026060876_0.07818482527178006"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"1.1.1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"1.1.1","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^4.1.4","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.0.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"7ff776e154a27dbc1921b8c333f0e46519b87824","_id":"sprucebot-skills-kit-server@1.1.1","_npmVersion":"5.6.0","_nodeVersion":"8.9.4","_npmUser":{"name":"mcdermed","email":"shmcdermed@gmail.com"},"dist":{"integrity":"sha512-Kq5oig1VBAkEH2RLN25O+yEggEhQe+2HfCcElZaE+fXZW7vf7T9IdCa/4E2AvUuz7ODkz+wHynQ+NDB2mSxzIQ==","shasum":"e1a560b04ce0ee0c04c16bb492e7fdf0981b78f9","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-1.1.1.tgz","fileCount":27,"unpackedSize":270728,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIHqD9D2CKHVM6g7/bqDtmJAZbW9Hd6nyaOEf6vIVTD/JAiEA2E/OjOo0lunbjuYVT8vR4ToxiztaETrDEXyA1sKWn2c="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_1.1.1_1520295441383_0.4395541251947177"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"2.0.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"2.0.0","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.0.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"76ef7f03c1d090c8e475b8abf0b23d7f38b611d2","_id":"sprucebot-skills-kit-server@2.0.0","_npmVersion":"5.6.0","_nodeVersion":"8.10.0","_npmUser":{"name":"mcdermed","email":"shmcdermed@gmail.com"},"dist":{"integrity":"sha512-CXCHdKijpCX2BH3PFTxQgusAFgsiPbSdwz4aEBj0AHZOY+lpEegc6aQhLZidYuybd0hWLpnxBeVeHGkAMHIOgg==","shasum":"b4ced2d8d305c8243bbbd77ccbd7704d8d624f35","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-2.0.0.tgz","fileCount":27,"unpackedSize":46195,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDAkf+uhM9jTDzjv16b36amFo8u1N//jstSJy62sFpAFQIhAKt1tRYCkmo5D1ke/0qAnDvOV0JiYDfn2eRX2d/TObk6"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_2.0.0_1520869120372_0.3390733875256502"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"5.0.1-test.1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"5.0.1-test.1","scripts":{"lint":"eslint '**/**/*{.js,.scss}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.0.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@5.0.1-test.1","dist":{"shasum":"d761e886304a4231413b4c8d18817e4de83e3528","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-5.0.1-test.1.tgz","fileCount":38,"unpackedSize":46202,"integrity":"sha512-TdCK2YRNnEicglbXG3pFC65+vAa44Z1Gkq5ChXkYABlHFFuGrOd8897tkJKcsm9ceY/4xc8cRKg4EDG/OZt72w==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBXEt8RHzjG5lQRwAaUzEx1DeSVxeV/ZxM1RDigNzDBHAiEA0b1naLuEJCWR8sg0G2cew7btFCtaO5XBy9U+z1kxZOc="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"shmcdermed@gmail.com","name":"mcdermed"}],"_npmUser":{"name":"mcdermed","email":"shmcdermed@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_5.0.1-test.1_1521045324699_0.3049679637178413"},"_hasShrinkwrap":false},"3.0.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"3.0.0","scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.0.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@3.0.0","dist":{"shasum":"68ffeec86d23607c8cda198807798ec9d190ec80","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-3.0.0.tgz","fileCount":38,"unpackedSize":46144,"integrity":"sha512-N4/Jb4NKGJgIUufA0Jk+ufKWS3S/Dr0jSo3TovfCR1Cm7W86kpf9CeGBGq6MjkrT1qPM0ooXsHjyAJfYDnYY8g==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCl7EY1oyRziL9+ZYkYp9vXqw+j0PfyEOrfWdSrGw89eQIhALUtIzWH0CNnqlrEu/2Vxit3R7jdS+a6pKSOSZuuWGZj"}]},"maintainers":[{"name":"artisgarbage","email":"artisgarbage@gmail.com"},{"name":"liquidg3","email":"npm@taylorrome.ro"},{"name":"lkkadiri","email":"lkkadiri@gmail.com"},{"name":"mcdermed","email":"shmcdermed@gmail.com"},{"name":"sprucelabs","email":"scientists@sprucelabs.ai"}],"_npmUser":{"name":"mcdermed","email":"shmcdermed@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_3.0.0_1521066873198_0.34751825695331684"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.3.0-beta.2361777":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.3.0-beta.2361777","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.0.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2361777d2b81d7bd5f4b089ff17dc4ced478edde","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.3.0-beta.2361777","dist":{"shasum":"c5076d7682e2e62fad1b41619db54af00ea79834","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.3.0-beta.2361777.tgz","fileCount":37,"unpackedSize":46305,"integrity":"sha512-4x9e8vBRZpDlROlMH3PuJwmyrW18zSsyNOXvuqcMc7wEP7J7UGGAw/kDSi0Xp3YXRpHsECBkaAwuNtLtrLRw7g==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIH0c/c5lb+Mm55Y6FT2436qseCwJ3JTpj6JESsHyDGg9AiEAuKbvt+51nxjmSQ29VhUMkX7Qb9FlWuC3iPsAk7dh8bc="}]},"maintainers":[{"name":"artisgarbage","email":"artisgarbage@gmail.com"},{"name":"liquidg3","email":"npm@taylorrome.ro"},{"name":"lkkadiri","email":"lkkadiri@gmail.com"},{"name":"mcdermed","email":"shmcdermed@gmail.com"},{"name":"sprucelabs","email":"scientists@sprucelabs.ai"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.3.0-beta.2361777_1523033868905_0.8139839697454296"},"_hasShrinkwrap":false},"6.4.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.4.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.0.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"59e941f49ac9878d39c3611fdbe9b138e06307cd","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.4.0","dist":{"shasum":"165fbcf627dd610b76e574f7a15a9310fcd88020","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.4.0.tgz","fileCount":37,"unpackedSize":46253,"integrity":"sha512-SRK1hf04sGAPbQyU37XA7VEqQmB96tjW1bIFLlXig6wkvONiA1Hr8cgTCN/Xfa/9qDJ9srhRTtm3FCAr2Ti5Xw==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIA2gF9x01wF11ZLNpykN1n7GKEEuS72/31gTFYZbnhaIAiEArMevs9XLus10dM9YkljkCbV5QKKLgceJbXqw+WvqwxI="}]},"maintainers":[{"name":"artisgarbage","email":"artisgarbage@gmail.com"},{"name":"liquidg3","email":"npm@taylorrome.ro"},{"name":"lkkadiri","email":"lkkadiri@gmail.com"},{"name":"mcdermed","email":"shmcdermed@gmail.com"},{"name":"sprucelabs","email":"scientists@sprucelabs.ai"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.4.0_1523220792453_0.4934532433260037"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.5.0-beta.f6da2e8":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.5.0-beta.f6da2e8","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"f6da2e88df541e3b811bd7965508098353c8af31","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.5.0-beta.f6da2e8","dist":{"shasum":"5e5f86e6ad6b132566884063aed45d0cf0b3a0bb","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.5.0-beta.f6da2e8.tgz","fileCount":37,"unpackedSize":46511,"integrity":"sha512-giql+X75ktpuChkp9SV7wk9TldIiix+I/qkl0/QdRT25IvwkKq8rA30/O/EZzGMwvQAKqwxIeZZFeHThP4azow==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDxmc/FLHDSJXtzAAtojjqsaAybGHcv2LyjOen3+8rvHAiEA6sbrCG9Poa6hZj4Wlk8F1pQrBhnstXBOmayorFK9y9w="}]},"maintainers":[{"name":"artisgarbage","email":"artisgarbage@gmail.com"},{"name":"liquidg3","email":"npm@taylorrome.ro"},{"name":"lkkadiri","email":"lkkadiri@gmail.com"},{"name":"mcdermed","email":"shmcdermed@gmail.com"},{"name":"sprucelabs","email":"scientists@sprucelabs.ai"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.5.0-beta.f6da2e8_1523471042340_0.17033571123950075"},"_hasShrinkwrap":false},"6.4.2":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.4.2","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"6686104f14c886883f62b945f422016cf5b48163","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.4.2","dist":{"shasum":"19953aa089f8392e11c020af4223292bfdcd780d","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.4.2.tgz","fileCount":37,"unpackedSize":46460,"integrity":"sha512-Xk5h5ExTNPK4OlaJXEekHJl0lH1xx1jRX1g7+VKHu+jEkodadggkcjIC65O5VhQSHDaBW9OZRalhE6B3GKIn4A==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDGNc8PJ1fRqQGWfyzaVBWHFrRniXE0cXiLR4iyNDA4GAIgF4N2fCfXvGGUjI4ERQQIh6Ti9Kp5uJ4Tr4v+uZEgOqs="}]},"maintainers":[{"name":"artisgarbage","email":"artisgarbage@gmail.com"},{"name":"liquidg3","email":"npm@taylorrome.ro"},{"name":"lkkadiri","email":"lkkadiri@gmail.com"},{"name":"mcdermed","email":"shmcdermed@gmail.com"},{"name":"sprucelabs","email":"scientists@sprucelabs.ai"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.4.2_1523473644405_0.9021212420464304"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.6.0-beta.15b47a1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.6.0-beta.15b47a1","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"15b47a1db86e78f731c8beab687ffc2bd0711f77","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.6.0-beta.15b47a1","dist":{"shasum":"4820ed86f4ecfbadcb97e7d64866b2dd25f5fce0","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.6.0-beta.15b47a1.tgz","fileCount":39,"unpackedSize":48381,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJa44AdCRA9TVsSAnZWagAAkf0P/i775cJfGJJfeG5m0x61\nPgnu1EyZ9Kxo/3MCpIsQoqQOB4Y3wCfCr4cOxHnWO4kiJSV2LHunM7kUkMpY\nnTeeMfhZ4tR8d0Zn0T8BszvJBC6FxC0ITxyqYIkyPQofHE/tXkJj8yRXFAvX\nQ/gp9sHT4dPNwF1LHTEq4vex1ki+v7hEnseLniDGG3F+9qzRbmXvRP2WWz+2\nt2+WZzveOCRTkCtLyPJKwUPekx2Ki+OPiOPJC0Qn8JE3L7Fq2urF1RZ/HwNg\n53sV/DdTRcUHX7AZpglNVsmoiyizzTX0ahAcMgVnnDKwBe7Z4sK97TgfvNrH\neb2r9MStxE64/eFahk69rlC3wC8p5ENXptLEYUxm57T/jN0FwBURtdSKsJcF\nLb2lkw75u7exiX6NVvbK/X8FI6w6lvAKtaTLhYhqHEF/gYC6RwPw+5k81uCY\nS5hxAoIzq5Ctn0S9LVi/7ZfF5AQVzqCyjEqOktFRN7SVZSwJjgXIY2Rx2Zz4\nZs30pGzsRvdZevEe0CTG4jdp2LFVJa4/RE+369+FQl1zC/A5OjGsZTseU1g8\npWd6+K2NuJFPdo4KNnMK7lHvr7GcE9efEJBmRbw4WIWVHKqArUXwNCK4i3nP\nJQD4SftzZpQIbgL3WpWzLtuO53gtZeqMhbr4X+43rSAASzQk5wfBsDdVBQQ8\nTsUo\r\n=xb5W\r\n-----END PGP SIGNATURE-----\r\n","integrity":"sha512-yz4lvHARQK3JaMFGSFDhxsABAR7218LQZtGVJnM0Jh5zUgW+TIFaAfpvAIrF1yxkY0B7SnjTgrmMOPKJnzBl1w==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDuMdN4EExqR9IX4MnNQgzXhmJ2eEUKzLJjSlvntmMcagIgMNXdHqTffl6J/DEhh6Fur/8miHwt5+I7oQ6YvWnLt+E="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.6.0-beta.15b47a1_1524858909299_0.760357954605007"},"_hasShrinkwrap":false},"6.6.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.6.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.6.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.7.4","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"b8eec5e6dba95f82d60e9831735913f692785bed","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.6.0","dist":{"shasum":"17ffd2749eb1b1fb12ecd104271b8075c7aa2ca3","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.6.0.tgz","fileCount":39,"unpackedSize":48329,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJa44dGCRA9TVsSAnZWagAAACUQAIRxdbJ8S3xXk5oIMUg5\n6GFTw7zPmHUKOcfePHs4H8n2FrQr936q6jV2l0g2SB67NjM9iGErtBgzWMFZ\nDy4q95bnVU8Y+TMaCK5Dp+B3JnGbuVTQE8DDmQdx2BYB2JTLDYJSZq/TjcKV\njAeL7f8DBlQXYEb1PwBt2+k01w44UsXRh7ypq7d0daKl1b8RxYHmhmqJAagR\nhcqG5tNhZFbER35qGydpoisuQJIQm+HhQFl8oXBxnkhoVpuYaFmr+z6fl1bq\n7uzqk9sikVg5nIuFGxWA7t3+frOQuaV69yPGmAqtJJjYxdspUJMmIpGgJxTM\nvPM5gxWnZlTvWlZX193PXs7EdQWsJmGNgSJv4BmFP05sXWIo0s1Wokd9K3+t\nKs5OF9DNzLfHN7NSDk2Uvhmst17ugm7UedNR3XPibr+5MoHtQB1/jDrpW+5G\nyG42nOSRo6cKSsVgr6NKTFbbjwrANfsWTWb9S21JP9dHp8vy0IthPfBQyfnI\nCbtUUyON9K+qiZGCQ2+r2ml0O2raG5lrh4M2WcMXLaK1Q+TeFjOki0CSq50h\nrVyZ8JeZgde+4zHuQSOOTJALHZ0LeOwB5a5ZQJeEP3kKIjcwXJ7tX3OEiCRT\nZRyNgKo9CcgMcyNVAV62TsgCSaDt/BX1ehH3lmOb472aAYfzlqn9Fyn7nwTl\nPpJW\r\n=Wpyk\r\n-----END PGP SIGNATURE-----\r\n","integrity":"sha512-god+cm69a6K/3xoGJNSoOoMGo+qhqBqyo/69rG5Tvbaw3pbJKtuAFgsIMLjAYMLS0b2bGjbjrfXzuyPkUUOnyQ==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIH0vzlI6L5y23mI2h8CfugyJGqZOP/nrxEw3oHkX54sUAiAboJsa2fTphkPL0QJ44FlPCCSDN2O0EnfhOH6MtBk4mQ=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.6.0_1524860740949_0.8276484151342298"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.7.0-beta.63485cc":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.7.0-beta.63485cc","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"63485ccafccd66213df66c351b4d62059c6095b8","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.7.0-beta.63485cc","dist":{"shasum":"1e8e367e3c09e65e7703a9f1c37c4f79794ef7d3","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.7.0-beta.63485cc.tgz","fileCount":39,"unpackedSize":48567,"integrity":"sha512-+zmqJMXnMiV5IBvv7RfVxt5/6UkzwPkGd2GLZ0TwHqEcgFltXafkwECgzMZ8qnI2qGtd5RW0+rkd6pa6NfBu8w==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICEgOPKiAUD4P3flQ8Myr9S1ik37lSmSJ/RhNCkLNmVVAiEA7pQe8jNKqNZFc4M4rYetc0IFQRoKVdQvVRPqFdNVDjM="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.7.0-beta.63485cc_1529698395924_0.8231750855512929"},"_hasShrinkwrap":false},"6.7.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.7.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"15c28e1fe7f6fca4806be0d2b3e9f4bf4029d640","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.7.0","dist":{"shasum":"1285087731632bee4fcde79d73ce3530d9c0971e","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.7.0.tgz","fileCount":39,"unpackedSize":48515,"integrity":"sha512-VXdzT4M3QgSnLBPFBNGV3mtkuNgWhB2VaOxlymVD67swhDXbSr0nYHEBPQXZQ4lk8gu/UoVc8G5UHXKcuPaokQ==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGN5h6ZmC0QQy7NMWKMSu2+9HImgHpDdlqXiNPxUpsfMAiEAiyRwlQDstxch8YC2p9ye5kb5Rme9EJ7xN6+cQFba8P8="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.7.0_1530067164737_0.5690342485915052"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.8.0-beta.15c28e1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.8.0-beta.15c28e1","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"15c28e1fe7f6fca4806be0d2b3e9f4bf4029d640","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.8.0-beta.15c28e1","dist":{"shasum":"f3b37662f1fabe6753a8d7a63a9e14d7711090af","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.8.0-beta.15c28e1.tgz","fileCount":39,"unpackedSize":48773,"integrity":"sha512-qo1zQdQUqdW4g0OsSqSmOBUCs9+aFV4o42yyAMr64dRcFd9eCzYvXoe2SeGqrEYH8AJLvAsOLZN/vHfThVchzA==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDMyHwhQRmqDMBUkHsLulZtzM2Yez86wu7zWMXss5+ZAAIgF/g2eR5oyhwtps8s1bviF5NGEHFuu2JjCleTNsNvmLQ="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.8.0-beta.15c28e1_1530116645978_0.9815232752270031"},"_hasShrinkwrap":false},"6.14.0-beta.40e5194":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.14.0-beta.40e5194","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"40e519432165ac036340c53a064d728f267d824f","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.14.0-beta.40e5194","dist":{"shasum":"84939722b2b3f872cf98afdc27dd2891fe611dde","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.14.0-beta.40e5194.tgz","fileCount":39,"unpackedSize":49463,"integrity":"sha512-pCKMuyXEg4d1umW3CWSKnCbBHCyVueZgp7kSufEAdYRKk/cVJ2mgUbixVxTeDm07l5UZ8aN6nqSirj00HtdC5A==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIHTOnEOjNdSVlzF8nzFXNJNVJboruCGJ9QO4cEIN2XrGAiA0LnWyzPsJ4g4Gi54rus7/qlQt2hlRaenzVCeopxuOVQ=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.14.0-beta.40e5194_1531160980465_0.3586419806850778"},"_hasShrinkwrap":false},"6.14.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.14.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"c48000d4c5467f62dd90468faf78865171cc35bd","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.14.0","dist":{"shasum":"85da5b56f730a58a96678c400894eac213c7095e","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.14.0.tgz","fileCount":39,"unpackedSize":49411,"integrity":"sha512-wvqWATmPC9+dn52XECStUkY4Eo2g/XhdDw8uN38/ib3c2i5Pes8bbWZRZeVpfGutJf+p3IvgiCNKb9Sns5tymg==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAK/HuH0mmpTK7UIGgN78jGa7EakLtZtvqYUbxIrBgE5AiEAynvIvEqX/Jy2IukIjAsmjvy0EcbPVwsfGsGrM66SDb4="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.14.0_1531168517651_0.8907120781586124"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.15.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.15.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"e779235887e5095032e6828cef631b21e58ca3f4","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.15.0","dist":{"shasum":"2a44339eb3b35c63b379f41bb1b29204fcfbb735","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.15.0.tgz","fileCount":40,"unpackedSize":50730,"integrity":"sha512-V6kGajjvnWzvXehYOZaIs3ZlZEGUyaZBS35Ypf0HOiscyTGBG6yofNJKLTwSZvCrpNBNdvQqvx3qSrSJIQ89tw==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC3A92NXpNBkMZaNOt9dHFdw8EINZwDv7NaZan0gvACNQIhAOgfTGPmdtCOn1XFVk2lteSv19ydlVz9uYzOY98iq10i"}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.15.0_1531270007460_0.1767001929885006"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.16.0-beta.e779235":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.16.0-beta.e779235","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"e779235887e5095032e6828cef631b21e58ca3f4","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.16.0-beta.e779235","dist":{"shasum":"c38829647a02305dee7263c95a995a390b036a4e","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.16.0-beta.e779235.tgz","fileCount":40,"unpackedSize":50992,"integrity":"sha512-NnbevsLvP0YNsqZGNPUow8WhdPgO+PkKO0KTmJkxfMVR3OGbLcgKUNGel8SvgwsIZQPYY5FsQQ+Q0aT04IBUVg==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDSg1yuhrbHR6eajtq2hlU9/uOrF/GGSHIyi2GKCJfQ9wIgWHc4hmoOB+3+XggDgauitgWYQHM4HdJ36j5O8eJwsZ8="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.16.0-beta.e779235_1531271635177_0.2542686008195816"},"_hasShrinkwrap":false},"6.16.0-beta.7d965cc":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.16.0-beta.7d965cc","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"7d965ccabd82e53032666a193517dfe0b311ca9f","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.16.0-beta.7d965cc","dist":{"shasum":"385a4745d19d38bc8dcc4f327ee3ba5b2d3be3ad","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.16.0-beta.7d965cc.tgz","fileCount":40,"unpackedSize":51061,"integrity":"sha512-nOS65fnmrAuQQdHZ6hziVdOKsv1AZ3SOLQJaInCk+ST8Q3mjFYxGhWhV85ElgX99pRL3di1+o/PM7EkL1KB07w==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDl7slk9tsQvxykFkAw8Og5HTi4PQcaOEyTrc+JGVJjxAiAk+GVVZ1sYJF2YjLq4c4b2PXHvVAaemhpvF862jaC2pQ=="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.16.0-beta.7d965cc_1531287332636_0.39862141609466706"},"_hasShrinkwrap":false},"6.16.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.16.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"f1635ac23545ae1d0f3d02d0e0571c5064658538","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.16.0","dist":{"shasum":"6e3f71d3d97371f62f3fa1e90a5abf61421530d9","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.16.0.tgz","fileCount":40,"unpackedSize":51009,"integrity":"sha512-vwEyFiWyU7Y64gQzTNYQcLDkdL1K48fdtsOwgoZrPEFt3qhR4mZC13Vghmsk8eFz+2zbargsywIKp0BcjR3m7A==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCUfYsDRJ/je5Po22M5WjTYQ+VHaIuA+yY1NzZCG/hpYgIgBXGYEvln3Qw1bux2vjkJHkpnPDHSyEyWJS+rHw6oHrM="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.16.0_1531309544233_0.8890605428923155"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.18.0":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.18.0","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2b0bbf734ec54afa330a1e2dd107b713ed28123c","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.18.0","dist":{"shasum":"8512457a7db21d12e7d6c577e3ac322a616a4a31","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.18.0.tgz","fileCount":40,"unpackedSize":51376,"integrity":"sha512-o3O+SeG4NHu7NtBisZXKm8e4OOHJpHv8jykhmWHVE72HS869BlTep1B5pqdih6CIa6BqrckvGRiwyNQZRAFzOw==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCBqFYWuSBlzDzvjrox5RJOocdY4MJNiVno4mCSNjEVgwIgWZd1qqVwCoFK+Wvbf+bZXcBVRO2c19mLqp2J1efGBjQ="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.18.0_1531334149226_0.13351447638796832"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."},"6.19.0-beta.2b0bbf7":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.19.0-beta.2b0bbf7","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"gitHead":"2b0bbf734ec54afa330a1e2dd107b713ed28123c","readmeFilename":"README.md","readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","licenseText":"MIT License\n\nCopyright (c) 2017 Spruce Labs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","license":"MIT","_id":"sprucebot-skills-kit-server@6.19.0-beta.2b0bbf7","dist":{"shasum":"f9422208a577802a9504bb60d31d51aa1787496a","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.19.0-beta.2b0bbf7.tgz","fileCount":40,"unpackedSize":51638,"integrity":"sha512-1J/RjLGKnUTOYNZv2e+TexrxrfMxLsCDjC8mEtlHxJrXg0ZHZ+K7RCTN3ruqthvqxfJ8GJMOsGlD+dgjTrGkqQ==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB+mkRF5WBhmC3NKK8pObVuKyAmARjE2xqv1VeFBmcH3AiEA1G+IYVt/c5DgAYwfQHgILVI8S9QwXMdFz+xqqJv8hNo="}]},"maintainers":[{"email":"artisgarbage@gmail.com","name":"artisgarbage"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs"}],"_npmUser":{"name":"sprucelabs","email":"scientists@sprucelabs.ai"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.19.0-beta.2b0bbf7_1531335907411_0.7802574213675952"},"_hasShrinkwrap":false},"6.18.1":{"name":"sprucebot-skills-kit-server","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","version":"6.18.1","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"},"keywords":["node","sprucebot","sprucelabs"],"scripts":{"lint":"eslint '**/**/*{.js}'","test":"jest","precommit":"npm run lint","prepush":"npm run lint && npm run test"},"dependencies":{"@koa/cors":"2","config":"^1.28.0","cookies":"^0.7.1","debug":"^3.1.0","glob":"^7.1.2","jsonwebtoken":"^8.1.0","koa":"^2.4.1","koa-bodyparser":"^4.2.0","koa-router":"^7.2.1","koa-static":"^4.0.1","next":"^5.1.0","node-cron":"^1.2.1","pg":"^7.4.1","react":"^16.2.0","react-dom":"^16.0.0","sequelize":"^4.33.4","umzug":"^2.1.0"},"devDependencies":{"add":"^2.0.6","aws-sdk":"^2.153.0","eslint":"^4.10.0","eslint-config-prettier":"^2.9.0","eslint-plugin-prettier":"^2.3.1","jest":"^22.0.5","prettier":"^1.13","yarn":"^1.2.1"},"jest":{"clearMocks":true,"testPathIgnorePatterns":["/config/"],"collectCoverageFrom":["**/*.js","!**/node_modules/**","!**/__TEST__/**"],"coverageDirectory":"./coverage/","collectCoverage":true},"readme":"# `sprucebot-skills-kit-server`\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/sprucelabsai/sprucebot-skills-kit-server.svg)](https://greenkeeper.io/)\nThis module relies heavily on [koajs](http://koajs.com) and [koa-router](https://github.com/alexmingoia/koa-router). It can help to be familiar with those modules, but it's probably not 100% necessary.\n\n## Where to start?\nIf you haven't already, you should totally checkout the [sprucebot-skills-kit's documentation](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs). In fact, this readme is assuming you already read it.  \n\n## File structure\nIt is probably a good idea to go through each file to understand how they work. It'll help a lot when building your skill.\n\n * `.vscode` - Settings for your favorite IDE.\n * `controllers` - For built in [`controllers`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#controllers) that are made available in every skill.\n    * `auth.js` - An [authentication](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#auth) endpoint. Also, a condition role set for when `DEV_MODE` is enabled in your skill.\n* `factories` - Factories for helping us setup and run your skill.\n    * `context.js` - Reusable factory for dropping things onto your `ctx`. Used to populate `services` and `utilities`.\n    * `listeners.js` - Sets up all your [`listeners`](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/events.md), which are `.js` files that exist in `server/events` in your skill.\n    * `routes.js` - Sets up your `controllers`.\n    * `wares.js` - Sets up your [middleware](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/server.md#middleware).\n* `helpers` - Simple `utilities` we make available to your skill.\n    * `lang.js` - Handles [language support](https://github.com/sprucelabsai/sprucebot-skills-kit/blob/dev/docs/lang.md). **TODO: move to separate module and import**.\n* `middleware` - Built-in middleware that works on all skills.\n    * `auth.js` - Handles authorization, i.e. locks routes by role.\n* `node_modules` - Nodejs stuff.\n* `services` - Built-in [`services`](https://github.com/liquidg3/sprucebot-skills-kit/blob/dev/docs/server.md#services).\n    * `uploads` - Built in upload adapters.\n        * `s3.js` - For uploads to [S3](https://aws.amazon.com/s3).\n    * `uploads.js` - Handles picking the upload adapter and passing it your file.\n* `support` - Built-in configs and settings made available to your skill.\n    * `errors.js` - Built-in errors.\n* `utilities` - Built-in `utilities` made available to your skill.\n    * `auth.js` - Helpful methods for checking role hierarchy.\n\n## Checking permissions\nLets say you want to send an alert to the team when a `user` arrives. But, you have rules around how it should work. \n\n* `guest` arrives -> notify `teammates` and `owners`\n* `teammate` arrives -> notify `owners`\n* `owner` arrives -> no notification\n\nUsing the built-in `auth` `utility`, you have the following.\n\n * `auth.isAbove(teammate, guest)` \n * `auth.isAboveOrEqual(teammate, guest)`\n\nYou should check the source of `utilities/auth` in this module to see how it works.\n\nFor the rules defined above, we'll use `auth.isAbove()`.\n\n```js\n// server/events/did-enter.js\nmodule.exports = async (ctx, next) => {\n    next() // let sprucebot to it's usual (which is nothing on did-enter)\n    try {\n        // we are probs gonna want special error reporting here so we can know the context\n        // of the failure. remember, everything good goes in utilities or services\n        await ctx.services.alerts.send(ctx.event)\n\n    } catch (err) {\n\n        // a helpful message about the error to help us track it down from the logs\n        console.error('did-enter failed to send alert')\n\n        // followed be the actual error\n        console.error(err)\n    }\n}\n```\n\nNow we'll create our `service` for sending the alerts.\n\n```js\n// server/services/alerts.js\nmodule.exports = {\n\n    // an event object mirrors a user object, so this works 100%\n    async send(user) {\n\n        // load all teammates\n        const teammates = await this.sb.users(locationId, { role: 'ownerteammate' })\n        const sendTo = teammates.reduce((sendTo, teammate) => {\n\n            // use built in auth utilities to check role. Honors rules above.\n            if (this.utilities.auth.isAbove(teammate, user)) {\n                sendTo.push(teammate)\n            }\n\n            return sendTo\n\n        }, [])\n\n        //send to everyone\n        await Promise.all(sendTo.map(teammate => {\n            return this.sb.message(teammate.Location.id, teammate.User.id, this.utilities.lang.getText('arrivalAlert', { teammate, user }))\n        }))\n\n    }\n}\n```\n\nFor the sake of it, lets define our lang.\n```js\n// lang/default.js\nmodule.exports = {\n    arrivalAlert ({ teammate, user }) => `Hey ${teammate.user.firstName || teammate.user.name}, ${teammate.user.name} has arrived!`\n}\n```\n\n## Uploading files\nCurrently the only data store built-in is S3. You can add your own very easily. Lets start by setting up S3 and along the way talk about how to specify your own.\n\nWe'll start on the `interface` with a file input. We're gonna make the file input hidden because it's ugly. Instead, we'll prompt the `user` to upload a file after they tap a fancy `<Button />`.\n\nWe're going to depend on newer browser features, including `FileReader` to make this work. Also, we'll only let them upload a pdf.\n\n```js\n// interface/pages/owner/index.js\nimport { Container, Button } from 'react-sprucebot'\n\nclass OwnerDashboard extends Component {\n\n    constructor (props) {\n        super(props)\n        this.state = {\n            errorMessage: undefined\n        }\n    }\n    // setup the file reader when client side\n    componentDidMount() {\n\t\t// is browser out-to-date\n\t\tif (typeof FileReader === 'undefined') {\n\t\t\tthis.setState({\n\t\t\t\terrorMessage: this.props.lang.getText('outOfDateBrowserMessage')\n\t\t\t})\n\t\t} else {\n\t\t\t// setup file reader, we're\n\t\t\tthis.reader = new FileReader()\n\t\t\tthis.reader.onload = this.onFileReaderLoadFile.bind(this)\n\t\t\tthis.reader.onerror = this.onFileReaderLoadFileFail.bind(this)\n        }\n        \n        this.props.skill.read()\n        this.props.actions.files.fetch()\n\t}\n    \n    // tiggered when clicking our nice <Button />\n    selectFile() {\n\n        // triggers the \"select file\" prompt\n        this.fileInput.click()\n    }\n\n    // triggered when a file is selected\n    onFileSelect(e) {\n        // pull the first file (only one at a time for this example)\n        const file = e.target.files[0]\n\n        // always good to do a mime-type check\n        if (file.type !== 'application/pdf') {\n\t\t\tthis.setState({\n                errorMessage: this.props.lang.getText('badFileFormatErrorMessage')\n            })\n\t\t\treturn\n\t\t}\n\n\n        // read the file using the reader\n        this.reader.readAsDataURL(file)\n    }\n\n    // called when the FileReader has read the whole file\n    onFileReaderLoadFile(e) {\n        const content = e.target.result\n        const name = e.target.name\n\n     \n        // defined in our actions in the code sample below\n        this.props.actions.files.upload(content, name)\n\t}\n\n    // if the FileReader fails for some reason\n\tonFileReaderLoadImageFail(err) {\n\t\tconsole.error(err)\n\t\tthis.setState({ errorMessage: this.props.lang.getText('uploadImageFailedMessage') })\n\t}\n\n\n    render() {\n\n        const { lang, files } = this.props\n        const { errorMessage } = this.state\n\n        // errors can be set in our state or by an action failing\n        const error = errorMessage || (files.uploadError && files.uploadingError.friendlyMessage)\n\n        return (\n            <Container className=\"ownerDashboard\">\n                {!error && (\n                    <BotText>{lang.getText('ownerDashboardBotText')}</BotText>\n                )}\n                \n                {error && (\n                    <BotText>{error}</BotText>\n                )}\n\n                {files.file.value && (\n                    <BotText>{`Current file url: ${files.file.value}`}</BotText>\n                )}\n                \n                <Button busy={files.uploading} primary onClick={this.selectFile.bind(this)}>\n                    {lang.getText('uploadFileButtonLabel')}\n                </Button>\n\n                <input \n                    type=\"file\" \n                    ref={input => {\n                        this.fileInput = input\n                    }}\n                    onChange={this.onFileSelect.bind(this)}\n                    style={{ display:'none' }} \n                />\n            </Container>\n        )\n    }\n}\n\n```\nSome things to notice in the above example:\n * We can manually set an error using `state`, but also errors in `actions` are reported through `props`. So, we check both places. This can feel tedious until you actually want to handle different errors differently, then it's a life saver.\n * We use `<Button busy={files.uploading}>` to show a nice `<Loader />` inside the button while the upload is in progress.\n * We check `files.file.value` for the currently uploaded file. This is actually the `url` of the file which is saved as `meta` after upload (which is why we check `value`)\n\nLets move into the `action` for this upload process.\n\n```js\n// interface/store/actions/files.js\nexport const FETCH_FILE_REQUEST = 'files/FETCH_FILE_REQUEST'\nexport const FETCH_FILE_SUCCESS = 'files/FETCH_FILE_SUCCESS'\nexport const FETCH_FILE_ERROR = 'files/FETCH_FILE_ERROR'\n\nexport const UPLOAD_FILE_REQUEST = 'files/UPLOAD_FILE_REQUEST'\nexport const UPLOAD_FILE_SUCCESS = 'files/UPLOAD_FILE_SUCCESS'\nexport const UPLOAD_FILE_ERROR = 'files/UPLOAD_FILE_ERROR'\n\nexport function fetch() {\n    return {\n        types: [\n            FETCH_FILE_REQUEST,\n            FETCH_FILE_SUCCESS,\n            FETCH_FILE_ERROR\n        ],\n        promise: (client, auth) => client.get(`/api/1.0/owner/files/file.json`)\n    }\n}\n\nexport function upload(content, name) {\n    return {\n        types: [\n            UPLOAD_FILE_REQUEST,\n            UPLOAD_FILE_SUCCESS,\n            UPLOAD_FILE_ERROR\n        ],\n        promise: (client, auth) =>\n            client.post(`/api/1.0/owner/files/upload.json`, {\n            body: {\n                content,\n                name\n            }\n        })\n    }\n}\n```\nDon't forget to let your `interface` know your new action exists.\n\n```js\n// interface/store/actions/index.js\nimport * as users from './users'\nimport * as locations from './locations'\nimport * as files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n\n```\nOk, time for the `reducer`.\n```js\n// interface/store/reducers/files.js\nimport {\n    FETCH_FILE_REQUEST,\n    FETCH_FILE_SUCCESS,\n    FETCH_FILE_ERROR,\n    UPLOAD_FILE_REQUEST,\n    UPLOAD_FILE_SUCCESS,\n    UPLOAD_FILE_ERROR\n} from '../actions/files'\n\nexport default function reducer(state = null, action) {\n    switch (action.type) {\n        case FETCH_FILE_REQUEST:\n            return {\n                ...state,\n                fetching: true\n            }\n        case FETCH_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                fetchError: false,\n                fetching: false\n            }\n        case FETCH_FILE_ERROR:\n            return {\n                ...state,\n                fetchError: action.error,\n                fetching: false\n            }\n        case UPLOAD_FILE_REQUEST:\n            return {\n                ...state,\n                uploading: true\n            }\n        case UPLOAD_FILE_SUCCESS:\n            return {\n                ...state,\n                file: action.result,\n                uploadError: false,\n                uploading: false\n            }\n        case UPLOAD_FILE_ERROR:\n            return {\n                ...state,\n                uploadError: action.error,\n                uploading: false\n            }\n        default:\n            return state\n\t}\n}\n\n```\nExpose your `reducer` to the `interface`.\n```js\n// interface/store/reducers/index.js\nimport users from './users'\nimport locations from './locations'\nimport files from './files'\n\nmodule.exports = {\n    users,\n    locations,\n    files\n}\n```\nOk, `interface` is good to go. Lets setup the `controller` on the `server` to receive the file and pass it to S3 (or whatever storage platform we want). We're going to store the files URL in `meta` for when we want it later. In this example, we're going to save the file for the `location`.\n\n```js\n// controllers/owner/files.js\nmodule.exports = router => {\n\n    router.get('/api/1.0/owner/files/files.json', async (ctx, next) {\n\n        // check if the file has been uploaded yet\n        const meta = await ctx.sb.meta('file', {\n            locationId: ctx.auth.Location.id\n        })\n\n        ctx.body = meta || {}\n\n        await next()\n\n    })\n\n    router.post('/api/1.0/owner/files/upload.json', async (ctx, next) {\n\n        // ensure file and name exist\n        ctx.assert(typeof(ctx.body.content) === 'string', 'FILE_MISSING')\n        ctx.assert(typeof(ctx.body.name) === 'string', 'NAME_MISSING')\n\n        // stop race condition if multiple owners are uploading files\n        // with this is place, last upload will win, but we'll never\n        // end up with duplicates\n        const key = `saving-file-for-${ctx.auth.Location.id}`\n        await ctx.sb.wait(key)\n\n        try {\n\n            // upload the file (THIS WILL overwrite a file be the same name)\n            // this is using s3, which is is defined in `config/default.js`\n            const url = await ctx.services.uploads.upload(ctx.body.content, {\n                Key: `uploads/${ctx.body.name}`,\n                ACL: 'public-read'\n            })\n\n            // save the image to meta for later\n            const meta = ctx.sb.upsertMeta('file', url, {\n                locationId: ctx.auth.Location.Id\n            })\n\n            ctx.body = meta\n\n        } catch(err) {\n\n            console.error(error)\n            ctx.throw('UPLOAD_FAILED')\n\n        } finally {\n\n            // always unblock\n            ctx.sb.go(key)\n            await next()\n        }\n\n    })\n}\n```\n\nOk, we're almost there! We need to configure our uploads `service` to work properly.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n\t\tuploads: {\n\t\t\tuploader: './uploads/s3.js',\n\t\t\toptions: {\n\t\t\t\tBucket: 'my-bucket-name',\n\t\t\t\taccessKeyId: process.env.AWS_ACCESS_KEY_ID,\n\t\t\t\tsecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY\n\t\t\t}\n\t\t}\n\t}\n}\n\n```\nThat's it! Now, if you want to create your own upload `service`, you could do this.\n\n```js\n// config/default.js\nmodule.exports = {\n    ...,\n    services: {\n        uploads: {\n            uploader: path.join(__dirname, '../server/services/ftp.js'),\n            options: {\n                endpoint: process.env.FTP_ENDPOINT,\n                path: process.env.FTP_PATH\n            }\n        }\n    }\n}\n\n```\nNow, when you call `ctx.services.files.upload()` it'll invoke your `service`'s `upload()` method. \n\nNote: Make sure you define `init(options)` in your uploader. It'll receive whatever is defined in `config/default.js` -> `services.uploads.options`.\n\n# What's next?\nHmm, tbd on this one.","readmeFilename":"README.md","_id":"sprucebot-skills-kit-server@6.18.1","_npmVersion":"5.6.0","_nodeVersion":"8.11.3","_npmUser":{"name":"kengoldfarb","email":"hello@kengoldfarb.com"},"dist":{"integrity":"sha512-Cmz0fy69X9M3V6cTL7PWscW3P9N0R7imfiiqk52ZqYEeCy6HT/c1R8pvoXFNXapKJjH3ial7ZPl7JuA1o3nnmA==","shasum":"4e22037ad77c4f7998243d427a503bb33593863e","tarball":"https://registry.npmjs.org/sprucebot-skills-kit-server/-/sprucebot-skills-kit-server-6.18.1.tgz","fileCount":29,"unpackedSize":51320,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJbmrHHCRA9TVsSAnZWagAARz8QAJMW3QsA6c2lBrthZdFz\n6FnM8SCZEOOtlMyQRwcDB3jklWPWmja2ReLla80+FYVEq04vDurS+IEN1iL8\nLl1TXFZV80xUigBGUyUJVr3lK9XcE+Q687s9cSJuQ8+xMgruX++7YHNNdeq9\n4t1nhV65QFYuxiukGPemiNpQId/Z4OU9ANH0NpOysU+Q6xOBsX8kQUCrU2vB\nn90YzNdavE0joamGNbZoaZ+WbwBMLMbIfSBKuT2rP8wNNZETKvPOoCTsBPvw\n/BNK8ZTZoxoMRkTKrH3+l//sYiz282ERyde8O79SCbWF+n2vWWa58wNMnMJ5\nK7CuqML5gGi3ihpcP0l2fnsUT4mUBOdQc9Io7YNDCGPHtTcKJSVuMLJ0XMgl\nkgdQ7UCVZqYKd5L3XRXn5j096eb9XBLzoRbdJqUWFvxD3qhkOpxTEmpH1EqT\n3xUo6o0h7UVZ8gi/G1E4eVcpclyo9P7mWJCS4cB49y5tmJ4lzv1iC3fYi1Tt\n9ODnDMxl4jiCZ77DHu2BZgauuKh4PoSMub5jHvbvZO8Y0U7myhGI8ruvHVc8\n8lGR2aiNB8PI000iOEf7yLVgdkkLJWYm+WTXGJdx22DeedexS2jlD1teKjBW\np7/B2VXV9zoZwj3ajk2NJzjQMmoHrJtNspf4lzyFM7giqdNJmTaTnPd/LsGa\nmaqS\r\n=ktuH\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC5/KF+6bQ531/p08qTw4pieTLVdvvYuRmnad44vXTU3gIhAL9Xo008y0NfJXQR5VzcGsLa5a/P+awcmQ1rCOQPtphj"}]},"maintainers":[{"email":"brandonminch@gmail.com","name":"bminch"},{"email":"carolyn.selheim@gmail.com","name":"carolynsm"},{"email":"hartigan.hm@gmail.com","name":"hartiganhm"},{"email":"hello@kengoldfarb.com","name":"kengoldfarb"},{"email":"npm@taylorrome.ro","name":"liquidg3"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"scientists@sprucelabs.ai","name":"sprucelabs-ci"}],"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sprucebot-skills-kit-server_6.18.1_1536864710570_0.6782672876851974"},"_hasShrinkwrap":false,"deprecated":"Please use the new package @sprucelabs/spruce-skill-server. See https://github.com/sprucelabsai/workspace.sprucebot-skills-kit for more details and upgrade instructions."}},"readme":"","maintainers":[{"email":"scientists@sprucelabs.ai","name":"sprucelabs-ci"},{"email":"hello@kengoldfarb.com","name":"kengoldfarb"},{"email":"ryan@sprucelabs.ai","name":"ryanrd"},{"email":"npm@taylorrome.ro","name":"liquidg3"}],"time":{"modified":"2022-06-26T23:11:05.849Z","created":"2017-11-08T17:26:46.912Z","0.0.5":"2017-11-08T17:26:46.912Z","0.0.6":"2017-11-08T19:16:40.556Z","0.0.7":"2017-11-09T00:11:18.784Z","0.0.8":"2017-11-09T03:14:20.276Z","0.1.0":"2017-11-09T21:39:44.885Z","0.2.0":"2017-11-13T20:08:07.626Z","0.2.1":"2017-11-14T20:38:35.822Z","0.2.2":"2017-11-14T22:36:06.095Z","0.3.0":"2017-11-14T23:27:08.646Z","0.3.1":"2017-11-15T21:39:52.066Z","0.3.2":"2017-11-16T18:21:15.441Z","0.4.0":"2017-11-18T00:06:16.981Z","0.5.0":"2017-11-19T18:21:25.259Z","0.5.1":"2017-11-20T21:21:34.543Z","0.5.2":"2017-11-22T17:42:16.737Z","0.6.0":"2017-11-27T16:31:07.027Z","0.6.1":"2017-12-13T21:30:03.751Z","0.6.2":"2018-01-05T21:19:22.665Z","1.0.0":"2018-01-21T23:29:08.236Z","1.0.1":"2018-02-02T23:32:15.141Z","1.0.2":"2018-02-11T03:12:01.062Z","1.0.3":"2018-02-15T20:09:30.544Z","1.0.4":"2018-02-15T21:35:38.357Z","1.0.5":"2018-02-17T14:18:48.961Z","1.0.6":"2018-02-19T19:54:47.609Z","1.0.7":"2018-02-28T23:16:45.151Z","1.0.8":"2018-02-28T23:18:36.634Z","1.1.0":"2018-03-02T21:27:41.077Z","1.1.1":"2018-03-06T00:17:21.560Z","2.0.0":"2018-03-12T15:38:40.455Z","5.0.1-test.1":"2018-03-14T16:35:24.786Z","3.0.0":"2018-03-14T22:34:33.964Z","6.3.0-beta.2361777":"2018-04-06T16:57:48.973Z","6.4.0":"2018-04-08T20:53:12.565Z","6.5.0-beta.f6da2e8":"2018-04-11T18:24:02.448Z","6.4.2":"2018-04-11T19:07:24.556Z","6.6.0-beta.15b47a1":"2018-04-27T19:55:09.374Z","6.6.0":"2018-04-27T20:25:41.109Z","6.7.0-beta.63485cc":"2018-06-22T20:13:15.978Z","6.7.0":"2018-06-27T02:39:24.844Z","6.8.0-beta.15c28e1":"2018-06-27T16:24:06.108Z","6.14.0-beta.40e5194":"2018-07-09T18:29:40.537Z","6.14.0":"2018-07-09T20:35:17.732Z","6.15.0":"2018-07-11T00:46:47.574Z","6.16.0-beta.e779235":"2018-07-11T01:13:55.293Z","6.16.0-beta.7d965cc":"2018-07-11T05:35:32.751Z","6.16.0":"2018-07-11T11:45:44.297Z","6.18.0":"2018-07-11T18:35:49.392Z","6.19.0-beta.2b0bbf7":"2018-07-11T19:05:07.505Z","6.18.1":"2018-09-13T18:51:50.788Z"},"readmeFilename":"","description":"The engine that drives your Sprucebot Skill! ⚙️🌲🤖","license":"MIT","homepage":"https://github.com/sprucelabsai/sprucebot-skills-kit","keywords":["node","sprucebot","sprucelabs"],"bugs":{"url":"https://github.com/sprucelabsai/sprucebot-skills-kit/issues"}}