[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"commonjs\": true,\n    \"es2021\": true,\n    \"node\": true,\n    \"jest\": true\n  },\n  \"extends\": [\n    \"airbnb-base\"\n  ],\n  \"parserOptions\": {\n    \"ecmaVersion\": 12\n  },\n  \"rules\": {\n    \"no-console\": \"off\",\n    \"no-trailing-spaces\": \"off\",\n    \"semi\": \"off\",\n    \"object-shorthand\": \"off\",\n    \"no-unused-vars\": \"warn\",\n    \"no-useless-catch\": \"warn\",\n    \"no-underscore-dangle\": \"off\",\n    \"no-await-in-loop\": \"warn\",\n    \"no-else-return\": \"off\",\n    \"camelcase\": \"off\",\n    \"no-restricted-syntax\": \"warn\",\n    \"prefer-destructuring\": \"warn\",\n    \"no-continue\": \"warn\"\n  }\n}"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ master ]\n # pull_request:\n #   branches: [ master ]\n\njobs:\n  build:\n\n    strategy:\n      matrix:\n        platform: [ubuntu-latest]\n    runs-on: ${{ matrix.platform }}\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Use Node.js 10.x\n      uses: actions/setup-node@v1\n      with:\n        node-version: 10.x \n    - name: Install\n      run: npm ci                         # beware of dashes (-) before run\n    - name: Test                                        \n      run: npm run test\n      env:                                              # env is only kept in this step\n        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} \n        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n        AWS_REGION: ${{ secrets.AWS_REGION }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\ncoverage/\ndde/lamb/\ndde/lambs/\nnode_modules/\nultra/zoo/\ndist/\nhyperform.json\n.env"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021 Hyperform\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "\n\n![Hyperform Banner](https://github.com/qngapparat/hyperform/blob/master/hyperform-banner.png)\n\n\n> ⚡ Lightweight serverless framework for NodeJS\n\n* **Unopinionated** (Any JS code works)\n* **Lightweight** (no wrapping)\n* **1-click deploy** (1 command)\n* **Multi-Cloud** (for AWS & Google Cloud)\n* **Maintains** (provider's conventions)\n\n\n## Install\n\n```sh\n$ npm install -g hyperform-cli\n```\n\n\n\n## Usage\n\n\n* Everything works like a normal NodeJS app. You can use NPM packages, external files, assets, since the entire folder containing `hyperform.json` is included with each function.\n\n### AWS Lambda\n\n\n```js\n// somefile.js\n\n// AWS Lambda uses 'event', 'context', and 'callback'  convention\n// Learn more: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html\n\nexports.foo = (event, context, callback) => {\n  context.succeed({\n    message: \"I'm Foo on AWS Lambda!\"\n  })\n}\n\nexports.bar = (event, context, callback) => {\n  context.succeed({\n    message: \"I'm Bar on AWS Lambda!\"\n  })\n}\n\n// ... \n```\n\nCreate a `hyperform.json` in the current folder, with your AWS credentials:\n\n```json \n{\n  \"amazon\": {\n    \"aws_access_key_id\": \"...\",\n    \"aws_secret_access_key\": \"...\",\n    \"aws_region\": \"...\"\n  }\n}\n```\n\nIn the terminal, type:\n\n``` \n$ hyperform deploy somefile.js --amazon --url\n  > 🟢 foo https://w3g434h.execute-api.us-east-2.amazonaws.com/foo\n  > 🟢 bar https://w3g434h.execute-api.us-east-2.amazonaws.com/bar\n\n```\n\n... and your functions are deployed & invocable via `GET` and `POST`.\n\n\n### Google Cloud Functions\n\n\n```js\n// somefile.js\n\n// Google Cloud uses Express's 'Request' and 'Response' convention\n// Learn more: https://expressjs.com/en/api.html#req \n//             https://expressjs.com/en/api.html#res\n\nexports.foo = (req, res) => {\n  let message = req.query.message || req.body.message || \"I'm a Google Cloud Function, Foo\";\n  res.status(200).send(message);\n};\n\nexports.bar = (req, res) => {\n  let message = req.query.message || req.body.message || \"I'm a Google Cloud Function, Bar\";\n  res.status(200).send(message);\n};\n```\n\n\nCreate a `hyperform.json` in the current folder with your Google Cloud credentials:\n\n```json \n{\n  \"google\": {\n    \"gc_project\": \"...\",\n    \"gc_region\": \"...\",\n  }\n}\n```\n\nIn the terminal, type:\n\n``` \n$ hyperform deploy somefile.js --google --url    \n  > 🟢 foo https://us-central1-someproject-153dg2.cloudfunctions.net/foo \n  > 🟢 bar https://us-central1-someproject-153dg2.cloudfunctions.net/bar \n\n```\n\n... and your functions are deployed & invocable via `GET` and `POST`.\n\n## Hints & Caveats\n\n* New functions are deployed with 256MB RAM, 60s timeouts \n* The flag `--url` creates **unprotected** URLs to the functions. Anyone with these URLs can invoke your functions\n* The entire folder containing `hyperform.json` will be deployed with each function, so you can use NPM packages, external files (...) just like normal.\n\n\n\n### FAQ\n\n**Where are functions deployed to?**\n\n* On AWS: To AWS Lambda\n* On Google Cloud: To Google Cloud Functions\n\n**Where does deployment happen?**\n\nIt's a client-side tool, so on your computer. It uses the credentials it finds in `hyperform.json`\n\n\n**Can I use NPM packages, external files, (...) ?**\n\nYes. The entire folder where `hyperform.json` is is uploaded, excluding `.git`, `.gitignore`, `hyperform.json`, and for Google Cloud `node_modules` (Google Cloud installs NPM dependencies freshly from `package.json`). So everything works like a normal NodeJS app.\n\n**How does `--url` create URLs?**\n\nOn AWS, it creates an API Gateway API (called `hf`), and a `GET` and `POST` route to your function. \n\nOn Google Cloud, it removes IAM checking from the function by adding `allUsers` to the group \"Cloud Functions Invoker\" of that function.\n\nNote that in both cases, **anyone with the URL can invoke your function. Make sure to add Authentication logic inside your function**, if needed. \n\n\n\n## Opening Issues\n\nFeel free to open issues if you find bugs.\n\n## Contributing\n\nAlways welcome ❤️ Please see CONTRIBUTING.md\n\n## License\n\nApache 2.0\n"
  },
  {
    "path": "authorizer-gen/index.js",
    "content": "// const AWS = require('aws-sdk')\n// const { deployAmazon } = require('../deployer/amazon/index')\n// const { allowApiGatewayToInvokeLambda } = require('../publisher/amazon/utils')\n// const { zip } = require('../zipper/index')\n// const { ensureBearerTokenSecure } = require('./utils')\n// const { logdev } = require('../printers/index')\n\n// AWS.config.update({\n//   accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n//   secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n//   region: process.env.AWS_REGION, \n// })\n\n// /**\n//  * @description Creates or updates Authorizer lambda with name \"authorizerName\" \n//  * that if used as Authorizer in API Gateway, will\n//  * greenlight requests with given expectedBearer token\n//  * @param {string} authorizerName For example 'myfn-authorizer'\n//  * @param {string} expectedBearer The 'Authorization': 'Bearer ...' token \n//  * the Authorizer will greenlight\n//  * @param {{region: string}} options \n//  * @returns {Promise<string>} ARN of the deployed authorizer lambda\n//  */\n// async function deployAuthorizerLambda(authorizerName, expectedBearer, options) {\n//   if (options == null || options.region == null) { \n//     throw new Error('optionsregion is required') // TODO HF programmer mistake\n//   }\n \n//   // will mess up weird user-given Tokens but that's on the user\n//   // will lead to false negatives (still better than false positives or injections)\n//   // This should not be needed, as expectedBearer is generated by our code, but to be sure\n//   const sanitizedExpectedBearer = encodeURI(expectedBearer)\n//   ensureBearerTokenSecure(sanitizedExpectedBearer)\n\n//   const authorizerCode = `\n//   exports.handler = async(event) => {\n//     const expected = \\`Bearer ${sanitizedExpectedBearer}\\`\n//     const isAuthorized = (event.headers.authorization === expected)\n//     return {\n//       isAuthorized\n//     }\n//   };\n//   `\n//   // TODO do this private somehow, in RAM, so that no program can tamper with authorizer zip\n//   const zipPath = await zip(authorizerCode)\n\n//   const deployOptions = {\n//     name: authorizerName,\n//     timeout: 1, // 1 second is ample time\n//     handler: 'index.handler',\n//     region: options.region,  \n//   }\n\n//   // create or update Authorizer Lambda\n//   const authorizerArn = await deployAmazon(zipPath, deployOptions)\n\n//   await allowApiGatewayToInvokeLambda(authorizerName, options.region)\n  \n//   return authorizerArn\n// }\n\n// /**\n//  * @description Gets the RouteId of a route belonging to an API on API Gateway\n//  * @param {string} apiId Id of the API in API Gateway\n//  * @param {string} routeKey For example '$default'\n//  * @param {string} region Region of the API in API Gateway\n//  * @returns {Promise<string>} RouteId of the route\n//  * @throws If query items did not include a Route named \"routeKey\"\n//  */\n// async function getRouteId(apiId, routeKey, region) {\n//   const apigatewayv2 = new AWS.ApiGatewayV2({\n//     apiVersion: '2018-11-29',\n//     region: region,\n//   })\n\n//   // TODO Amazon might return a paginated response here  (?)\n//   // In that case with many routes, the route we look for may not be on first page\n//   const params = {\n//     ApiId: apiId,\n//     MaxResults: '9999', // string according to docs and it works... uuh?\n//   }\n\n//   const res = await apigatewayv2.getRoutes(params).promise() \n\n//   const matchingRoutes = res.Items.filter((item) => item.RouteKey === routeKey)\n//   if (matchingRoutes.length === 0) {\n//     throw new Error(`Could not get RouteId of apiId, routeKey ${apiId}, ${routeKey}`)\n//   }\n\n//   // just take first one\n//   // Hyperform convention is there's only one with any given name\n//   const routeId = matchingRoutes[0].RouteId\n\n//   return routeId\n// }\n\n// /**\n//  * @description Sets the $default path of \"apiId\" to be guarded by \"authorizerArn\" lambda.\n//  * @param {string} apiId Id of API in API Gateway to be guarded\n//  * @param {string} authorizerArn ARN of Lambda that should act as the authorizer\n//  * @returns {void}\n//  * @throws Throws if authorizerArn is not formed like a Lambda ARN. \n//  * Fails silently if authorizerArn Lambda does not exist.\n//  */\n// async function setDefaultRouteAuthorizer(apiId, authorizerArn, apiRegion) {\n//   // TODO what happens when api (set to REGIONAL) and authorizer lambda are in different regions\n\n//   // region is the fourth field\n//   const authorizerRegion = authorizerArn.split(':')[3] \n//   // name is the last field\n//   const authorizerName = authorizerArn.split(':').slice(-1)[0]\n\n//   const authorizerType = 'REQUEST'\n//   const identitySource = '$request.header.Authorization'\n\n//   const authorizerUri = `arn:aws:apigateway:${authorizerRegion}:lambda:path/2015-03-31/functions/${authorizerArn}/invocations`\n\n//   // Try to create authorizer for that API\n//   // succeeds => Authorizer with that name did not exist yet. Use that authorizerId going forward\n//   // Fails => Authorizer already existed with that name. \n//   // Get that one's authorizerId (Follow Hyperform conv: same name - assume identical)\n\n//   const apigatewayv2 = new AWS.ApiGatewayV2({\n//     apiVersion: '2018-11-29',\n//     region: apiRegion,\n//   })\n\n//   const createAuthorizerParams = {\n//     ApiId: apiId,\n//     Name: authorizerName,\n//     AuthorizerType: authorizerType,\n//     IdentitySource: [identitySource],\n//     AuthorizerUri: authorizerUri,\n//     AuthorizerPayloadFormatVersion: '2.0',\n//     EnableSimpleResponses: true,\n//   }\n\n//   let authorizerId\n\n//   try {\n//     const createRes = await apigatewayv2.createAuthorizer(createAuthorizerParams).promise()\n//     // authorizer does not exist\n//     authorizerId = createRes.AuthorizerId\n//   } catch (e) {\n//     if (e.code === 'BadRequestException') {\n//       // authorizer already exists\n//       // TODO update authorizer to make sure it points \n//       // ...to authorizerArn lambda (to behave exactly as stated in @description)\n\n//       // TODO pull-up this and/or add update authorizer\n      \n//       // obtain its id\n//       const getAuthorizersParams = {\n//         ApiId: apiId,\n//         MaxResults: '9999',\n//       }\n//       const getRes = await apigatewayv2.getAuthorizers(getAuthorizersParams).promise()\n\n//       const matchingRoutes = getRes.Items.filter((item) => item.Name === authorizerName)\n//       if (matchingRoutes.length === 0) {\n//         throw new Error(`Could not get AuthorizerId of apiId ${apiId}`)\n//       }\n\n//       // just take first one\n//       // Hyperform convention is there's only one with any given name\n//       authorizerId = matchingRoutes[0].AuthorizerId\n//     } else {\n//       // some other error\n//       throw e\n//     }\n//   }\n  \n//   // attach authorizer to $default\n//   const routeKey = '$default'\n//   const routeId = await getRouteId(apiId, routeKey, apiRegion)\n\n//   const updateRouteParams = {\n//     ApiId: apiId,\n//     RouteId: routeId,\n//     AuthorizerId: authorizerId,\n//     AuthorizationType: 'CUSTOM',\n//   }\n//   await apigatewayv2.updateRoute(updateRouteParams).promise()\n//   logdev('set authorizer')\n//   // done\n// }\n\n// /**\n//  * @description Detaches the current authorizer, if any, from the $default route of API \n//  * with ID \"apiId\". The route is then in any case unauthorized and the underlying Lambda becomes \n//  * invokable by anyone with the URL.\n//  * This does not delete the authorizer or the authorizer Lambda.\n//  * @param {string} apiId \n//  * @param {string} apiRegion\n//  */\n// async function detachDefaultRouteAuthorizer(apiId, apiRegion) {\n//   const apigatewayv2 = new AWS.ApiGatewayV2({\n//     apiVersion: '2018-11-29',\n//     region: apiRegion,\n//   })\n\n//   const routeKey = '$default'\n//   const routeId = await getRouteId(apiId, routeKey, apiRegion)\n\n//   const updateRouteParams = {\n//     ApiId: apiId,\n//     RouteId: routeId,\n//     AuthorizationType: 'NONE',\n//   }\n\n//   await apigatewayv2.updateRoute(updateRouteParams).promise()\n//   logdev('detached authorizer ')\n// }\n// // TODO set authorizer cache ??\n\n// module.exports = {\n//   deployAuthorizerLambda,\n//   setDefaultRouteAuthorizer,\n//   detachDefaultRouteAuthorizer,\n//   _only_for_testing_getRouteId: getRouteId,\n// }\n"
  },
  {
    "path": "authorizer-gen/index.oldtest.js",
    "content": "// /* eslint-disable global-require */\n// const LAMBDANAME = 'jest-reserved-authorizer'\n// const LAMBDAREGION = 'us-east-2'\n// const APIREGION = 'us-east-2'\n// const LAMBDAARN = 'arn:aws:lambda:us-east-2:735406098573:function:jest-reserved-authorizer'\n// const BEARERTOKEN = 'somelengthyrandombearertoken1234567890'\n\n// // NOTE Currently convention is one API per endpoint\n// // Don't extend tests until we are sure of this convention / committed to\n\n// describe('authorizer-gen', () => {\n//   describe('index', () => {\n//     describe('getRouteId', () => {\n//       test('returns non-empty string for existing route', async () => {\n//         const getRouteId = require('./index')._only_for_testing_getRouteId\n\n//         const apiId = 'vca3i8138h' // first-http-api in my API Gateway\n//         const routeKey = '$default'\n\n//         let err \n//         let res \n//         try {\n//           res = await getRouteId(apiId, routeKey, LAMBDAREGION)\n//         } catch (e) {\n//           console.log(e)\n//           err = e\n//         }\n\n//         // It did not throw\n//         expect(err).not.toBeDefined()\n//         // Returned routeid: a non-empty string\n//         expect(res).toBeDefined()\n//         expect(typeof res).toEqual('string')\n//         expect(res.length > 0).toEqual(true)\n//       })\n\n//       test('throws for non-existing API', async () => {\n//         const getRouteId = require('./index')._only_for_testing_getRouteId\n\n//         const apiId = 'invalid-api-id' \n//         const routeKey = '$default'\n\n//         let err \n//         let res \n//         try {\n//           res = await getRouteId(apiId, routeKey, LAMBDAREGION)\n//         } catch (e) {\n//           err = e\n//         }\n//         // It threw\n//         expect(err).toBeDefined()\n//         expect(err.toString()).toMatch(/Invalid API identifier/)\n//       })\n\n//       test('throws for non-existing route', async () => {\n//         const getRouteId = require('./index')._only_for_testing_getRouteId\n\n//         const apiId = 'vca3i8138h' // first-http-api in my API Gateway\n//         const routeKey = 'invalid-route-name'\n\n//         let err \n//         let res \n//         try {\n//           res = await getRouteId(apiId, routeKey, LAMBDAREGION)\n//         } catch (e) {\n//           err = e\n//         }\n\n//         // It threw\n//         expect(err).toBeDefined()\n//       })\n//     })\n\n//     describe('deployAuthorizerLambda', () => {\n//       test('throws on expectedBearer shorter than 10 digits (not secure)', async () => {\n//         const { deployAuthorizerLambda } = require('./index')\n        \n//         const expectedBearer = '123456789  ' \n//         const options = {\n//           region: LAMBDAREGION,\n//         }\n//         let err \n//         try {\n//           await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n//         } catch (e) {\n//           err = e\n//         }\n\n//         expect(err).toBeDefined()\n//       })\n\n//       test('throws on expectedBearer is null', async () => {\n//         const { deployAuthorizerLambda } = require('./index')\n        \n//         const expectedBearer = null \n//         const options = {\n//           region: LAMBDAREGION,\n//         }\n//         let err \n//         try {\n//           await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n//         } catch (e) {\n//           err = e\n//         }\n\n//         expect(err).toBeDefined()\n//       })\n        \n//       test('throws on expectedBearer is empty string', async () => {\n//         const { deployAuthorizerLambda } = require('./index')\n       \n//         const expectedBearer = '  ' \n//         const options = {\n//           region: LAMBDAREGION,\n//         }\n//         let err \n//         try {\n//           await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n//         } catch (e) {\n//           err = e\n//         }\n\n//         expect(err).toBeDefined()\n//       })\n\n//       // allow 15 seconds\n\n//       test('completes if authorizer lambda does not exist yet, returns ARN', async () => {\n//         const { deployAuthorizerLambda } = require('./index')\n//         const { deleteAmazon } = require('../deployer/amazon/index')\n        \n//         const expectedBearer = BEARERTOKEN\n//         const options = {\n//           region: LAMBDAREGION,\n//         }\n\n//         /// //////////////////////////////////////////////\n//         // Setup: delete authorizer if it exists already\n\n//         try {\n//           await deleteAmazon(LAMBDANAME, LAMBDAREGION)\n//           // deleted lambda\n//         } catch (e) {\n//           if (e.code === 'ResourceNotFoundException') {\n//             // does not exist in the first place, nice\n//           } else {\n//             throw e\n//           }\n//         }\n\n//         /// ///////////////////////////////////////\n\n//         let err \n//         let res\n//         try {\n//           res = await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n//         } catch (e) {\n//           console.log(e)\n//           err = e\n//         }\n\n//         expect(err).not.toBeDefined()\n//         expect(typeof res).toBe('string')\n//       }, 30 * 1000)\n\n//       test('completes if authorizer lambda exists already, returns ARN', async () => {\n//         const { deployAuthorizerLambda } = require('./index')\n        \n//         const expectedBearer = BEARERTOKEN\n//         const options = {\n//           region: LAMBDAREGION,\n//         }\n\n//         /// ///////////////////////////////////////\n//         // Setup: ensure authorizer lambda exists\n\n//         await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n\n//         /// ///////////////////////////////////////\n\n//         let err \n//         let res\n//         try {\n//           res = await deployAuthorizerLambda(LAMBDANAME, expectedBearer, options)\n//         } catch (e) {\n//           console.log(e)\n//           err = e\n//         }\n\n//         expect(err).not.toBeDefined()\n//         expect(typeof res).toBe('string')\n//       }, 30 * 1000)\n//     })\n\n//     // allow 15 seconds\n//     describe('setDefaultRouteAuthorizer', () => {\n//       test('completes when authorizer exists already', async () => {\n//         const { setDefaultRouteAuthorizer } = require('./index')\n\n//         const apiId = 'vca3i8138h' // first-http-api in my API Gateway\n\n//         let err \n//         try {\n//           await setDefaultRouteAuthorizer(apiId, LAMBDAARN, APIREGION)\n//         } catch (e) {\n//           console.log(e)\n//           err = e\n//         }\n\n//         expect(err).not.toBeDefined()\n//       }, 15 * 1000)\n\n//       test('completes when API has no authorizer yet', async () => {\n//         const { setDefaultRouteAuthorizer, detachDefaultRouteAuthorizer } = require('./index')\n//         const apiId = 'vca3i8138h' // first-http-api in my API Gateway\n\n//         /// ////////////////////////////////////////////////\n//         // Setup: detach current authorizer (if any)\n  \n//         await detachDefaultRouteAuthorizer(apiId, APIREGION)\n\n//         /// /////////////////////////////////////////////////\n       \n//         let err \n//         try {\n//           await setDefaultRouteAuthorizer(apiId, LAMBDAARN, APIREGION)\n//         } catch (e) {\n//           console.log(e)\n//           err = e\n//         }\n\n//         expect(err).not.toBeDefined()\n//         // \n//       }, 15 * 1000)\n\n//       // TODO test for when authorizer does not exist yet\n//       /// ////////////////////\n//     })\n\n//     test('throws on authorizerArn not being valid ARN format', async () => {\n//       const { setDefaultRouteAuthorizer } = require('./index')\n\n//       const invalidArn = 'INVALID_AMAZON_ARN_FORMAT'\n//       const apiId = 'vca3i8138h' // first-http-api in my API Gateway\n\n//       let err \n//       try {\n//         await setDefaultRouteAuthorizer(apiId, invalidArn, APIREGION)\n//       } catch (e) {\n//         err = e\n//       }\n\n//       expect(err).toBeDefined()\n//     }, 15 * 1000)\n//   })\n// })\n"
  },
  {
    "path": "authorizer-gen/utils.js",
    "content": "// const uuidv4 = require('uuid').v4\n\n// /**\n//  * @description Generates a '''random''' bearer token TODO \n//  * @returns {string} '''Random''' bearer token\n//  */\n// function generateRandomBearerToken() {\n//   const token = uuidv4()\n//     .replace(/-/g, '')\n//   return token\n// }\n\n// /**\n//  * @returns {void}\n//  * @throws if \"bearerToken\" does not fix requirements\n//  */\n// function ensureBearerTokenSecure(bearerToken) {\n//   // messages mostly for us\n//   if (typeof bearerToken !== 'string') throw new Error(`Bearer token must be a string but is ${typeof bearerToken}`)\n//   if (bearerToken.trim().length < 10) throw new Error('Bearer token, trimmed, must be equal longer than 10')\n//   if (/^[a-zA-Z0-9]+$/.test(bearerToken) === false) throw new Error('Bearer token must fit regex /^[a-zA-Z0-9]+$/ (alphanumeric)')\n// }\n\n// module.exports = {\n//   generateRandomBearerToken,\n//   ensureBearerTokenSecure,\n// }\n"
  },
  {
    "path": "authorizer-gen/utils.oldtest.js",
    "content": "// /* eslint-disable global-require */\n// describe('authorizer-gen', () => {\n//   describe('utils', () => {\n//     describe('generateRandomBearerToken', () => {\n//       test('is between 0 and 50 characters', () => {\n//         const { generateRandomBearerToken } = require('./utils')\n\n//         const output = generateRandomBearerToken()\n\n//         expect(output.length).toBeDefined()\n//         expect(output.length <= 50).toEqual(true)\n//         expect(output.length > 0).toEqual(true)\n//       })\n\n//       test('is alphanumeric', () => {\n//         const { generateRandomBearerToken } = require('./utils')\n\n//         const output = generateRandomBearerToken() \n//         const regex = /^[a-zA-Z0-9]+$/ \n//         expect(regex.test(output)).toEqual(true)\n//       })\n//     })\n//   })\n// })\n"
  },
  {
    "path": "bundler/amazon/index.js",
    "content": "const { _bundle } = require('../utils')\n/**\n * @description Bundles a given .js files for Amazon with its dependencies using webpack.\n * @param {string} inpath Path to entry .js file\n * @returns {Promise<string>} The bundled code\n */\nasync function bundleAmazon(inpath) {\n  const externals = {\n    'aws-sdk': 'aws-sdk',\n  }\n  const res = await _bundle(inpath, externals)\n  return res\n}\n\nmodule.exports = {\n  bundleAmazon,\n}\n"
  },
  {
    "path": "bundler/google/index.js",
    "content": "const fs = require('fs')\nconst { _bundle } = require('../utils')\n/**\n * @description Bundles a given .js files for Google using Webpack. IMPORTANT: excludes any npm packages. \n * @param {string} inpath Path to entry .js file\n * @returns {Promise<string>} The bundled code\n */\nasync function bundleGoogle(inpath) {\n  // Exclude any npm packages (if present)\n  // From https://github.com/webpack/webpack/issues/603#issuecomment-180509359\n  const externals = fs.existsSync('node_modules') && fs.readdirSync('node_modules')\n  \n  // TODO check if @google is included on Google?\n  const res = await _bundle(inpath, externals)\n  return res\n}\n\nmodule.exports = {\n  bundleGoogle,\n}\n"
  },
  {
    "path": "bundler/utils.js",
    "content": "const webpack = require('webpack')\nconst path = require('path')\nconst fsp = require('fs').promises\nconst os = require('os')\nconst { log } = require('../printers/index')\n\n/**\n * @description Bundles a given .js files with its dependencies using webpack. \n * Does not include dependencies that are given in \"externals\".\n * @param {string} inpath Path to entry .js file\n * @param {*} externals Webpack 'externals' field of package names we don't need to bundle. \n * For example { 'aws-sdk': 'aws-sdk' } to skip 'aws-sdk'\n * @returns {Promise<string>} The bundled code\n */\nasync function _bundle(inpath, externals) {\n  // create out dir (silly webpack)\n  const outdir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bundle-'))\n  const outpath = path.join(outdir, 'bundle.js')\n  \n  // console.log(`bundling to ${outpath}`)\n  return new Promise((resolve, reject) => {\n    webpack(\n      {\n        mode: 'development',\n        entry: inpath,\n        target: 'node',\n        output: {\n          path: outdir,\n          filename: 'bundle.js',\n          // so amazon sees it\n          libraryTarget: 'commonjs',\n          // Make webpack perform identical as node\n          // See https://github.com/node-fetch/node-fetch/issues/450#issuecomment-494475397\n          // extensions: ['.js'],\n          // mainFields: ['main'],\n\n          // Fixes \"global\"\n          // See https://stackoverflow.com/a/64639975\n          //     globalObject: 'this',\n        },\n        // aws-sdk is already provided in lambda\n        externals: externals,\n      },\n      (err, stats) => {\n        if (err || stats.hasErrors() || (stats.compilation.errors.length > 0)) {\n          // always show bundling error it's useful\n          log(`Bundling ${inpath} did not work: `)\n          log(stats.compilation.errors)\n          reject(err, stats)\n        } else {\n          // return the bundle code\n          fsp.readFile(outpath, { encoding: 'utf8' })\n            .then((code) => resolve(code))\n          // TODO clean up file \n          // TODO do in-memory \n        }\n      },\n    )\n  })\n}\n\nmodule.exports = {\n  _bundle,\n}\n"
  },
  {
    "path": "bundler/utils.test.js",
    "content": "const fsp = require('fs').promises\nconst os = require('os')\nconst path = require('path')\nconst uuidv4 = require('uuid').v4 \nconst { _bundle } = require('./utils')\n\ndescribe('bundler', () => {\n  test('does not throw on empty js file & returns string', async () => {\n    const tmpd = await fsp.mkdtemp(path.join(os.tmpdir(), 'bundle-'))\n    const inpath = path.join(tmpd, 'index.js')\n    const filecontents = ' '\n    await fsp.writeFile(inpath, filecontents)\n\n    let err;\n    let res\n    try {\n      res = await _bundle(inpath)\n    } catch (e) {\n      console.log(e)\n      err = e\n    }\n\n    expect(err).not.toBeDefined()\n    expect(typeof res === 'string').toBe(true)\n  })\n\n  test('does not throw on js file & returns string', async () => {\n    const tmpd = await fsp.mkdtemp(path.join(os.tmpdir(), 'bundle-'))\n    const code = 'module.exports.inc = (x) => x + 1'\n    const inpath = path.join(tmpd, 'index.js')\n      \n    await fsp.writeFile(inpath, code)\n\n    let err;\n    let res\n    try {\n      res = await _bundle(inpath)\n    } catch (e) {\n      console.log(e)\n      err = e\n    }\n\n    expect(err).not.toBeDefined()\n    expect(typeof res === 'string').toBe(true)\n  })\n\n  test('throws on invalid input path', async () => {\n    const code = 'module.exports.inc = (x) => x + 1'\n    const invalidinpath = path.join(os.tmpdir(), `surely-this-path-does-not-exist-${uuidv4()}`)\n    \n    let err;\n    let res\n    try {\n      res = await _bundle(invalidinpath)\n      console.log(res)\n    } catch (e) {\n      err = e\n    }\n\n    expect(err).toBeDefined()\n  })\n})\n"
  },
  {
    "path": "cli.js",
    "content": "#!/usr/bin/env node\nconst path = require('path')\nconst fs = require('fs')\nconst semver = require('semver')\nconst { init, initDumb } = require('./initer/index')\nconst { getParsedHyperformJson } = require('./parser/index')\nconst { log } = require('./printers/index')\nconst { maybeShowSurvey, answerSurvey } = require('./surveyor/index')\nconst packagejson = require('./package.json')\n\n// Ingest CLI arguments\n// DEV NOTE: Keep it brief and synchronious\n\nconst args = process.argv.slice(2)\n\n// Check node version\nconst version = packagejson.engines.node \nif (semver.satisfies(process.version, version) !== true) {\n  console.log(`Hyperform needs node ${version} or newer, but version is ${process.version}.`);\n  process.exit(1);\n}\n\nif (\n  (/deploy/.test(args[0]) === false) \n || ((args.length === 1)) \n|| (args.length === 2)\n|| (args.length === 3 && args[2] !== '--amazon' && args[2] !== '--google') \n|| (args.length === 4 && ([args[2], args[3]].includes('--url') === false))\n|| args.length >= 5) {\n  log(`Usage: \n $ hf deploy ./some/file.js --amazon  # Deploy exports to AWS Lambda\n $ hf deploy ./some/file.js --google  # Deploy exports to Google Cloud Functions\n`)\n  process.exit(1)\n}\n\n// $ hf MODE FPATH [--url]\n// const mode = args[0]\nconst fpath = args[1]\nconst isPublic = (args.length === 4 \n  ? ([args[2], args[3]].includes('--url'))\n  : false)\n\nconst currdir = process.cwd() \n\nlet platform \nif (args[2] === '--amazon') platform = 'amazon'\nif (args[2] === '--google') platform = 'google'\n\n// // Mode is init\n// if (mode === 'init') {\n//   initDumb(currdir)\n//   process.exit()\n// }\n\n// Mode is answer survey\n// if (mode === 'answer') {\n//   const answer = args.slice(1) // words after $ hf answer\n//   // Send anonymous answer (words and date recorded)\n//   answerSurvey(answer)\n//     .then(process.exit())\n// }\n\n// Mode is deploy\n\n// try to read hyperform.json\nconst hyperformJsonExists = fs.existsSync(path.join(currdir, 'hyperform.json'))\nif (hyperformJsonExists === false) {\n  if (platform === 'amazon') {\n    log(`No hyperform.json found in current directory. Create it with these fields:\n      \n    {\n      \"amazon\": {\n          \"aws_access_key_id\": \"...\",\n          \"aws_secret_access_key\": \"...\",\n          \"aws_region\": \"...\"\n      }\n    }\n      \n      `)\n  }\n\n  if (platform === 'google') {\n    log(`No hyperform.json found in current directory. Create it with these fields:\n      \n    { \n      \"google\": {\n        \"gc_project\": \"...\",\n        \"gc_region\": \"...\",\n      }\n    }\n    \n    `)\n  }\n  process.exit(1)\n}\n// parse and validate hyperform.json\nconst parsedHyperformJson = getParsedHyperformJson(currdir, platform)\n  \n// Dev Note: Do this as early as possible\n  \n// Load AWS Credentials from hyperform.json into process.env\n// These are identical with variables that Amazon CLI uses, so they may be set\n// However, that is fine, hyperform.json should still take precedence\nif (parsedHyperformJson.amazon != null) {\n  process.env.AWS_ACCESS_KEY_ID = parsedHyperformJson.amazon.aws_access_key_id,\n  process.env.AWS_SECRET_ACCESS_KEY = parsedHyperformJson.amazon.aws_secret_access_key,\n  process.env.AWS_REGION = parsedHyperformJson.amazon.aws_region\n  // may, may not be defined.\n  process.env.AWS_SESSION_TOKEN = parsedHyperformJson.amazon.aws_session_token\n}\n  \n// Load GC Credentials from hyperform.json into process.env\n// These are different from what Google usually occupies (GCLOUD_...)\nif (parsedHyperformJson.google != null) {\n  process.env.GC_PROJECT = parsedHyperformJson.google.gc_project\n  process.env.GC_REGION = parsedHyperformJson.google.gc_region\n}\n\n// Top-level error boundary\ntry {\n  // Main\n  // Do not import earlier, it needs to absorb process.env set above\n  // TODO: make less sloppy\n  const { main } = require('./index')\n  main(currdir, fpath, platform, parsedHyperformJson, isPublic)\n  // show anonymous survey question with 1/30 probability\n  //  .then(() => maybeShowSurvey())\n} catch (e) {\n  log(e)\n  process.exit(1)\n}\n"
  },
  {
    "path": "copier/index.js",
    "content": "const { ncp } = require('ncp')\nconst path = require('path')\nconst os = require('os')\nconst fsp = require('fs').promises\n/**\n * Creates a copy of a directory in /tmp.\n * @param {string} dir\n * * @param {string[]} except names of directories or files that will not be included \n * (usually [\"node_modules\", \".git\", \".github\"]) Uses substring check.\n * @returns {string} outpath\n */\nasync function createCopy(dir, except) {\n  const outdir = await fsp.mkdtemp(path.join(os.tmpdir(), 'copy-'))\n \n  // See https://www.npmjs.com/package/ncp\n\n  // concurrency limit\n  ncp.limit = 16 \n\n  // Function that is called on every file / dir to determine if it'll be included in the zip\n  const filterFunc = (p) => {\n    for (let i = 0; i < except.length; i += 1) {\n      if (p.includes(except[i])) {\n        console.log(`Excluding ${p}`)\n        return false \n      }\n    }\n    return true\n  }\n\n  const res = await new Promise((resolve, rej) => {\n    ncp(\n      dir,\n      outdir,\n      {\n        filter: filterFunc,\n      },       \n      (err) => {\n        if (err) {\n          rej(err)\n        } else {\n          resolve(outdir)\n        }\n      },\n    );\n  })\n\n  return res \n}\n\nmodule.exports = {\n  createCopy,\n}\n"
  },
  {
    "path": "deployer/amazon/index.js",
    "content": "const {\n  createLambda,\n  deleteLambda,\n  updateLambdaCode,\n  createLambdaRole,\n  isExistsAmazon, \n} = require('./utils')\nconst { logdev } = require('../../printers/index')\n\n/**\n * @description If Lambda \"options.name\" does not exist yet in \"options.region\", \n * it deploys a new Lambda with given code (\"pathToZip\") and \"options\". \n * If Lambda exists, it just updates its code with \"pathToZip\", \n * and ignores all options except \"options.name\" and \"options.region\"\n * @param {*} pathToZip Path to the zipped Lambda code\n * @param {{\n * name: string, \n * region: string,\n * ram?: number,\n * timeout?: number,\n * handler?: string\n * }} options \n * @returns {Promise<string>} The Lambda ARN\n */\nasync function deployAmazon(pathToZip, options) {\n  if (!options.name || !options.region) {\n    throw new Error(`name and region must be specified, but are ${options.name}, ${options.region}`) // HF programmer mistake\n  }\n\n  const existsOptions = {\n    name: options.name,\n    region: options.region,\n  }\n  // check if lambda exists \n  const exists = await isExistsAmazon(existsOptions)\n\n  logdev(`amazon isexists ${options.name} : ${exists}`)\n  // if not, create new role \n  const roleName = `hf-${options.name}`\n  const roleArn = await createLambdaRole(roleName)\n\n  /* eslint-disable key-spacing */\n  const fulloptions = {\n    name:           options.name,\n    region:         options.region,\n    role:           roleArn,\n    runtime:        'nodejs12.x',\n    timeout:        options.timeout || 60, // also prevents 0\n    ram:            options.ram || 128,\n    handler:        options.handler || `index.${options.name}`,\n  }\n  /* eslint-enable key-spacing */\n  \n  // anonymous function that when run, creates or updates Lambda\n  let upload\n  if (exists === true) {\n    upload = async () => updateLambdaCode(pathToZip, fulloptions)\n  } else {\n    upload = async () => createLambda(pathToZip, fulloptions)\n  }\n\n  // Helper\n  const sleep = async (millis) => new Promise((resolve) => {\n    setTimeout(() => {\n      resolve()\n    }, millis);\n  })\n\n  // Retry loop (4 times). Usually it fails once or twice \n  // if role is newly created because it's too fresh\n  // See: https://stackoverflow.com/a/37503076\n\n  let arn \n\n  for (let i = 0; i < 4; i += 1) {\n    try {\n      logdev('trying to upload to amazon')\n      arn = await upload()\n      logdev('success uploading to amazon')\n      break // we're done\n    } catch (e) {\n      // TODO write test that enters here, reliably\n      if (e.code === 'InvalidParameterValueException') {\n        logdev('amazon deploy threw InvalidParameterValueException (role not ready yet). Retrying in 3 seconds...')\n        await sleep(3000) // wait 3 seconds\n        continue\n      } else {\n        logdev(`Amazon upload errorred: ${e}`)\n        logdev(JSON.stringify(e, null, 2))\n        throw e;\n      }\n    }\n  }\n\n  console.timeEnd(`Amazon-deploy-${options.name}`)\n\n  return arn\n}\n\n/**\n * @description Deletes a Lambda function in a given region.\n * @param {string} name Name, ARN or partial ARN of the function\n * @param {string} region Region of the function\n * @throws ResourceNotFoundException, among others\n */\nasync function deleteAmazon(name, region) { \n  await deleteLambda(name, region)\n}\n\nmodule.exports = {\n  deployAmazon,\n  deleteAmazon,\n}\n"
  },
  {
    "path": "deployer/amazon/index.test.js",
    "content": "/* eslint-disable global-require */\n\nconst LAMBDANAME = 'jest-reserved-returna1'\nconst LAMBDAREGION = 'us-east-2'\n\n// After all tests, delete the Lambda\nafterAll(async () => {\n  const { deleteAmazon } = require('./index')\n  try {\n    await deleteAmazon(LAMBDANAME, LAMBDAREGION)\n  } catch (e) {\n    /* tests themselves already deleted the Lambda */\n  }\n}) \n\n// Helpers \nconst arnRegex = /arn:(aws[a-zA-Z-]*)?:lambda:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:\\d{12}:function:[a-zA-Z0-9-_]+(:(\\$LATEST|[a-zA-Z0-9-_]+))?/\nconst isArn = (str) => (typeof str === 'string') && (arnRegex.test(str) === true)\n\ndescribe('deployer', () => {\n  describe('amazon', () => {\n    describe('deployAmazon', () => {\n      test('completes if Lambda does not exist, and returns ARN', async () => {\n        const { deleteAmazon, deployAmazon } = require('./index.js')\n        const { zip } = require('../../zipper/index')\n\n        /// //////////////////////////////////////////////\n        // Setup: delete function if it exists already\n\n        try {\n          await deleteAmazon(LAMBDANAME, LAMBDAREGION)\n          // deleted function\n        } catch (e) {\n          if (e.code === 'ResourceNotFoundException') {\n            // does not exist in the first place, nice\n          } else {\n            throw e\n          }\n        }\n\n        /// //////////////////////////////////////////////\n        // Setup: create code zip\n\n        const code = `module.exports = { ${LAMBDANAME}: () => ({a: 1}) }`\n        const zipPath = await zip({\n          'index.js': code, \n        })\n \n        /// ////////////////////////////////////////////////////\n \n        const options = {\n          name: LAMBDANAME,\n          region: LAMBDAREGION,\n        }\n \n        let err \n        let lambdaArn \n        try {\n          lambdaArn = await deployAmazon(zipPath, options)\n        } catch (e) {\n          console.log(e)\n          err = e\n        }\n \n        // it completed\n        expect(err).not.toBeDefined()\n \n        // it's an ARN\n        expect(isArn(lambdaArn)).toBe(true)\n\n        // TODO\n      }, 15 * 1000)\n\n      test('completes if Lambda already exists, and returns ARN', async () => {\n        const { deployAmazon } = require('./index')\n        const { zip } = require('../../zipper/index')\n        \n        /// //////////////////////////////////////////////\n        // Setup: create code zip\n        \n        const code = `module.exports = { ${LAMBDANAME}: () => ({a: 1}) }`\n        \n        const zipPath = await zip({\n          'index.js': code,\n        })\n\n        /// //////////////////////////////////////////////\n        // Setup: ensure function exists \n        \n        const options = {\n          name: LAMBDANAME,\n          region: LAMBDAREGION,\n        }\n        try {\n          await deployAmazon(zipPath, options)\n        } catch (e) {\n          if (e.code === 'ResourceConflictException') {\n            /* exists already anyway, cool */\n          } else {\n            throw e\n          }\n        }\n\n        /// ////////////////////////////////////////////////////\n        // Actual test\n\n        let err \n        let lambdaArn \n        try {\n          lambdaArn = await deployAmazon(zipPath, options)\n        } catch (e) {\n          console.log(e)\n          err = e\n        }\n\n        // it completed\n        expect(err).not.toBeDefined()\n\n        // it's an ARN\n        expect(isArn(lambdaArn)).toBe(true)\n      }, 30 * 1000)\n    })\n\n    // NOTE if we're short on API calls we can sacrifice this:\n    // describe('isExistsAmazon', () => {\n    //   test('returns true on existing Lambda in name, region', async () => {\n    //     const { isExistsAmazon } = require('./utils')\n    //     const { deployAmazon } = require('./index')\n    //     const { zip } = require('../../zipper/index')\n        \n    //     /// //////////////////////////////////////////////\n    //     // Setup: create code zip\n        \n    //     const code = `module.exports = { ${LAMBDANAME}: () => ({a: 1}) }`\n        \n    //     const zipPath = await zip(code)\n\n    //     /// //////////////////////////////////////////////\n    //     // Setup: ensure function exists \n        \n    //     const deployOptions = {\n    //       name: LAMBDANAME,\n    //       region: LAMBDAREGION,\n    //     }\n    //     try {\n    //       await deployAmazon(zipPath, deployOptions)\n    //     } catch (e) {\n    //       if (e.code === 'ResourceConflictException') {\n    //         /* exists already anyway, cool */\n    //       } else {\n    //         throw e\n    //       }\n    //     }\n\n    //     let err \n    //     let res \n    //     try {\n    //       res = await isExistsAmazon({ name: LAMBDANAME, region: LAMBDAREGION })\n    //     } catch (e) {\n    //       err = e\n    //     }\n\n    //     expect(err).not.toBeDefined()\n    //     expect(typeof res).toBe('boolean')\n    //     expect(res).toEqual(true)\n    //   })\n\n    //   test('returns false on non-existing Lambda in name, region', async () => {\n    //     const { isExistsAmazon } = require('./utils')\n    //     const uuidv4 = require('uuid').v4 \n\n    //     const options = {\n    //       name: `some-invalid-lambda-name-${uuidv4()}`,\n    //       region: LAMBDAREGION, // or whatever\n    //     }\n\n    //     let err \n    //     let res \n    //     try {\n    //       res = await isExistsAmazon(options)\n    //     } catch (e) {\n    //       err = e\n    //     }\n\n    //     expect(err).not.toBeDefined()\n    //     expect(typeof res).toBe('boolean')\n    //     expect(res).toEqual(false)\n    //   })\n    // })\n\n    // TODO more tests for the other methods\n  })\n})\n"
  },
  {
    "path": "deployer/amazon/utils.js",
    "content": "const util = require('util');\nconst exec = util.promisify(require('child_process').exec);\nconst AWS = require('aws-sdk')\nconst fsp = require('fs').promises\nconst { logdev } = require('../../printers/index')\n\nconst conf = {\n  accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n  region: process.env.AWS_REGION, \n  // may, may not be defined\n  // sessionToken: process.env.AWS_SESSION_TOKEN || undefined, \n}\n\nif (process.env.AWS_SESSION_TOKEN != null && process.env.AWS_SESSION_TOKEN !== 'undefined') {\n  conf.sessionToken = process.env.AWS_SESSION_TOKEN\n}\n\nAWS.config.update(conf)\n\n/**\n * @description Creates a new Lambda with given function code and options\n * @param {string} pathToZip Path to the zipped Lambda code\n * @param {{\n * name: string, \n * region: string,\n * role: string,\n * runtime: string,\n * timeout: number,\n * ram: number,\n * handler: string\n * }} options\n * @returns {Promise<string>} The ARN of the Lambda\n * @throws If Lambda exists or creation did not succeed\n */\nasync function createLambda(pathToZip, options) {\n  const lambda = new AWS.Lambda({\n    region: options.region,\n    apiVersion: '2015-03-31',\n  })\n\n  const zipContents = await fsp.readFile(pathToZip)\n  const params = {\n    Code: {\n      ZipFile: zipContents,\n    },\n    FunctionName: options.name,\n    Timeout: options.timeout,\n    Role: options.role,\n    MemorySize: options.ram, \n    Handler: options.handler, \n    Runtime: options.runtime,\n  }\n\n  const res = await lambda.createFunction(params).promise()\n  const arn = res.FunctionArn\n\n  return arn\n}\n\n/**\n * @description Deletes a Lambda function in a given region.\n * @param {string} name Name, ARN or partial ARN of the function\n * @param {string} region Region of the function\n * @throws ResourceNotFoundException, among others\n */\nasync function deleteLambda(name, region) { \n  const lambda = new AWS.Lambda({\n    region: region, \n    apiVersion: '2015-03-31',\n  })\n\n  const params = {\n    FunctionName: name, \n  }\n \n  await lambda.deleteFunction(params).promise()\n}\n\n/**\n * @description Updates a Lambda's function code with a given .zip file\n * @param {string} pathToZip Path to the zipped Lambda code\n * @param {{\n * name: string,\n * region: string\n * }} options \n * @throws If Lambda does not exist or update did not succeed\n * @returns {Promise<string>} The ARN of the Lambda\n */\nasync function updateLambdaCode(pathToZip, options) {\n  const lambda = new AWS.Lambda({\n    region: options.region,\n    apiVersion: '2015-03-31',\n  })\n\n  const zipContents = await fsp.readFile(pathToZip)\n\n  const params = {\n    FunctionName: options.name,\n    ZipFile: zipContents,\n  }\n\n  const res = await lambda.updateFunctionCode(params).promise()\n  const arn = res.FunctionArn\n  \n  return arn\n}\n\n/**\n * @description Creates a new role, and attaches a basic Lambda policy \n * (AWSLambdaBasicExecutionRole) to it. If role with that name \n * exists already, it just attaches the policy to it\n * @param {string} roleName Unique name to be given to the role\n * @returns {Promise<string>} ARN of the created or updated role\n * @see https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/using-lambda-iam-role-setup.html\n */\nasync function createLambdaRole(roleName) {\n  const iam = new AWS.IAM()\n\n  const lambdaPolicy = {\n    Version: '2012-10-17',\n    Statement: [\n      {\n        Effect: 'Allow',\n        Principal: {\n          Service: 'lambda.amazonaws.com',\n        },\n        Action: 'sts:AssumeRole',\n      },\n    ],\n  }\n\n  const createParams = {\n    AssumeRolePolicyDocument: JSON.stringify(lambdaPolicy),\n    RoleName: roleName,\n  }\n\n  let roleArn\n  try {\n    const createRes = await iam.createRole(createParams).promise()\n    // Role did not exist yet\n    roleArn = createRes.Role.Arn\n  } catch (e) {\n    if (e.code === 'EntityAlreadyExists') {\n      // Role with that name already exists\n      // Use that role, proceed normally\n      const getParams = {\n        RoleName: roleName,\n      }\n      logdev(`role with name ${roleName} already exists. getting its arn`)\n      const getRes = await iam.getRole(getParams).promise()\n      roleArn = getRes.Role.Arn \n    } else {\n      // some other error\n      throw e\n    }\n  }\n\n  // Attach a basic Lambda policy to the role (allows writing to cloudwatch logs etc)\n  // Equivalent to in Lambda console, selecting 'Create new role with basic permissions'\n  const policyParams = {\n    PolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',\n    RoleName: roleName,\n  }\n  await iam.attachRolePolicy(policyParams).promise()\n  logdev(`successfully attached AWSLambdaBasicExecutionRole to ${roleName}`)\n\n  return roleArn\n}\n\n/**\n * @description Checks whether a Lambda exists in a given region\n * @param {{\n  * name: string,\n  * region: string\n  * }} options \n  * @returns {Promise<boolean>}\n  */\nasync function isExistsAmazon(options) {\n  const lambda = new AWS.Lambda({\n    region: options.region,\n    apiVersion: '2015-03-31',\n  })\n\n  const params = {\n    FunctionName: options.name,\n  }\n\n  try {\n    await lambda.getFunction(params).promise()\n    return true\n  } catch (e) {\n    if (e.code === 'ResourceNotFoundException') {\n      return false \n    } else {\n      // some other error\n      throw e\n    }\n  }\n}\n\n// TODO\n// /**\n//  * @throws If Lambda does not exist\n//  */\n// async function updateLambdaConfiguration() {\n\n// }\n\nmodule.exports = {\n  createLambda,\n  deleteLambda,\n  updateLambdaCode,\n  createLambdaRole,\n  isExistsAmazon,\n}\n"
  },
  {
    "path": "deployer/google/index.js",
    "content": "const fsp = require('fs').promises\nconst { CloudFunctionsServiceClient } = require('@google-cloud/functions');\nconst fetch = require('node-fetch')\nconst { logdev } = require('../../printers/index')\n\nlet gcOptions\nif (process.env.GC_PROJECT) {\n  gcOptions = {\n    projectId: process.env.GC_PROJECT,\n  }\n}\n// Don't consult hyperform.json yet for Google credentials\n\n// if (process.env.GC_CLIENT_EMAIL && process.env.GC_PRIVATE_KEY && process.env.GC_PROJECT) {\n//   gcOptions = {\n//     credentials: {\n//       client_email: process.env.GC_CLIENT_EMAIL,\n//       private_key: process.env.GC_PRIVATE_KEY,\n//     },\n//     projectId: process.env.GC_PROJECT,\n//   }\n// }\n\nconst client = new CloudFunctionsServiceClient(gcOptions)\n\n/**\n * @description Checks whether a GCF \n * exists in a given project & region\n * @param {{\n * name: string,\n * project:string\n * region: string,\n * }} options \n * @returns {Promise<boolean>}\n */\nasync function isExistsGoogle(options) {\n  const getParams = {\n    name: `projects/${options.project}/locations/${options.region}/functions/${options.name}`,\n  }\n\n  try {\n    const res = await client.getFunction(getParams)\n    if (res.length > 0 && res.filter((el) => el).length > 0) {\n      return true \n    } else {\n      return false\n    }\n  } catch (e) {\n    return false \n  }\n}\n\n/**\n * @description Uploads a given file (usually code .zips) to a temporary \n * Google storage and returns \n * its so-called signed URL.\n * This URL can then be used, for example \n * as sourceUploadUrl for creating and updating Cloud Functions. \n * @param {string} pathToFile \n * @param {{\n *  project: string,\n *  region: string\n * }} options \n * @returns {Promise<string>} The signed upload URL\n * @see https://cloud.google.com/storage/docs/access-control/signed-urls Google Documentation\n */\nasync function uploadGoogle(pathToFile, options) {\n  const generateUploadUrlOptions = {\n    parent: `projects/${options.project}/locations/${options.region}`,\n  }\n\n  const signedUploadUrl = (await client.generateUploadUrl(generateUploadUrlOptions))[0].uploadUrl\n\n  // Upload zip\n  // TODO use createReadStream instead\n  const zipBuf = await fsp.readFile(pathToFile)\n  await fetch(signedUploadUrl, {\n    method: 'PUT',\n    headers: {\n      'content-type': 'application/zip',\n      'x-goog-content-length-range': '0,104857600',\n    },\n    body: zipBuf,\n  })\n  logdev('uploaded zip to google signed url')\n\n  return signedUploadUrl\n}\n\n/**\n * @description Updates an existing GCF \"options.name\" in \"options.project\", \"options.region\" \n * with given uploaded code .zip. \n * And, in theory, arbitrary options too (timeout, availableMemoryMb), \n * but currently not needed but could easily be added.\n * Returns immediately, but Google updates for 1-2 minutes more\n* @param {string} signedUploadUrl Signed upload URL where .zip has been uploaded to already.\n*  Output of \"uploadGoogle\".\n* @param {{\n* name: string,\n* project: string,\n* region: string,\n* runtime: string\n* }} options\n*/\nasync function _updateGoogle(signedUploadUrl, options) {\n  const updateOptions = {\n    function: {\n      name: `projects/${options.project}/locations/${options.region}/functions/${options.name}`,\n      httpsTrigger: {\n        url: `https://${options.region}-${options.project}.cloudfunctions.net/${options.name}`,\n      },\n      runtime: options.runtime,\n      timeout: {\n        seconds: 120,\n      },\n      sourceUploadUrl: signedUploadUrl,\n    },\n    // TODO Empty array to not overwrite 'timeout' or 'runtime'\n    updateMask: null,\n    // this does not work :(\n    // updateMask: {\n    //   paths: ['sourceUploadUrl']\n    // }\n  }\n  const res = await client.updateFunction(updateOptions)\n  logdev(`google: updated function ${options.name}`)\n}\n\n/**\n * @description Creates a new GCF \"options.name\" in \"options.project\", \"options.region\" \n * with given uploaded code .zip and options. \n * Returns immediately, but Google creates for 1-2 minutes more\n * @param {string} signedUploadUrl \n * @param {{\n *  name: string,\n * project: string,\n * region: string,\n * runtime: string,\n * entrypoint?: string,\n * }} options \n */\nasync function _createGoogle(signedUploadUrl, options) {\n  const createOptions = {\n    location: `projects/${options.project}/locations/${options.region}`,\n    function: {\n      name: `projects/${options.project}/locations/${options.region}/functions/${options.name}`,\n      httpsTrigger: {\n        url: `https://${options.region}-${options.project}.cloudfunctions.net/${options.name}`,\n      },\n      entryPoint: options.entrypoint || options.name, \n      runtime: options.runtime,\n      sourceUploadUrl: signedUploadUrl,\n      timeout: {\n        seconds: 60, //\n      }, // those are the defaults anyway\n      availableMemoryMb: 256,\n    },\n  }\n  const res = await client.createFunction(createOptions)\n  // TODO wait for operaton to complete (ie setInterval done && !error, promise resolve then)\n  // TODO in _updateGoogle too\n  logdev(`google: created function ${options.name}`)\n}\n\n/**\n * \n * @param {{\n * name: string,\n * project: string,\n * region: string\n * }} options \n */\nasync function _allowPublicInvokeGoogle(options) {\n  // TODO GetIam and get etag of current role first \n  // And then specify that in setIam, to avoid race conditions\n  // @see \"etag\" on https://cloud.google.com/functions/docs/reference/rest/v1/Policy\n\n  const setIamPolicyOptions = {\n    resource: `projects/${options.project}/locations/${options.region}/functions/${options.name}`,\n    policy: {\n      // @see https://cloud.google.com/functions/docs/reference/rest/v1/Policy#Binding\n      bindings: [\n        {\n          role: 'roles/cloudfunctions.invoker',\n          members: ['allUsers'],\n          version: 3,\n        },\n      ],\n    },\n  }\n\n  logdev('setting IAM policy')\n  const res = await client.setIamPolicy(setIamPolicyOptions)\n}\n\n/**\n * @description If Google Cloud Function \"options.name\" \n * does not exist yet in \"options.project\", \"options.region\", \n * it creates a new GCF with given code (\"pathToZip\") and \"options\". \n * If GCF exists already, it updates its code with \"pathToZip\". \n * If other options are specified, it can update those too (currently only \"runtime\"). \n * Returns IAM-protected URL immediately, but Cloud Function takes another 1-2 minutes to be invokable.\n * @param {string} pathToZip \n * @param {{\n * name: string,\n * project: string,\n * region: string,\n * runtime: string,\n * entrypoint?: string\n * }} options \n * @returns {Promise<string>} The endpoint URL\n * @see https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions#CloudFunction For the underlying Google SDK documentation\n */\nasync function deployGoogle(pathToZip, options) {\n  if (!options.name || !options.project || !options.region || !options.runtime) {\n    throw new Error(`name, project and region and runtime must be defined but are ${options.name}, ${options.project}, ${options.region}, ${options.runtime}`) // HF programmer mistake\n  }\n\n  const existsOptions = {\n    name: options.name,\n    project: options.project,\n    region: options.region, \n  }\n  // Check if GCF exists\n  const exists = await isExistsGoogle(existsOptions)\n\n  logdev(`google isexists ${options.name}: ${exists}`)\n\n  // Either way, upload the .zip\n  // @see https://cloud.google.com/storage/docs/access-control/signed-urls\n  const signedUploadUrl = await uploadGoogle(pathToZip, {\n    project: options.project,\n    region: options.region, \n  })\n\n  // if GCF does not exist yet, create it\n  if (exists !== true) {\n    const createParams = {\n      ...options,\n    }\n    await _createGoogle(signedUploadUrl, createParams)\n  } else {\n    // GCF exists, update code and options (currently none)\n    const updateParams = {\n      name: options.name,\n      project: options.project,\n      region: options.region, \n      runtime: options.runtime, \n    }\n    await _updateGoogle(signedUploadUrl, updateParams)\n  }\n   \n  // Construct endpoint URL (it's deterministic)\n  const endpointUrl = `https://${options.region}-${options.project}.cloudfunctions.net/${options.name}`\n\n  // Note: GCF likely not ready by the time we return its URL here\n  return endpointUrl \n}\n\n/**\n * @description Allows anyone to call function via its HTTP endpoint. \n * Does so by turning IAM checking of Google off. \n * Unlike publishAmazon, publishgoogle it does not return an URL, deployGoogle does that already.\n *  @param {*} name \n  * @param {*} project \n  * @param {*} region \n */\nasync function publishGoogle(name, project, region) {\n  const allowPublicInvokeOptions = {\n    name: name,\n    project: project, \n    region: region, \n  }\n  await _allowPublicInvokeGoogle(allowPublicInvokeOptions)\n}\n\nmodule.exports = {\n  deployGoogle,\n  publishGoogle,\n  _only_for_testing_isExistsGoogle: isExistsGoogle,\n}\n"
  },
  {
    "path": "deployer/google/index.test.js",
    "content": "const GCFREGION = 'us-central1'\nconst GCFPROJECT = 'firstnodefunc'\nconst GCFRUNTIME = 'nodejs12'\n\ndescribe('deployer', () => {\n  describe('google', () => {\n    describe('deployGoogle', () => {\n      // Google does not reliably complete deploy/delete at returning\n      // Therefore you can't really setup it properly \n      // because setup might overlap with the test itself\n\n      test('completes if GCF exists already, and returns an URL', async () => {\n        const { deployGoogle } = require('./index')\n        const { zip } = require('../../zipper/index')\n\n        /// //////////////////////////////////////////////\n        // Setup: create folder with code\n\n        /// //////////////////////////////////////////////\n        // Setup: create code zip\n        \n        const name = 'jest_reserved_deployGoogle_A'\n        const code = `module.exports = { ${name}: () => ({a: 1}) }`\n        const zipPath = await zip({\n          'index.js': code,\n        })\n\n        /// ////////////////////////////////////////////////////\n\n        const options = {\n          name: name,\n          project: GCFPROJECT,\n          region: GCFREGION,\n          runtime: GCFRUNTIME, \n        }\n        \n        let err\n        let res\n        try {\n          res = await deployGoogle(zipPath, options)\n        } catch (e) {\n          console.log(e)\n          err = e\n        }\n\n        // it completed \n        expect(err).not.toBeDefined()\n\n        // it's an URL \n        const tryUrl = () => new URL(res)\n        expect(tryUrl).not.toThrow()\n        // Note: the URL would then take another 1-2 minutes to point to a proper GCF\n      }, 2 * 60 * 1000)\n    })\n\n    describe('isExistsGoogle', () => {\n      test('true on existing function in project, region', async () => {\n        const isExistsGoogle = require('./index')._only_for_testing_isExistsGoogle\n\n        const input = {\n          name: 'endpoint_oho', // TODO make reserved funcs for tests\n          project: GCFPROJECT,\n          region: GCFREGION,\n        }\n\n        let err \n        let res \n        try {\n          res = await isExistsGoogle(input)\n        } catch (e) {\n          err = e\n        }\n\n        expect(err).not.toBeDefined()\n        expect(res).toEqual(true)\n      })\n\n      test('false on non-existing function in project, region', async () => {\n        const isExistsGoogle = require('./index')._only_for_testing_isExistsGoogle\n\n        const input = {\n          name: 'SOME_NONEXISTING_FUNCTION_0987654321', // TODO make reserved funcs for tests\n          project: GCFPROJECT,\n          region: GCFREGION,\n        }\n\n        let err \n        let res \n        try {\n          res = await isExistsGoogle(input)\n        } catch (e) {\n          err = e\n        }\n\n        // should still not throw\n        expect(err).not.toBeDefined()\n        expect(res).toEqual(false)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "discoverer/index.js",
    "content": "const findit = require('findit')\nconst path = require('path')\nconst { EOL } = require('os')\nconst fs = require('fs')\nconst { log } = require('../printers/index')\n\nconst BLACKLIST = [\n  '.git',\n  'node_modules',\n]\n\n/**\n * @description Searches \"absdir\" and its subdirectories for .js files\n * @param {string} absdir An absolute path to a directory\n * @returns {Promise<string[]>} Array of absolute file paths to .js files\n */\n// TODO do not follow symlinks (or do?)\nfunction getJsFilepaths(absdir) {\n  return new Promise((resolve, reject) => {\n    const fnames = []\n    const finder = findit(absdir)\n  \n    finder.on('directory', (_dir, stat, stop) => {\n      const base = path.basename(_dir);\n      if (BLACKLIST.includes(base)) {\n        stop()\n      }\n    });\n  \n    finder.on('file', (file, stat) => {\n      // only return .js files\n      if (/.js$/.test(file) === true) {\n        fnames.push(file)\n      }\n    });\n  \n    finder.on('end', () => {\n      resolve(fnames)\n    })\n  \n    finder.on('error', (err) => {\n      reject(err)\n    })\n  })\n}\n\n/**\n * @description Runs a .js file to get its named export keys \n * @param {string} filepath Path to .js file\n * @returns {string[]} Array of named export keys\n */\nfunction getNamedExportKeys(filepath) {\n  // hides that code is run but actually runs it lol\n  // TODOfind a way to get exports without running code\n  try {\n    const imp = (() => \n      // console.log = () => {}\n      // console.error = () => {}\n      // console.warn = () => {}\n      require(filepath)\n    )()\n    const namedexpkeys = Object.keys(imp)\n    return namedexpkeys\n  } catch (e) {\n    // if js file isn't parseable, top level code throws, etc\n    // ignore it\n    log(`Could not determine named exports of ${filepath}. Try to fix the error and try again: ${EOL} Error: ${e}`)\n    return []\n  }\n}\n\n/**\n * Checks if file contents match a regex\n * @param {*} fpath \n * @param {*} regex \n * @returns {boolean}\n */\nfunction isFileContains(fpath, regex) {\n  const contents = fs.readFileSync(fpath, { encoding: 'utf-8' })\n  const res = regex.test(contents) \n  return res \n}\n\n/**\n * @description Scouts \"dir\" and its subdirectories for .js files named \n * exports that match \"fnregex\"\n * @param {string} dir \n * @param {Regex} fnregex \n * @returns {[ { p: string, exps: string[] } ]} Array of \"p\" (path to js file)\n *  and its named exports that match fnregex (\"exps\")\n */\nasync function getInfos(dir, fnregex) {\n  let jsFilePaths = await getJsFilepaths(dir)\n  // First check - filter out files that don't even contain a string matching fnregex (let alone export it)\n  jsFilePaths = jsFilePaths\n    .filter((p) => isFileContains(p, fnregex))\n  // Second check - determine exports by running file, and keep those that export sth matching fnregex\n  const infos = jsFilePaths\n    .map((p) => ({\n      p: p,\n      exps: getNamedExportKeys(p),\n    }))\n    // skip files that don't have named exports\n    .filter(({ exps }) => exps != null && exps.length > 0)\n    // skip files that don't have named exports that fit fnregex\n    .filter(({ exps }) => exps.some((exp) => fnregex.test(exp) === true))\n    // filter out exports that don't fit fnregex\n    .map((el) => ({ ...el, exps: el.exps.filter((exp) => fnregex.test(exp) === true) }))\n\n  return infos\n}\n\nmodule.exports = {\n  getInfos,\n  getNamedExportKeys,\n}\n"
  },
  {
    "path": "index.js",
    "content": "/* eslint-disable max-len */\n\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst fs = require('fs')\nconst fsp = require('fs').promises\nconst path = require('path')\nconst { EOL } = require('os')\nconst { bundleAmazon } = require('./bundler/amazon/index')\nconst { bundleGoogle } = require('./bundler/google/index')\nconst { getNamedExportKeys } = require('./discoverer/index')\nconst { deployAmazon } = require('./deployer/amazon/index')\nconst { publishAmazon } = require('./publisher/amazon/index')\nconst { spinnies, log, logdev } = require('./printers/index')\nconst { zip } = require('./zipper/index')\nconst { deployGoogle, publishGoogle } = require('./deployer/google/index')\nconst { transpile } = require('./transpiler/index')\nconst packagejson = require('./package.json')\nconst { createCopy } = require('./copier/index')\nconst { zipDir } = require('./zipper/google/index')\nconst { kindle } = require('./kindler/index')\nconst { amazonSchema, googleSchema } = require('./schemas/index')\n/**\n * \n * @param {string} fpath Path to .js file\n */\nasync function bundleTranspileZipAmazon(fpath) {\n  // Bundle \n  let amazonBundledCode\n  try {\n    amazonBundledCode = await bundleAmazon(fpath)\n  } catch (e) {\n    log(`Errored bundling ${fpath} for Amazon: ${e}`)\n    return // just skip that file \n  }\n\n  // Transpile \n  const amazonTranspiledCode = transpile(amazonBundledCode)\n\n  // Zip\n  try {\n    const amazonZipPath = await zip({\n      'index.js': amazonTranspiledCode,\n    })\n    return amazonZipPath\n  } catch (e) {\n    // probably underlying issue with the zipping library or OS\n    // skip that file \n    log(`Errored zipping ${fpath} for Amazon: ${e}`)\n  }\n}\n\n// TODO those are basically the same now\n// but for later it may be good to have them separate\n// in case they start to diverge\n\n// /**\n//  * \n//  * @param {string} fpath Path to .js file\n//  * @param {string} dir\n//  */\n// async function bundleTranspileZipGoogle(fpath, dir) {\n//   // Bundle (omits any npm packages) \n//   let googleBundledCode\n//   try {\n//     googleBundledCode = await bundleGoogle(fpath)\n//   } catch (e) {\n//     log(`Errored bundling ${fpath} for Google: ${e}`)\n//     return // just skip that file \n//   }\n\n//   // Transpile \n//   const googleTranspiledCode = transpile(googleBundledCode)\n\n//   // Try to locate a package.json\n//   // Needed so google installs the npm packages\n//   const packageJsonPath = path.join(dir, 'package.json')\n//   let packageJsonContent\n//   if (fs.existsSync(packageJsonPath)) {\n//     packageJsonContent = fs.readFileSync(packageJsonPath, { encoding: 'utf-8' })\n//   } else {\n//     // warn\n//     log(`No package.json found in this directory. \n//       On Google, therefore no dependencies will be included`)\n//   }\n\n//   // Zip code and package.json\n//   try {\n//     const googleZipPath = await zip({\n//       'index.js': googleTranspiledCode,\n//       'package.json': packageJsonContent || undefined,\n//     })\n//     return googleZipPath\n//   } catch (e) {\n//     // probably underlying issue with the zipping library or OS\n//     throw new Error(`Errored zipping ${fpath} for Google: ${e}`)\n//   }\n// }\n\nasync function bundleTranspileZipGoogle(fpath, dir, exps) {\n  // warn if package.json does not exist\n  // (Google won't install npm dependencies then)\n  if (fs.existsSync(path.join(dir, 'package.json')) === false) {\n    log(`No package.json found in this directory. \n      On Google, therefore no dependencies will be included`)\n  }\n\n  // copy whole dir to /tmp so we can tinker with it\n  const googlecopyDir = await createCopy(\n    dir,\n    ['node_modules', '.git', '.github', 'hyperform.json'],\n  )\n\n  const indexJsPath = path.join(googlecopyDir, 'index.js')\n\n  let indexJsAppendix = '' \n  // add import-export appendix\n  indexJsAppendix = kindle(indexJsAppendix, dir, [\n    {\n      p: fpath,\n      exps: exps,\n    },\n  ])\n  // add platform appendix\n  indexJsAppendix = transpile(indexJsAppendix)\n\n  // write or append to index.js in our tinker folder\n  if (fs.existsSync(indexJsPath) === false) {\n    await fsp.writeFile(indexJsPath, indexJsAppendix, { encoding: 'utf-8' })\n  } else {\n    await fsp.appendFile(indexJsPath, indexJsAppendix, { encoding: 'utf-8' })\n  }\n\n  // zip tinker folder\n  const googleZipPath = await zipDir(\n    googlecopyDir, \n    ['node_modules', '.git', '.github', 'hyperform.json'], // superfluous we didnt copy them in the first place\n  )\n\n  return googleZipPath\n}\n\n/**\n *  @description Deploys a given code .zip to AWS Lambda, and gives it a HTTP endpoint via API Gateway\n * @param {string} name \n * @param {string} region \n * @param {string} zipPath \n * @param {boolean} isPublic whether to publish \n * @returns {string?} If isPublic was true, URL of the endpoint of the Lambda\n */\nasync function deployPublishAmazon(name, region, zipPath, isPublic) {\n  const amazonSpinnieName = `amazon-main-${name}`\n  try {\n    spinnies.add(amazonSpinnieName, { text: `Deploying ${name} to AWS Lambda` })\n\n    // Deploy it\n    const amazonDeployOptions = {\n      name: name,\n      region: region,\n    }\n    const amazonArn = await deployAmazon(zipPath, amazonDeployOptions)\n    let amazonUrl\n    // Publish it if isPpublic\n    if (isPublic === true) {\n      amazonUrl = await publishAmazon(amazonArn, region)\n    }\n    spinnies.succ(amazonSpinnieName, { text: `🟢 Deployed ${name} to AWS Lambda ${amazonUrl || ''}` })\n\n    // (return url)\n    return amazonUrl\n  } catch (e) {\n    spinnies.f(amazonSpinnieName, {\n      text: `Error deploying ${name} to AWS Lambda: ${e.stack}`,\n    })\n    logdev(e, e.stack)\n    return null\n  }\n}\n\n// TODO probieren\n// TODO tests anpassen\n// TODO testen\n// TODO tests schreiben, refactoren\n\n/**\n * @description Deploys and publishes a give code .zip to Google Cloud Functions\n * @param {string} name \n * @param {string} region \n * @param {string} project \n * @param {string} zipPath \n * @param {boolean} isPublic whether to publish\n * @returns {string?} If isPublic was true, URL of the Google Cloud Function \n */\nasync function deployPublishGoogle(name, region, project, zipPath, isPublic) {\n  const googleSpinnieName = `google-main-${name}`\n  try {\n    spinnies.add(googleSpinnieName, { text: `Deploying ${name} to Google Cloud Functions` })\n    const googleOptions = {\n      name: name,\n      project: project, // process.env.GC_PROJECT,\n      region: region, // TODO get from parsedhyperfromjson\n      runtime: 'nodejs12',\n    }\n    const googleUrl = await deployGoogle(zipPath, googleOptions)\n\n    if (isPublic === true) {\n      // enables anyone with the URL to call the function\n      await publishGoogle(name, project, region)\n    }\n    spinnies.succ(googleSpinnieName, { text: `🟢 Deployed ${name} to Google Cloud Functions ${googleUrl || ''}` })\n    console.log('Google takes another 1 - 2m for changes to take effect')\n\n    // return url\n    return googleUrl\n  } catch (e) {\n    spinnies.f(googleSpinnieName, {\n      text: `${chalk.rgb(255, 255, 255).bgWhite(' Google ')} ${name}: ${e.stack}`,\n    })\n    logdev(e, e.stack)\n    return null\n  }\n}\n/**\n * @param {string} dir \n * @param {Regex} fpath the path to the .js file whose exports should be deployed \n * @param {amazon|google} platform\n * @param {boolean?} _isPublic\n * @param {{amazon: {aws_access_key_id:string, aws_secret_access_key: string, aws_region: string}}} parsedHyperformJson \n */\nasync function main(dir, fpath, platform, parsedHyperformJson, _isPublic) {\n  // Check node version (again)\n  const version = packagejson.engines.node \n  if (semver.satisfies(process.version, version) !== true) {\n    console.log(`Hyperform needs node ${version} or newer, but version is ${process.version}.`);\n    process.exit(1);\n  }\n\n  // verify parsedHyperformJson (again)\n  let schema \n  if (platform === 'amazon') schema = amazonSchema\n  if (platform === 'google') schema = googleSchema\n  const { error, value } = schema.validate(parsedHyperformJson)\n  if (error) {\n    throw new Error(`${error} ${value}`)\n  }\n\n  const absfpath = path.resolve(dir, fpath)\n\n  // determine named exports\n  const exps = getNamedExportKeys(absfpath)\n\n  if (exps.length === 0) {\n    log(`No named CommonJS exports found in ${absfpath}. ${EOL}Named exports have the form 'module.exports = { ... }' or 'exports.... = ...' `)\n    return [] // no endpoint URLs created\n  }\n\n  const isToAmazon = platform === 'amazon'\n  const isToGoogle = platform === 'google'\n\n  let amazonZipPath\n  let googleZipPath\n\n  if (isToAmazon === true) {\n    amazonZipPath = await bundleTranspileZipAmazon(absfpath)\n  }\n\n  if (isToGoogle === true) {\n    googleZipPath = await bundleTranspileZipGoogle(absfpath, dir, exps)\n  }\n\n  /// ///////////////////////////////////////////////////\n  /// Each  export, deploy as function & publish. Obtain URL.\n  /// ///////////////////////////////////////////////////\n  const isPublic = _isPublic || false\n\n  let endpoints = await Promise.all(\n    // For each export\n    exps.map(async (exp) => {\n      /// //////////////////////////////////////////////////////////\n      /// Deploy to Amazon\n      /// //////////////////////////////////////////////////////////\n      let amazonUrl\n      if (isToAmazon === true) {\n        amazonUrl = await deployPublishAmazon(\n          exp,\n          parsedHyperformJson.amazon.aws_region,\n          amazonZipPath,\n          isPublic,\n        )\n      }\n\n      /// //////////////////////////////////////////////////////////\n      /// Deploy to Google\n      /// //////////////////////////////////////////////////////////\n      let googleUrl\n      if (isToGoogle === true) {\n        googleUrl = await deployPublishGoogle(\n          exp,\n          // TODO lol\n          parsedHyperformJson.google.gc_region,\n          parsedHyperformJson.google.gc_project, // TODO\n          googleZipPath,\n          isPublic,\n        )\n      }\n\n      return amazonUrl || googleUrl // for tests etc\n    }),\n  )\n\n  endpoints = endpoints.filter((el) => el)\n\n  return { urls: endpoints }\n\n  /// //////////////////////////////////////////////////////////\n  // Bundle and zip for Google  (once) //\n  /// //////////////////////////////////////////////////////////\n\n  // TODO \n\n  // NOTE that google and amazon now work fundamentally different\n  // Google - 1 deployment package\n\n  // For each file \n  //   bundle\n  //   transpile \n  // //   Amazon\n  // //     zip\n  // //     deployAmazon \n  // //     publishAmazon  \n\n  // // Later instead of N times, just create 1 deployment package for all functions\n\n  // const endpoints = await Promise.all(\n  //   // For each file\n  //   infos.map(async (info) => {\n  //     const toAmazon = parsedHyperformJson.amazon != null\n  //     const toGoogle = parsedHyperformJson.google != null\n  //     /// //////////////////////////////////////////////////////////\n  //     // Bundle and zip for Amazon //\n  //     /// //////////////////////////////////////////////////////////\n  //     let amazonZipPath\n  //     if (toAmazon === true) {\n  //       amazonZipPath = await bundleTranspileZipAmazon(info.p)\n  //     }\n\n  //     /// //////////////////////////////////////////////////////////\n  //     // Bundle and zip for Google //\n  //     // NOW DONE ABOVE\n  //     /// //////////////////////////////////////////////////////////\n  //     // let googleZipPath \n  //     // if (toGoogle === true) {\n  //     //   googleZipPath = await bundleTranspileZipGoogle(info.p)\n  //     // }\n\n  //     // For each matching export\n  //     const endpts = await Promise.all(\n  //       info.exps.map(async (exp, idx) => {\n  //         /// //////////////////////////////////////////////////////////\n  //         /// Deploy to Amazon\n  //         /// //////////////////////////////////////////////////////////\n  //         let amazonUrl\n  //         if (toAmazon === true) {\n  //           amazonUrl = await deployPublishAmazon(\n  //             exp,\n  //             parsedHyperformJson.amazon.aws_region,\n  //             amazonZipPath,\n  //             isPublic,\n  //           )\n  //         }\n\n  //         /// //////////////////////////////////////////////////////////\n  //         /// Deploy to Google\n  //         /// //////////////////////////////////////////////////////////\n  //         let googleUrl\n  //         if (toGoogle === true) {\n  //           googleUrl = await deployPublishGoogle(\n  //             exp,\n  //             'us-central1',\n  //             'hyperform-7fd42', // TODO\n  //             googleZipPath,\n  //             isPublic,\n  //           )\n  //         }\n\n  //         return [amazonUrl, googleUrl].filter((el) => el) // for tests etc\n  //       }),\n  //     )\n\n  //     return [].concat(...endpts)\n  //   }),\n  // )\n  // return { urls: endpoints }\n}\n\nmodule.exports = {\n  main,\n}\n"
  },
  {
    "path": "index.test.js",
    "content": "/* eslint-disable no-await-in-loop, global-require */\n\n// One test to rule them all\n\nconst os = require('os')\nconst path = require('path')\nconst fsp = require('fs').promises\n\nconst TIMEOUT = 1 * 60 * 1000\n\ndescribe('System tests (takes 1-2 minutes)', () => {\n  describe('main', () => {\n    describe('amazon', () => {\n      test('completes', async () => {\n        const { main } = require('./index')\n        /// ////////////////////////////////////////////\n        // Set up\n\n        const tmpdir = path.join(\n          os.tmpdir(),\n          `${Math.ceil(Math.random() * 100000000000)}`,\n        )\n\n        // // What we will pass to the functions\n\n        // const random_string = uuidv4()\n        // const event_body = {\n        //   random_string,\n        // }\n        // const event_querystring = `?random_string=${random_string}`\n      \n        // Create javascript files\n\n        await fsp.mkdir(tmpdir)\n        const code = `\n        function irrelevant() {\n          return 100\n        }\n        \n        function jest_systemtest_amazon(event, context, callback) {\n          context.succeed({})\n        }\n        \n        module.exports = {\n          jest_systemtest_amazon\n        }\n        `\n\n        const tmpcodepath = path.join(tmpdir, 'index.js')\n        await fsp.writeFile(tmpcodepath, code, { encoding: 'utf-8' })\n    \n        const arg1 = tmpdir\n        const arg2 = tmpcodepath\n        /// ////////////////////////////////////////////\n        // Run main for Amazon\n        /// ////////////////////////////////////////////\n        let amazonMainRes\n        \n        const amazonarg3 = 'amazon'\n        const amazonarg4 = {\n          amazon: {\n            aws_access_key_id: process.env.AWS_ACCESS_KEY_ID,\n            aws_secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,\n            aws_region: process.env.AWS_REGION,\n          },\n          \n        }\n        \n        let err\n        try {\n          amazonMainRes = await main(arg1, arg2, amazonarg3, amazonarg4)\n        } catch (e) {\n          console.log(e)\n          err = e\n        }\n\n        // Expect main did not throw\n        expect(err).not.toBeDefined()\n        // // Expect main returned sensible data\n        // expect(amazonMainRes).toBeDefined()\n        // expect(amazonMainRes.urls).toBeDefined()\n      }, TIMEOUT)\n    })\n\n    describe('google', () => {\n      test('completes', async () => {\n        const { main } = require('./index')\n        /// ////////////////////////////////////////////\n        // Set up\n\n        const tmpdir = path.join(\n          os.tmpdir(),\n          `${Math.ceil(Math.random() * 100000000000)}`,\n        )\n\n        // // What we will pass to the functions\n\n        // const random_string = uuidv4()\n        // const event_body = {\n        //   random_string,\n        // }\n        // const event_querystring = `?random_string=${random_string}`\n      \n        // Create javascript files\n\n        await fsp.mkdir(tmpdir)\n        const code = `\n        function irrelevant() {\n          return 100\n        }\n        \n        function jest_systemtest_google(req, resp) {\n          resp.json({})\n        }\n        \n        module.exports = {\n          jest_systemtest_google\n        }\n        `\n\n        const tmpcodepath = path.join(tmpdir, 'index.js')\n        await fsp.writeFile(tmpcodepath, code, { encoding: 'utf-8' })\n    \n        const arg1 = tmpdir\n        const arg2 = tmpcodepath\n        /// ////////////////////////////////////////////\n        // Run main for Google\n        /// ////////////////////////////////////////////  \n        let googleMainRes\n        \n        const googlearg3 = 'google'\n        const googlearg4 = {\n          google: {\n            gc_project: process.env.GC_PROJECT,\n            gc_region: process.env.GC_REGION,\n          },\n        }\n\n        let err\n        try {\n          googleMainRes = await main(arg1, arg2, googlearg3, googlearg4)\n        } catch (e) {\n          console.log(e)\n          err = e\n        }\n\n        // Expect main did not throw\n        expect(err).not.toBeDefined()\n        // // Expect main returned sensible data\n        // expect(googleMainRes).toBeDefined()\n        // expect(googleMainRes.urls).toBeDefined()\n      }, TIMEOUT)\n    })\n    // test('completes, and echo endpoints return first arg (event) and second arg (http) on GET and POST', async () => {\n    //   const { main } = require('./index')\n    //   /// ////////////////////////////////////////////\n    //   // Set up\n\n    //   const tmpdir = path.join(\n    //     os.tmpdir(),\n    //     `${Math.ceil(Math.random() * 100000000000)}`,\n    //   )\n\n    //   // // What we will pass to the functions\n\n    //   // const random_string = uuidv4()\n    //   // const event_body = {\n    //   //   random_string,\n    //   // }\n    //   // const event_querystring = `?random_string=${random_string}`\n      \n    //   // Create javascript files\n\n    //   await fsp.mkdir(tmpdir)\n    //   const code = `\n    //     function irrelevant() {\n    //       return 100\n    //     }\n        \n    //     function jest_systemtest_echo(event, http) {\n    //       return { event: event, http: http }\n    //     }\n        \n    //     module.exports = {\n    //       jest_systemtest_echo\n    //     }\n    //     `\n\n    //   const tmpcodepath = path.join(tmpdir, 'index.js')\n    //   await fsp.writeFile(tmpcodepath, code, { encoding: 'utf-8' })\n    \n    //   const arg1 = tmpdir\n    //   const arg2 = tmpcodepath\n    //   /// ////////////////////////////////////////////\n    //   // Run main for Amazon\n    //   /// ////////////////////////////////////////////\n    //   let amazonMainRes\n\n    //   {\n    //     const amazonarg3 = {\n    //       amazon: {\n    //         aws_access_key_id: process.env.AWS_ACCESS_KEY_ID,\n    //         aws_secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,\n    //         aws_region: process.env.AWS_REGION,\n    //       },\n          \n    //     }\n        \n    //     // isPublic\n    //     const amazonarg4 = true\n        \n    //     let err\n    //     try {\n    //       amazonMainRes = await main(arg1, arg2, amazonarg3, amazonarg4)\n    //     } catch (e) {\n    //       console.log(e)\n    //       err = e\n    //     }\n\n    //     // Expect main did not throw\n    //     expect(err).not.toBeDefined()\n    //     // Expect main returned sensible data\n    //     expect(amazonMainRes).toBeDefined()\n    //     expect(amazonMainRes.urls).toBeDefined()\n    //   }\n        \n    //   /// ////////////////////////////////////////////\n    //   // Run main for Google\n    //   /// ////////////////////////////////////////////  \n    //   let googleMainRes\n    //   {\n    //     const googlearg3 = {\n    //       google: {\n    //         gc_client_email: '',\n    //         gc_private_key: '',\n    //         gc_project: '',\n    //       },\n\n    //     }\n\n    //     // to test publishing too\n    //     const googlearg4 = true\n     \n    //     let err\n    //     try {\n    //       googleMainRes = await main(arg1, arg2, googlearg3, googlearg4)\n    //     } catch (e) {\n    //       console.log(e)\n    //       err = e\n    //     }\n\n    //     // Expect main did not throw\n    //     expect(err).not.toBeDefined()\n    //     // Expect main returned sensible data\n    //     expect(googleMainRes).toBeDefined()\n    //     expect(googleMainRes.urls).toBeDefined()\n    //   }\n\n    //   /// ///////////////////////////////////////////\n    //   // Ping each Amazon URL\n    //   // Expect correct result\n    //   /// ///////////////////////////////////////////\n    //   // Don't test Google ones, they take another 1-2min to be ready\n    //   const urls = [].concat(...amazonMainRes.urls)\n    //   // TODO ensure in deployGoogle we return only on truly completed \n    //   // TODO then, we can start testing them here again\n        \n    //   for (let i = 0; i < urls.length; i += 1) {\n    //     const url = urls[i]\n    //     /// /////////////////\n    //     // POST /////////////\n    //     /// /////////////////\n\n    //     {\n    //       const postres = await fetch(url, {\n    //         method: 'POST',\n    //         body: JSON.stringify(event_body),\n    //       })\n    //       const statusCode = postres.status\n    //       const actualResult = await postres.json()\n    //       // HTTP Code 2XX\n    //       expect(/^2/.test(statusCode)).toBe(true)\n    //       // Echoed event\n    //       expect(actualResult.event).toEqual(event_body)\n    //       // Returned second argument; check if the wrapper formed it properly\n    //       expect(actualResult.http).toBeDefined()\n    //       expect(actualResult.http.headers).toBeDefined()\n    //       expect(actualResult.http.method).toBe('POST')\n    //     }\n\n    //     /// /////////////////\n    //     // GET /////////////\n    //     /// /////////////////\n\n    //     {\n    //       const getres = await fetch(`${url}${event_querystring}`, {\n    //         method: 'GET',\n    //       })\n    //       const statusCode = getres.status\n    //       const actualResult = await getres.json()\n    //       // HTTP Code 2XX\n    //       expect(/^2/.test(statusCode)).toBe(true)\n    //       // Echoed event\n    //       expect(actualResult.event).toEqual(event_body)\n    //       // Returned second argument; check if the wrapper formed it properly\n    //       expect(actualResult.http).toBeDefined()\n    //       expect(actualResult.http.headers).toBeDefined()\n    //       expect(actualResult.http.method).toBe('GET')\n    //     }\n    //   }\n    // }, TIMEOUT)\n  })\n\n  // describe('cli', () => {\n  //   // TODO\n  // })\n})\n"
  },
  {
    "path": "initer/index.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst os = require('os')\nconst { EOL } = require('os')\nconst { log, logdev } = require('../printers/index')\n/**\n * @description Extracts the [default] section of an AWS .aws/config or .aws/credentials file\n * @param {string} filecontents File contents of an .aws/credentials or .aws/config file\n * @returns {string} The string between [default] and the next [...] header, if exists.\n *  Otherwise returns empty string\n */\nfunction getDefaultSectionString(filecontents) {\n  // Collect all lines below the [default] header ...\n  let defaultSection = filecontents.split(/\\[default\\]/)[1]\n\n  if (typeof defaultSection !== 'string' || !defaultSection.trim()) {\n    // default section is non-existent\n    return ''\n  }\n  // ... but above the next [...] header (if any)\n  defaultSection = defaultSection.split(/\\[[^\\]]*\\]/)[0]\n  return defaultSection\n}\n\n// TODO refactor\n// TODO split up into better functions, for amazon, google inferrer\n// TODO error handling & meaningful stdout\n// TODO tests\n/**\n * @description Extracts aws_access_key_id and aws_secret_access_key fields from a given .aws/credentials file\n * @param {string} filecontents File contents of .aws/credentials\n * @returns {\n * default: { \n  * aws_access_key_id?: string, \n  * aws_secret_access_key?: string, \n  * aws_region?: string \n * }}\n */\nfunction parseAwsCredentialsOrConfigFile(filecontents) {\n  /* filecontents looks something like this:\n\n    [default]\n    region=us-west-2\n    output=json\n\n    [profile user1]\n    region=us-east-1\n    output=text\n\n    See: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html\n\n    */\n    \n  const fields = {\n    default: {},\n  }\n  \n  try {\n    const defaultSectionString = getDefaultSectionString(filecontents)\n    if (!defaultSectionString || !defaultSectionString.trim()) {\n      return fields\n    }\n    \n    // we found something\n    const defaultSectionLines = defaultSectionString.split('\\n') // TODO split by os-specific newline\n  \n    // Try to extract aws_access_key_id\n    if (defaultSectionLines.some((l) => /aws_access_key_id/.test(l))) {\n      const awsAccessKeyIdLine = defaultSectionLines.filter((l) => /aws_access_key_id/.test(l))[0]\n      let awsAccessKeyId = awsAccessKeyIdLine.split('=')[1]\n      if (typeof awsAccessKeyId === 'string') { // don't crash on weird invalid lines such 'aws_access_key_id=' or 'aws_access_key_id'\n        awsAccessKeyId = awsAccessKeyId.trim()\n        fields.default.aws_access_key_id = awsAccessKeyId\n      }\n    }\n  \n    // Try to extract aws_secret_access_key\n    if (defaultSectionLines.some((l) => /aws_secret_access_key/.test(l))) {\n      const awsSecretAccessKeyLine = defaultSectionLines.filter((l) => /aws_secret_access_key/.test(l))[0]\n      let awsSecretAccessKey = awsSecretAccessKeyLine.split('=')[1]\n      if (typeof awsSecretAccessKey === 'string') {\n        awsSecretAccessKey = awsSecretAccessKey.trim() \n        fields.default.aws_secret_access_key = awsSecretAccessKey\n      }\n    }\n\n    // Try to extract region\n    if (defaultSectionLines.some((l) => /region/.test(l))) {\n      const regionLine = defaultSectionLines.filter((l) => /region/.test(l))[0]\n      let region = regionLine.split('=')[1]\n      if (typeof region === 'string') {\n        region = region.trim() \n        fields.default.region = region\n      }\n    }\n\n    return fields\n    // \n  } catch (e) {\n    // console.log(e)\n    // non-critical, just return what we have so far\n    return fields \n  }\n}\n\n/**\n * Just creates an empty hyperform.json\n * @param {string} absdir \n */\nfunction initDumb(absdir, platform) {\n  let json \n  if (platform === 'amazon') {\n    json = {\n      amazon: {\n        aws_access_key_id: '',\n        aws_secret_access_key: '',\n        aws_region: '', \n      },\n    }\n  } else if (platform === 'google') {\n    json = {\n      google: {\n        gc_project: '',\n        gc_region: '',\n      },\n    }\n  } else {\n    throw new Error(`platform must be google or amazon but is ${platform}`)\n  }\n\n  // append 'hyperform.json' to .gitignore \n  // (or create .gitignore if it does not exist yet)\n  fs.appendFileSync(\n    path.join(absdir, '.gitignore'),\n    `${EOL}hyperform.json`,\n  )\n\n  // write results to hyperform.json\n  fs.writeFileSync(\n    path.join(absdir, 'hyperform.json'),\n    JSON.stringify(json, null, 2),\n  )\n  log('✓ Created `hyperform.json` ') //\n  log('✓ Added `hyperform.json` to `.gitignore`') //\n}\n\n// TODO shorten\n/**\n * @description Tries to infer AWS credentials and config, and creates a hyperform.json in \"absdir\" with what it could infer. If hyperform.json already exists in \"absdir\" it just prints a message.\n * @param {string} absdir The directory where 'hyperform.json' should be created\n * @returns {{ \n * amazon: { \n *  aws_access_key_id: string?,\n *  aws_secret_access_key: string?, \n *  aws_region: string? \n * }\n * }}\n */\nfunction init(absdir) {\n  const hyperformJsonContents = {\n    amazon: {\n      aws_access_key_id: '',\n      aws_secret_access_key: '',\n      aws_region: '', \n    },\n    google: {\n      gc_project: '',\n      gc_region: '',\n    },\n  }\n\n  const filedest = path.join(absdir, 'hyperform.json')\n  if (fs.existsSync(filedest)) {\n    log('hyperform.json exists already.')\n    return\n  }\n  \n  // try to infer AWS credentials\n\n  // AWS CLI uses this precedence:\n  // (1 - highest precedence) Environment variables AWS_ACCESS_KEY_ID, ...  \n  // (2) .aws/credentials and .aws/config\n\n  // Hence, do the same here\n\n  // First, start with (2)\n\n  // Check ~/.aws/credentials and ~/.aws/config\n  \n  const possibleCredentialsPath = path.join(os.homedir(), '.aws', 'credentials')\n  \n  if (fs.existsSync(possibleCredentialsPath) === true) {\n    const credentialsFileContents = fs.readFileSync(possibleCredentialsPath, { encoding: 'utf-8' })\n        \n    // TODO offer selection to user when there are multiple profiles\n    const parsedCredentials = parseAwsCredentialsOrConfigFile(credentialsFileContents)\n    hyperformJsonContents.amazon.aws_access_key_id = parsedCredentials.default.aws_access_key_id\n    hyperformJsonContents.amazon.aws_secret_access_key = parsedCredentials.default.aws_secret_access_key\n    logdev(`Inferred AWS credentials from ${possibleCredentialsPath}`)\n  } else {\n    logdev(`Could not guess AWS credentials. No AWS credentials file found in ${possibleCredentialsPath}`)\n  }\n\n  /// /////////////////\n  /// /////////////////\n\n  // try to infer AWS region\n  const possibleConfigPath = path.join(os.homedir(), '.aws', 'config')\n\n  if (fs.existsSync(possibleConfigPath) === true) {\n    const configFileContents = fs.readFileSync(possibleConfigPath, { encoding: 'utf-8' })\n\n    const parsedConfig = parseAwsCredentialsOrConfigFile(configFileContents)\n    hyperformJsonContents.amazon.aws_region = parsedConfig.default.region\n    logdev(`Inferred AWS region from ${possibleConfigPath}`)\n  } else {\n    logdev(`Could not guess AWS region. No AWS config file found in ${possibleConfigPath}`) // TODO region will not be a single region, but smartly multiple ones (or?)\n  }\n\n  // Then, do (1), possibly overriding values\n  // Check environment variables\n\n  if (typeof process.env.AWS_ACCESS_KEY_ID === 'string' && process.env.AWS_ACCESS_KEY_ID.trim().length > 0) {\n    hyperformJsonContents.amazon.aws_access_key_id = process.env.AWS_ACCESS_KEY_ID.trim()\n    logdev('Environment variable AWS_ACCESS_KEY_ID set, overriding value from credentials file')\n  }\n\n  if (typeof process.env.AWS_SECRET_ACCESS_KEY === 'string' && process.env.AWS_SECRET_ACCESS_KEY.trim().length > 0) {\n    hyperformJsonContents.amazon.aws_secret_access_key = process.env.AWS_SECRET_ACCESS_KEY.trim()\n    logdev('Environment variable AWS_SECRET_ACCESS_KEY set, overriding value from credentials file')\n  }\n\n  if (typeof process.env.AWS_REGION === 'string' && process.env.AWS_REGION.trim().length > 0) {\n    hyperformJsonContents.amazon.aws_region = process.env.AWS_REGION.trim()\n    logdev('Environment variable AWS_REGION set, overriding value from config file')\n  }\n\n  // append 'hyperform.json' to .gitignore \n  // (or create .gitignore if it does not exist yet)\n  fs.appendFileSync(\n    path.join(absdir, '.gitignore'),\n    `${EOL}hyperform.json`,\n  )\n\n  // write results to hyperform.json\n  fs.writeFileSync(\n    path.join(absdir, 'hyperform.json'),\n    JSON.stringify(hyperformJsonContents, null, 2),\n  )\n  log('✓ Inferred AWS credentials (\\'default\\' Profile)') // TODO ask for defaults guide through in init\n\n  log('✓ Created hyperform.json') // TODO ask for defaults guide through in init\n}\n\nmodule.exports = {\n  init,\n  initDumb,\n  _only_for_testing_getDefaultSectionString: getDefaultSectionString,\n  _only_for_testing_parseAwsCredentialsOrConfigFile: parseAwsCredentialsOrConfigFile,\n}\n"
  },
  {
    "path": "initer/index.test.js",
    "content": "describe('initer', () => {\n  describe('getDefaultSectionString', () => {\n    test('returns string on input: empty string', () => {\n      const getDefaultSectionString = require('./index')._only_for_testing_getDefaultSectionString\n\n      const filecontents = ' '\n    \n      const res = getDefaultSectionString(filecontents)\n    \n      expect(typeof res).toBe('string')\n      expect(res.trim()).toBe('')\n    })\n\n    test('returns empty string on input: [default] header', () => {\n      const getDefaultSectionString = require('./index')._only_for_testing_getDefaultSectionString\n\n      const filecontents = `\n      [defaut] \n      \n      `\n  \n      const res = getDefaultSectionString(filecontents)\n    \n      expect(typeof res).toBe('string')\n      expect(res.trim()).toBe('')\n    })\n\n    test('returns empty string on input: other header, other section', () => {\n      const getDefaultSectionString = require('./index')._only_for_testing_getDefaultSectionString\n\n      const filecontents = `\n[some-other-header]\nfirst\nsecond \n\n      `\n  \n      const res = getDefaultSectionString(filecontents)\n    \n      expect(typeof res).toBe('string')\n      expect(res.trim()).toBe('')\n    })\n\n    test('returns section on input: [default] header and section', () => {\n      const getDefaultSectionString = require('./index')._only_for_testing_getDefaultSectionString\n\n      const filecontents = `\n[default] \nfirst\nsecond\n      `\n  \n      const res = getDefaultSectionString(filecontents)\n    \n      expect(typeof res).toBe('string')\n      const text = res.trim() // 'first\\nsecond'\n      expect(/first/.test(text)).toBe(true)\n      expect(/second/.test(text)).toBe(true)\n    })\n\n    test('returns section on input: other section 1, [default] header, section, other section 2', () => {\n      const getDefaultSectionString = require('./index')._only_for_testing_getDefaultSectionString\n\n      const filecontents = `\n[some-other-profile-a]\nsky\nair\n[default] \nfirst\nsecond\n[some-other-profile-b]\nclouds\n      `\n  \n      const res = getDefaultSectionString(filecontents)\n    \n      expect(typeof res).toBe('string')\n      const text = res.trim()\n      // We want all between [default] and next header, but nothing else\n      expect(text).toBe('first\\nsecond')\n    })\n  })\n\n  describe('parseAwsCredentialsOrConfigFile', () => {\n    test('returns default credentials on just [default] section present', () => {\n      const parseAwsCredentialsOrConfigFile = require('./index')._only_for_testing_parseAwsCredentialsOrConfigFile\n\n      const filecontents = `\n[default]\naws_access_key_id = AKIA2WOM6JAHXXXXXXXX\naws_secret_access_key = XXXXXXXXXX+tgppEZPzdN/XXXXlXXXXX/XXXXXXX\n\n` \n      const res = parseAwsCredentialsOrConfigFile(filecontents)\n\n      expect(res).toBeDefined()\n      expect(res.default).toBeDefined()\n      expect(res.default.aws_access_key_id).toBe('AKIA2WOM6JAHXXXXXXXX')\n      expect(res.default.aws_secret_access_key).toBe('XXXXXXXXXX+tgppEZPzdN/XXXXlXXXXX/XXXXXXX')\n    })\n\n    test('returns default credentials on multiple sections present', () => {\n      const parseAwsCredentialsOrConfigFile = require('./index')._only_for_testing_parseAwsCredentialsOrConfigFile\n\n      // the more weirdly formed\n      const filecontents = `\n      [some-other-section-a]\n  some-other-section-field-b=1234567890\n[default]\naws_access_key_id= AKIA2WOM6JAHXXXXXXXX\naws_secret_access_key =XXXXXXXXXX+tgppEZPzdN/XXXXlXXXXX/XXXXXXX\n[some-other-section-b]\n  some-other-section-field-b=098765434567898765\n` \n      const res = parseAwsCredentialsOrConfigFile(filecontents)\n\n      expect(res).toBeDefined()\n      expect(res.default).toBeDefined()\n      expect(res.default.aws_access_key_id).toBe('AKIA2WOM6JAHXXXXXXXX')\n      expect(res.default.aws_secret_access_key).toBe('XXXXXXXXXX+tgppEZPzdN/XXXXlXXXXX/XXXXXXX')\n    })\n  })\n\n  describe('init', () => {\n    // TODO create mock .aws and see if fields are extracted correctly\n    test('runs, and output has expected structure', async () => {\n      const os = require('os')\n      const uuidv4 = require('uuid').v4 \n      const fs = require('fs')\n      const path = require('path')\n      const { init } = require('./index')\n      // init will write hyperform.json here\n      const absdir = path.join(os.tmpdir(), uuidv4())\n      fs.mkdirSync(absdir)\n\n      let err \n      try {\n        init(absdir)\n      } catch (e) {\n        console.log(e)\n        err = e\n      }\n\n      // it didn't throw\n      expect(err).not.toBeDefined()\n\n      // it wrote hyperform.json\n      const hyperformJsonPath = path.join(absdir, 'hyperform.json')\n      expect(fs.existsSync(hyperformJsonPath)).toBe(true)\n\n      // hyperform.json has the expected structure\n      let hyperformJson = fs.readFileSync(hyperformJsonPath)\n      hyperformJson = JSON.parse(hyperformJson)\n\n      expect(hyperformJson.amazon).toBeDefined()\n      expect(hyperformJson.amazon.aws_access_key_id).toBeDefined()\n      expect(hyperformJson.amazon.aws_secret_access_key).toBeDefined()\n      expect(hyperformJson.amazon.aws_region).toBeDefined()\n    })\n  })\n})\n"
  },
  {
    "path": "kindler/index.js",
    "content": "/* eslint-disable arrow-body-style */\nconst path = require('path')\n\n/**\n * Appends code to an index.js (\"code\") in \"dir\" that imports \n * and immediately exports given \"infos\".  Has no side effects.\n * Needed for Google that only looks into index.js\n * @param {string} code\n * @param {string} dir\n * @param {[ {p: string, exps: string[] }]}  infos For instance  [\n      {\n        p: '/home/qng/dir/somefile.js',\n        exps: [ 'endpoint_hello' ]\n      }\n    ]\n * */\nfunction kindle(code, dir, infos) {\n  const kindleAppendix = `\n  ;module.exports = {\n    ${\n  // for each file\n  infos.map(({ p, exps }) => {\n    // for each endpoint export\n    return exps.map((exp) => {\n      const relPath = path.relative(dir, p)\n      // it's exported from index.js, whose source code this will be (ie above)\n      if (relPath === 'index.js') {\n        return `${exp}: module.exports.${exp} || exports.${exp},`\n      } else {\n      // it's exported from other file\n        return `${exp}: require('./${relPath}').${exp},`\n      }\n    })\n      .join('\\n')\n  })\n    .join('\\n')\n}\n  };\n  `\n\n  const kindledCode = `\n${code}\n${kindleAppendix}\n`\n  return kindledCode\n}\n\nmodule.exports = {\n  kindle,\n}\n"
  },
  {
    "path": "kindler/index.tes.js",
    "content": "// TODO\n"
  },
  {
    "path": "meta/index.js",
    "content": "/**\n * @description Detects whether jest is running this code\n * @returns {boolean} \n */\nfunction isInTesting() {\n  if (process.env.JEST_WORKER_ID != null) {\n    return true \n  }\n  if (process.env.NODE_ENV === 'test') {\n    return true\n  } \n  return false \n}\n\nmodule.exports = {\n  isInTesting,\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hyperform-cli\",\n  \"version\": \"0.6.12\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"bin\": {\n    \"hf\": \"./cli.js\"\n  },\n  \"scripts\": {\n    \"test\": \"jest --runInBand --setupFiles dotenv/config --coverage\",\n    \"lint\": \"eslint\",\n    \"lintfix\": \"eslint --fix\"\n  },\n  \"engines\": {\n    \"node\": \">=10.10.0\"\n  },\n  \"jest\": {\n    \"testEnvironment\": \"node\"\n  },\n  \"engineStrict\": true,\n  \"author\": \"\",\n  \"license\": \"Apache 2.0\",\n  \"dependencies\": {\n    \"@google-cloud/functions\": \"^1.1.2\",\n    \"aws-sdk\": \"^2.820.0\",\n    \"chalk\": \"^4.1.0\",\n    \"clipboardy\": \"^2.3.0\",\n    \"dotenv\": \"^8.2.0\",\n    \"findit\": \"^2.0.0\",\n    \"joi\": \"^17.3.0\",\n    \"ncp\": \"^2.0.0\",\n    \"node-fetch\": \"^2.6.1\",\n    \"semver\": \"^7.3.5\",\n    \"spinnies\": \"^0.5.1\",\n    \"uuid\": \"^8.3.2\",\n    \"webpack\": \"^5.4.0\",\n    \"yazl\": \"^2.5.1\",\n    \"zip-dir\": \"^2.0.0\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^7.15.0\",\n    \"eslint-config-airbnb-base\": \"^14.2.1\",\n    \"eslint-plugin-import\": \"^2.22.1\",\n    \"husky\": \"^4.3.6\",\n    \"jest\": \"25.0.0\",\n    \"webpack-cli\": \"^4.2.0\"\n  }\n}\n"
  },
  {
    "path": "parser/index.js",
    "content": "// validates and parses hyperform.json \n\nconst path = require('path')\nconst { amazonSchema, googleSchema } = require('../schemas/index')\n\nlet parsedHyperformJson \n\n/**\n * @description Parses v,alidates, and returns contents of \"dir\"/hyperform.json\n * @param {string} dir Directory where to look for hyperform.json\n * @param {string} platform Whether to expect 'amazon' or 'google' content\n * \n */\nfunction getParsedHyperformJson(dir, platform) {\n  if (parsedHyperformJson == null) {\n    const json = require(path.join(dir, 'hyperform.json'))\n    // validate its schema\n    let schema \n    if (platform === 'amazon') schema = amazonSchema\n    if (platform === 'google') schema = googleSchema\n    // throws if platform is not 'amazon' or 'google'\n    const { error, value } = schema.validate(json)\n    if (error) {\n      throw new Error(`${error} ${value}`)\n    }\n    parsedHyperformJson = json \n  }\n\n  return parsedHyperformJson\n}\n\nmodule.exports = {\n  getParsedHyperformJson,\n}\n"
  },
  {
    "path": "printers/index.js",
    "content": "const Spinnies = require('spinnies')\nconst { isInTesting } = require('../meta/index')\n\nconst spinner = {\n  interval: 80,\n  frames: [\n    '⠁',\n    '⠂',\n    '⠄',\n    '⡀',\n    '⢀',\n    '⠠',\n    '⠐',\n    '⠈',\n  ],\n}\n\nconst spinnies = new Spinnies({ color: 'white', succeedColor: 'white', spinner: spinner });\n\nconst { log } = console\nlet logdev \n\n// Don't show dev-level logging\n// (Comment out to show dev-level logging)\nlogdev = () => { }\n// Don't show timings\n// (Comment out to see timings)\nconsole.time = () => { }\nconsole.timeEnd = () => { }\n\n// In testing, be silent but console.log successes and fails\nif (isInTesting() === true) {\n  spinnies.add = () => { }\n  spinnies.update = () => { }\n  spinnies.remove = () => { }\n  spinnies.succeed = (_, { text }) => console.log(text)\n  spinnies.fail = (_, { text }) => console.log(text)\n  spinnies.updateSpinnerState = () => {}\n}\n\nspinnies.f = spinnies.fail \nspinnies.succ = spinnies.succeed \n\nmodule.exports = {\n  spinnies,\n  log,\n  logdev,\n}\n"
  },
  {
    "path": "publisher/amazon/index.js",
    "content": "const {\n  createApi, \n  createIntegration, \n  createDefaultAutodeployStage, \n  setRoute, \n  allowApiGatewayToInvokeLambda, \n  getApiDetails, \n} = require('./utils')\n// TODO handle regional / edge / read up on how edge works\n\nconst HFAPINAME = 'hyperform-v1'\n\n/**\n * @description Creates a public HTTP endpoint that forwards request to a given Lambda.\n * @param {string} lambdaArn \n * @param {string} region\n * @returns {Promise<string>} Full endpoint URL, eg. https://48ytz1e6f3.execute-api.us-east-2.amazonaws.com/endpoint-hello\n */\nasync function publishAmazon(lambdaArn, region) {\n  // console.log('received lambdaar', lambdaArn)\n  const lambdaName = lambdaArn.split(':').slice(-1)[0]\n  // Lambda 'endpoint-hello' should be at 'https://.../endpoint-hello'\n  const routePath = `/${lambdaName}`\n\n  /// ///////////////////////////////////////////////\n  /// //ensure HF API exists in that region /////////\n  // TODO to edge\n  /// //////////////////////////////////////////////\n\n  let hfApiId \n  let hfApiUrl\n\n  /// ///////////////////////////////////////////////\n  /// Check if HF umbrella API exists in that region\n  /// //////////////////////////////////////////////\n  \n  const apiDetails = await getApiDetails(HFAPINAME, region)\n  // exists\n  // use it\n  if (apiDetails != null && apiDetails.apiId != null) {\n    hfApiId = apiDetails.apiId\n    hfApiUrl = apiDetails.apiUrl\n    // does not exist\n    // create HF API\n  } else {\n    const createRes = await createApi(HFAPINAME, region)\n    hfApiId = createRes.apiId\n    hfApiUrl = createRes.apiUrl\n  }\n  \n  /// ///////////////////////////////////////////////\n  /// Add permission to API to lambda accessed by API gateway\n  /// //////////////////////////////////////////////\n\n  // todo iwann spezifisch der api access der lambda erlauben via SourceArn\n  await allowApiGatewayToInvokeLambda(lambdaName, region)\n\n  /// ///////////////////////////////////////////////\n  ///  Create integration that represents the Lambda\n  /// //////////////////////////////////////////////\n\n  const integrationId = await createIntegration(hfApiId, region, lambdaArn)\n\n  /// ///////////////////////////////////////////////\n  ///  Create $default Auto-Deploy stage\n  /// //////////////////////////////////////////////\n\n  try {\n    await createDefaultAutodeployStage(hfApiId, region)\n  } catch (e) {\n    // already exists (shouldn't throw because of anything other)\n    // nice\n  }\n\n  /// ///////////////////////////////////////////////\n  /// Create / update route with that integration\n  /// //////////////////////////////////////////////\n\n  await setRoute(\n    hfApiId,\n    region,\n    routePath,\n    integrationId,\n  )\n\n  const endpointUrl = hfApiUrl + routePath\n\n  return endpointUrl\n}\n\nmodule.exports = {\n  publishAmazon,\n}\n"
  },
  {
    "path": "publisher/amazon/utils.js",
    "content": "const AWS = require('aws-sdk')\nconst { log, logdev } = require('../../printers/index')\n\nconst conf = {\n  accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n  region: process.env.AWS_REGION, \n  // may, may not be defined\n  // sessionToken: process.env.AWS_SESSION_TOKEN || undefined, \n}\n\nif (process.env.AWS_SESSION_TOKEN != null && process.env.AWS_SESSION_TOKEN !== 'undefined') {\n  conf.sessionToken = process.env.AWS_SESSION_TOKEN\n}\n\nAWS.config.update(conf)\n\n/**\n * @description Creates a new REGIONAL API in \"region\" named \"apiName\"\n * @param {string} apiName Name of API\n * @param {string} apiRegion \n * @returns {Promise<{apiId: string, apiUrl: string}>} Id and URL of the endpoint\n */\nasync function createApi(apiName, apiRegion) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  // @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#createApi-property\n  const createApiParams = {\n    Name: apiName,\n    ProtocolType: 'HTTP',\n    // TODO regional is default, but how the to set to EDGE later on?\n    // EndpointType: 'REGIONAL', // invalid field\n    // Target: targetlambdaArn, // TODO\n    CorsConfiguration: {\n      AllowMethods: [\n        'POST',\n        'GET',\n      ],\n      AllowOrigins: [\n        '*',\n      ],\n    },\n  }\n\n  const createApiRes = await apigatewayv2.createApi(createApiParams).promise()\n\n  const res = {\n    apiId: createApiRes.ApiId,\n    apiUrl: createApiRes.ApiEndpoint,\n  }\n\n  return res\n}\n\n/**\n * \n * @param {string} apiId \n * @param {string} apiRegion \n * @returns {Promise<void>}\n */\nasync function deleteApi(apiId, apiRegion) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n  const deleteApiParams = {\n    ApiId: apiId,\n  }\n  await apigatewayv2.deleteApi(deleteApiParams).promise()\n}\n\n/**\n * @description Returns the IntegrationId of the integration that matches 'name', or null.\n * If multiple exist, it returns the IntegrationId of the first one in the list.\n * @param {*} apiId \n * @param {*} apiRegion \n * @param {*} name \n */\nasync function getIntegrationId(apiId, apiRegion, name) {\n  // On amazon, integration names are not unique, \n  // but HF treats the as unique\n  // and always reuses them\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  const getParams = {\n    ApiId: apiId,\n    MaxResults: '9999',\n  }\n\n  // Get all integrations\n  let res = await apigatewayv2.getIntegrations(getParams).promise()\n\n  res = res.Items \n    // Only integrations that match name\n    .filter((el) => el.IntegrationUri.split(':')[-1] === name)\n\n  // Just take the first one \n  res = res[0]\n\n  if (res && res.IntegrationId) {\n    return res.IntegrationId\n  } else {\n    return null\n  }\n}\n\n/**\n * @description Gets route Ids of GET and POST methods that match routePath\n * @param {*} apiId \n * @param {*} apiRegion \n * @param {*} routePath \n * @returns {[ { AuthorizationType: string, RouteId: string, RouteKey: string }]}\n */\nasync function getGETPOSTRoutesAt(apiId, apiRegion, routePath) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  // TODO Amazon might return a paginated response here  (?)\n  // In that case with many routes, the route we look for may not be on first page\n  const params = {\n    ApiId: apiId,\n    MaxResults: '9999', // string according to docs and it works... uuh?\n  }\n\n  const res = await apigatewayv2.getRoutes(params).promise() \n\n  const matchingRoutes = res.Items\n    .filter((item) => item.RouteKey && item.RouteKey.includes(routePath) === true)\n    // only GET and POST ones\n    .filter((item) => /GET|POST/.test(item.RouteKey) === true)\n\n  return matchingRoutes\n}\n\n/**\n * Creates or update GET and POST routes with an integration. \n * If only one of GET or POST routes exist (user likely deleted one of them),\n * it updates that one. \n * Otherwise, it creates new both GET, POST routes.\n *  Use createDefaultAutodeployStage too when creating, so that changes are made public\n * @param {*} apiId \n * @param {*} apiRegion \n * @param {*} routePath '/endpoint-1' for example\n * @param {*} integrationId\n */\nasync function setRoute(apiId, apiRegion, routePath, integrationId) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  // Get route ids of GET & POST at that routePath\n  const routes = await getGETPOSTRoutesAt(apiId, apiRegion, routePath)\n\n  if (routes.length > 0) {\n    /// ////////////////////////////////////////////////\n    // Update routes (GET, POST) with integrationId\n    /// ////////////////////////////////////////////////\n   \n    await Promise.all(\n      routes.map(async (r) => {\n        // skip if integration id is set correctly already\n        if (r.Target && r.Target === `integrations/${integrationId}`) {\n          return\n        }\n\n        const updateRouteParams = {\n          ApiId: apiId,\n          AuthorizationType: 'NONE',\n          RouteId: r.RouteId,\n          RouteKey: r.RouteKey,\n          Target: `integrations/${integrationId}`,\n        }\n\n        try {\n          const updateRes = await apigatewayv2.updateRoute(updateRouteParams).promise()\n        } catch (e) {\n          // This happens eg when there is only one of GET OR POST route (routes.length > 0)\n          // Usually when the user deliberately deleted one of the\n          // Ignore, as it's likely intented\n        }\n      }),\n    )\n  } else {\n    /// ////////////////////////////////////////////////\n    // Create routes (GET, POST) with integrationId\n    /// ////////////////////////////////////////////////\n    \n    // Create GET route\n    const createGETRouteParams = {\n      ApiId: apiId,\n      AuthorizationType: 'NONE',\n      RouteKey: `GET ${routePath}`,\n      Target: `integrations/${integrationId}`,\n    }\n\n    const createGETRes = await apigatewayv2.createRoute(createGETRouteParams).promise()\n\n    // Create POST route\n    const createPOSTRouteParams = { ...createGETRouteParams }\n    createPOSTRouteParams.RouteKey = `POST ${routePath}`\n\n    const createPOSTRes = await apigatewayv2.createRoute(createPOSTRouteParams).promise()\n  }\n}\n\n/**\n * Creates a (possibly duplicate) integration that can be attached. \n * Before creating, check getIntegrationId to avoid duplicates\n * @param {*} apiId \n * @param {*} apiRegion \n * @param {string} targetLambdaArn For instance a Lambda ARN\n * @returns {string} IntegrationId\n */\nasync function createIntegration(apiId, apiRegion, targetLambdaArn) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  const params = {\n    ApiId: apiId,\n    IntegrationType: 'AWS_PROXY',\n    IntegrationUri: targetLambdaArn,\n    PayloadFormatVersion: '2.0',\n  }\n\n  const res = await apigatewayv2.createIntegration(params).promise()\n  const integrationId = res.IntegrationId \n\n  return integrationId\n}\n\n/**\n * @description Returns ApiId and ApiEndpoint of a regional API gateway API\n *  with the name \"apiName\", in \"apiRegion\".\n * If multiple APIs exist with that name, it warns, and uses the first one in the received list. \n * If none exist, it returns null.\n * @param {string} apiName \n * @param {string} apiRegion\n * @returns {Promise<{apiId: string, apiUrl: string}>} Details of the API, or null\n */\nasync function getApiDetails(apiName, apiRegion) {\n  // Check if API with that name exists\n  // Follows Hyperform conv: same name implies identical, for lambdas, and api endpoints etc\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n\n  const getApisParams = {\n    MaxResults: '9999',\n  }\n\n  const res = await apigatewayv2.getApis(getApisParams).promise()\n\n  const matchingApis = res.Items.filter((item) => item.Name === apiName)\n  if (matchingApis.length === 0) {\n    // none exist\n    return null\n  }\n\n  if (matchingApis.length >= 2) {\n    log(`Multiple (${matchingApis.length}) APIs found with same name ${apiName}. Using first one`)\n  }\n\n  // just take first one\n  // Hyperform convention is there's only one with any given name\n  const apiDetails = {\n    apiId: matchingApis[0].ApiId,\n    apiUrl: matchingApis[0].ApiEndpoint,\n  }\n\n  return apiDetails\n}\n\n/**\n * \n * @param {*} apiId \n * @param {*} apiRegion \n * @throws If $default stage already exists\n */\nasync function createDefaultAutodeployStage(apiId, apiRegion) {\n  const apigatewayv2 = new AWS.ApiGatewayV2({\n    apiVersion: '2018-11-29',\n    region: apiRegion,\n  })\n  const params = {\n    ApiId: apiId,\n    StageName: '$default',\n    AutoDeploy: true,\n  }\n\n  const res = await apigatewayv2.createStage(params).promise()\n}\n\n/**\n * @description Add permssion to allow API gateway to invoke given Lambda\n * @param {string} lambdaName Name of Lambda\n * @param {string} region Region of Lambda\n * @returns {Promise<void>}\n */\nasync function allowApiGatewayToInvokeLambda(lambdaName, region) {\n  const lambda = new AWS.Lambda({\n    region: region,\n    apiVersion: '2015-03-31',\n  })\n\n  const addPermissionParams = {\n    Action: 'lambda:InvokeFunction',\n    FunctionName: lambdaName,\n    Principal: 'apigateway.amazonaws.com',\n    // TODO SourceArn https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#addPermission-property\n    StatementId: `hf-stmnt-${lambdaName}`,\n  }\n\n  try {\n    await lambda.addPermission(addPermissionParams).promise()\n  } catch (e) {\n    if (e.code === 'ResourceConflictException') {\n      // API Gateway can already access that lambda (happens on all subsequent deploys), cool\n    } else {\n      logdev(`addpermission: some other error: ${e}`)\n      throw e\n    }\n  }\n}\n\nmodule.exports = {\n  createApi,\n  _only_for_testing_deleteApi: deleteApi,\n  allowApiGatewayToInvokeLambda,\n  getApiDetails,\n  createIntegration,\n  setRoute,\n  createDefaultAutodeployStage,\n}\n"
  },
  {
    "path": "publisher/amazon/utils.test.js",
    "content": "/* eslint-disable global-require */\nconst APIREGION = 'us-east-2'\n\n// other functions in publisher/amazon are pretty well covered by other tests\ndescribe('utils', () => {\n  describe('createApi', () => {\n    test('completes and returns apiId and apiUrl that is an URL', async () => {\n      const uuidv4 = require('uuid').v4 \n      const { createApi } = require('./utils')\n      const deleteApi = require('./utils')._only_for_testing_deleteApi\n      \n      const apiName = `jest-reserved-api-${uuidv4()}`\n      \n      let err \n      let res \n      try {\n        res = await createApi(apiName, APIREGION)\n      } catch (e) {\n        console.log(e)\n        err = e\n      }\n\n      // createApi did not throw\n      expect(err).not.toBeDefined()\n      expect(res).toBeDefined()\n      expect(typeof res.apiUrl).toBe('string')\n      expect(typeof res.apiId).toBe('string')\n\n      // apiUrl is an URL \n      const tryUrl = () => new URL(res.apiUrl)\n      expect(tryUrl).not.toThrow()\n\n      /// //////////////////////\n      // Clean up: Delete API\n\n      await deleteApi(res.apiId, APIREGION)\n    }, 10000)\n  })\n\n  describe('setRoute', () => {\n    test('completes on non-existing routes, existing routes', async () => {\n      /// Create API\n      const uuidv4 = require('uuid').v4 \n      const fetch = require('node-fetch')\n      const {\n        createApi, createIntegration, setRoute, createDefaultAutodeployStage, \n      } = require('./utils')\n      const deleteApi = require('./utils')._only_for_testing_deleteApi\n      \n      const apiName = `jest-reserved-api-${uuidv4()}`\n      const routePath = '/endpoint_test'\n      \n      const { apiId, apiUrl } = await createApi(apiName, APIREGION)\n      \n      // Create Integration to some lambda\n      // we won't call it, so doesn't matter really which lambda \n      const targetLambdaArn = 'arn:aws:lambda:us-east-2:735406098573:function:endpoint_hello'\n      const integrationId = await createIntegration(apiId, APIREGION, targetLambdaArn)\n\n      // Always make changes public (importantly, things we will do with setRoute)\n      await createDefaultAutodeployStage(apiId, APIREGION)\n       \n      let res \n      let err \n      try {\n        res = await setRoute(\n          apiId,\n          APIREGION,\n          routePath,\n          integrationId,\n        )\n      } catch (e) {\n        err = e\n      }\n\n      // setRoute did not throw\n      expect(err).not.toBeDefined()\n      const fullurl = `${apiUrl}${routePath}`\n      console.log(fullurl)\n      // URL returns 200\n      // On GET\n      {\n        const getres = await fetch(fullurl, {\n          method: 'GET',\n        })\n        const statusCode = `${getres.status}`\n        console.log(statusCode)\n        // GET route returns 2XX\n        expect(/^2/.test(statusCode)).toBe(true)\n      }\n      // On POST\n      {\n        const getres = await fetch(fullurl, {\n          method: 'POST',\n        })\n        const statusCode = `${getres.status}`\n        console.log(statusCode)\n        // POST route returns 2XX\n        expect(/^2/.test(statusCode)).toBe(true)\n      }\n\n      await deleteApi(apiId, APIREGION)\n    }, 10000)\n  })\n})\n"
  },
  {
    "path": "response-collector/.gitignore",
    "content": "\nhyperform.json"
  },
  {
    "path": "response-collector/index.js",
    "content": "const aws = require('aws-sdk')\nconst uuidv4 = require('uuid').v4\n\n/**\n * @description This serverless function gathers responses sent \n * by users answering $ hyperform survey\n */\n\nasync function collectSurveyResponse(event) {\n  const s3 = new aws.S3()\n  const filename = `${new Date().toDateString()}:${uuidv4()}.json`\n\n  if (event == null) return \n  \n  const putParams = {\n    Bucket: 'hyperform-survey-responses',\n    Key: `cli-responses/${filename}`,\n    Body: JSON.stringify(event, null, 2),\n  }\n  await s3.putObject(putParams).promise()\n}\n\nfunction getSurveyQuestion() {\n  return {\n    text: 'Some survey text',\n    postUrl: 'https://mj9jbzlpxi.execute-api.us-east-2.amazonaws.com',\n  }\n}\n\nmodule.exports = {\n  endpoint_getSurveyQuestion: getSurveyQuestion,\n  endpoint_collectSurveyResponse: collectSurveyResponse,\n}\n"
  },
  {
    "path": "schemas/index.js",
    "content": "const joi = require('joi')\n\nconst amazonSchema = joi.object({\n  amazon: joi.object({\n    aws_access_key_id: joi.string().required(),\n    aws_secret_access_key: joi.string().required(), \n    aws_region: joi.string().required(),\n    // allow if user enters it\n    aws_session_token: joi.string().allow(''),\n  }).required(),\n})\n\nconst googleSchema = joi.object({\n  google: joi.object({\n    gc_project: joi.string().required(),\n    gc_region: joi.string().required(),\n  }).required(),\n})\n\nmodule.exports = {\n  amazonSchema,\n  googleSchema,\n}\n"
  },
  {
    "path": "schemas/index.test.js",
    "content": "/* eslint-disable global-require */\ndescribe('schemas', () => {\n  describe('amazon schema', () => {\n    test('normal case', () => {\n      const { amazonSchema } = require('./index')\n\n      const input = {\n        amazon: {\n          aws_access_key_id: 'xx',\n          aws_secret_access_key: 'xx',\n          aws_region: 'xx',\n        },\n      }\n\n      const { error } = amazonSchema.validate(input)\n      expect(error).not.toBeDefined()\n    })\n\n    test('allows aws_session_token field', () => {\n      const { amazonSchema } = require('./index')\n\n      const input = {\n        amazon: {\n          aws_access_key_id: 'xx',\n          aws_secret_access_key: 'xx',\n          aws_region: 'xx',\n          aws_session_token: 'xx',\n        },\n      }\n\n      const { error } = amazonSchema.validate(input)\n      expect(error).not.toBeDefined()\n    })\n\n    test('does not allow missing field in amazon', () => {\n      const { amazonSchema } = require('./index')\n\n      const input = {\n        amazon: {\n          aws_access_key_id: 'xx',\n          aws_secret_access_key: 'xx',\n          // Missing: aws_region: '',\n        },\n      }\n\n      const { error } = amazonSchema.validate(input)\n      expect(error).toBeDefined()\n    })\n\n    test('does not allow both providers', () => {\n      const { amazonSchema } = require('./index')\n\n      const input = {\n        amazon: {\n          aws_access_key_id: 'abc',\n          aws_secret_access_key: 'abc',\n          aws_region: 'abc',\n        },\n        google: {\n          gc_project: 'abc',\n          gc_region: 'abc',\n        },\n      }\n\n      const { error } = amazonSchema.validate(input)\n      expect(error).toBeDefined()\n    })\n\n    test('does not allow no provider', () => {\n      const { amazonSchema } = require('./index')\n\n      const input = {\n        // empty\n      }\n\n      const { error } = amazonSchema.validate(input)\n      expect(error).toBeDefined()\n    })\n  })\n\n  describe('google schema', () => {\n    test('normal case', () => {\n      const { googleSchema } = require('./index')\n\n      const input = {\n        google: {\n          gc_project: 'abc',\n          gc_region: 'abc',\n        },\n      }\n\n      const { error } = googleSchema.validate(input)\n      expect(error).not.toBeDefined()\n    })\n  })\n})\n"
  },
  {
    "path": "surveyor/index.js",
    "content": "const fetch = require('node-fetch')\n\n// 0 to 1, with which to show survey\nconst probability = 0.04\n\nconst getSurveyUrl = 'https://era1vrrco0.execute-api.us-east-2.amazonaws.com'\nconst postSurveyAnswerUrl = 'https://mj9jbzlpxi.execute-api.us-east-2.amazonaws.com'\n\nfunction maybeShowSurvey() {\n  if ((new Date().getSeconds() / 60) < probability) {\n    fetch(getSurveyUrl)\n      .then((res) => res.json())\n      .then((res) => console.log(`\n\n  ${res.text}\n  You can type $ hf answer ... to answer :)\n  `))\n  }\n}\n\nasync function answerSurvey(text) {\n  const currentSurvey = await fetch(getSurveyUrl)\n    .then((res) => res.json())\n  \n  await fetch(postSurveyAnswerUrl, {\n    method: 'POST',\n    body: JSON.stringify({\n      currentSurvey: currentSurvey,\n      answer: text,\n      date: new Date(),\n    }),\n  })\n}\n\nmodule.exports = {\n  maybeShowSurvey,\n  answerSurvey,\n}\n"
  },
  {
    "path": "template/.eslintrc.json",
    "content": "{\n  \"env\": {\n    \"commonjs\": true,\n    \"es2021\": true,\n    \"node\": true,\n    \"jest\": true\n  },\n  \"extends\": [\n    \"airbnb-base\"\n  ],\n  \"parserOptions\": {\n    \"ecmaVersion\": 12\n  },\n  \"rules\": {\n    \"no-console\": \"off\",\n    \"object-shorthand\": \"off\",\n    \"no-restricted-syntax\": \"warn\",\n    \"prefer-destructuring\": \"warn\"\n  }\n}"
  },
  {
    "path": "template/index.js",
    "content": "/**\n * @description The module appendix template. Never import this,\n * only copy-paste from here to transpiler/index.js\n * @param {*} moduleexp\n * @param {*} [exp]\n */\nmodule.exports = () => {\n  // START PASTE\n\n  /**\n    * Start Hyperform wrapper\n    * It provides some simple usability features\n    * Amazon:\n    * Google:\n    *    - Send pre-flight headers\n    *    - console.error on error\n    */\n  global.alreadyWrappedNames = [];\n\n  function wrapExs(me, platform) {\n    const newmoduleexports = { ...me };\n    const expkeys = Object.keys(me);\n    for (let i = 0; i < expkeys.length; i += 1) {\n      const expkey = expkeys[i];\n      const userfunc = newmoduleexports[expkey];\n      // it should be idempotent\n      // TODO fix code so this doesn't happen\n      if (global.alreadyWrappedNames.includes(expkey)) {\n        continue;\n      }\n      global.alreadyWrappedNames.push(expkey);\n      let wrappedfunc;\n      if (platform === 'amazon') {\n        wrappedfunc = async function handler(event, context, callback) {\n          /// ////////////////////////////////\n          // Invoke user function ///////\n          /// ////////////////////////////////\n\n          const res = await userfunc(event, context, callback);\n          context.succeed(res);\n          // throwing will call context.fail automatically\n        };\n      }\n      if (platform === 'google') {\n        wrappedfunc = async function handler(req, resp, ...rest) {\n          // allow to be called from anywhere (also localhost)\n          //    resp.header('Content-Type', 'application/json');\n          resp.header('Access-Control-Allow-Origin', '*');\n          resp.header('Access-Control-Allow-Headers', '*');\n          resp.header('Access-Control-Allow-Methods', '*');\n          resp.header('Access-Control-Max-Age', 30);\n\n          // respond to CORS preflight requests\n          if (req.method === 'OPTIONS') {\n            resp.status(204).send('');\n          } else {\n            // Invoke user function\n            // (user must .json or .send himself)\n            try {\n              await userfunc(req, resp, ...rest);\n            } catch (e) {\n              console.error(e);\n              resp.status(500).send(''); // TODO generate URL to logs (similar to GH)\n            }\n          }\n        };\n      }\n      newmoduleexports[expkey] = wrappedfunc;\n    }\n    return newmoduleexports;\n  }\n  const curr = { ...exports, ...module.exports };\n  const isInAmazon = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_EXECUTION_ENV);\n  const isInGoogle = (/google/.test(process.env._) === true);\n  if (isInAmazon === true) {\n    return wrapExs(curr, 'amazon');\n  }\n  if (isInGoogle === true) {\n    return wrapExs(curr, 'google');\n  }\n  return curr; // Export unchanged (local, fallback)\n\n  // END PASTE\n};\n"
  },
  {
    "path": "transpiler/index.js",
    "content": "/**\n * @description Transpiles Javascript code so that its exported functions can run on Amazon, Google\n * @param {string} bundleCode Bundled Javascript code. \n * Output of the 'bundle' function in bundle/index.js\n * @returns {string} The code, transpiled for providers\n */\nfunction transpile(bundleCode) {\n  const appendix = `\n\n;module.exports = (() => {\n\n  // START PASTE\n\n  /**\n    * Start Hyperform wrapper\n    * It provides some simple usability features\n    * Amazon:\n    * Google:\n    *    - Send pre-flight headers\n    *    - console.error on error\n    */\n  global.alreadyWrappedNames = [];\n\n  function wrapExs(me, platform) {\n    const newmoduleexports = { ...me };\n    const expkeys = Object.keys(me);\n    for (let i = 0; i < expkeys.length; i += 1) {\n      const expkey = expkeys[i];\n      const userfunc = newmoduleexports[expkey];\n      // it should be idempotent\n      // TODO fix code so this doesn't happen\n      if (global.alreadyWrappedNames.includes(expkey)) {\n        continue;\n      }\n      global.alreadyWrappedNames.push(expkey);\n      let wrappedfunc;\n      if (platform === 'amazon') {\n        wrappedfunc = async function handler(event, context, callback) {\n          /// ////////////////////////////////\n          // Invoke user function ///////\n          /// ////////////////////////////////\n\n          const res = await userfunc(event, context, callback);\n          context.succeed(res);\n          // throwing will call context.fail automatically\n        };\n      }\n      if (platform === 'google') {\n        wrappedfunc = async function handler(req, resp, ...rest) {\n          // allow to be called from anywhere (also localhost)\n          //    resp.header('Content-Type', 'application/json');\n          resp.header('Access-Control-Allow-Origin', '*');\n          resp.header('Access-Control-Allow-Headers', '*');\n          resp.header('Access-Control-Allow-Methods', '*');\n          resp.header('Access-Control-Max-Age', 30);\n\n          // respond to CORS preflight requests\n          if (req.method === 'OPTIONS') {\n            resp.status(204).send('');\n          } else {\n            // Invoke user function\n            // (user must .json or .send himself)\n            try {\n              await userfunc(req, resp, ...rest);\n            } catch (e) {\n              console.error(e);\n              resp.status(500).send(''); // TODO generate URL to logs (similar to GH)\n            }\n          }\n        };\n      }\n      newmoduleexports[expkey] = wrappedfunc;\n    }\n    return newmoduleexports;\n  }\n  const curr = { ...exports, ...module.exports };\n  const isInAmazon = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_EXECUTION_ENV);\n  const isInGoogle = (/google/.test(process.env._) === true);\n  if (isInAmazon === true) {\n    return wrapExs(curr, 'amazon');\n  }\n  if (isInGoogle === true) {\n    return wrapExs(curr, 'google');\n  }\n  return curr; // Export unchanged (local, fallback)\n\n\n})();\n`\n\n  const transpiledCode = bundleCode + appendix\n\n  return transpiledCode\n}\n\nmodule.exports = {\n  transpile,\n}\n"
  },
  {
    "path": "uploader/amazon/index.js",
    "content": "// const AWS = require('aws-sdk')\n\n// /**\n//  * \n//  * @param {*} localpath \n//  * @param {*} bucket \n//  * @param {*} key \n//  */\n// async function uploadAmazon(localpath, bucket, key) {\n//   const s3 = new AWS.S3()\n//   const fsp = require('fs').promises \n\n//   const contents = await fsp.readFile(localpath)\n//   const uploadParams = {\n//     Bucket: bucket,\n//     Key: key,\n//     Body: contents,\n//   }\n//   await s3.upload(uploadParams).promise() \n\n//   const s3path = `s3://${bucket}/${key}`\n//   return s3path \n// }\n\n// module.exports = {\n//   uploadAmazon,\n// }\n\n"
  },
  {
    "path": "uploader/amazon/index.test.js",
    "content": "/* eslint-disable global-require */\n\nconst S3BUCKET = 'jak-bridge-typical'\n\ndescribe('uploader', () => {\n  describe('amazon', () => {\n    describe('uploadAmazon', () => {\n      test('uploads simple text file, subsequent get returns that file', async () => {\n      //   const AWS = require('aws-sdk')\n      //   const s3 = new AWS.S3()\n      //   const uuidv4 = require('uuid').v4\n       \n        //   const { uploadAmazon } = require('./index')\n        //   const key = `${uuidv4()}`\n        //   const filecontents = key\n        //   const filecontentsbuffer = Buffer.from(key)\n\n        //   let res \n        //   let err \n        //   try {\n        //     res = await uploadAmazon(filecontentsbuffer, S3BUCKET, key)\n        //   } catch (e) {\n        //     console.log(e)\n        //     err = e\n        //   }\n\n        //   // it didn't throw\n        //   expect(err).not.toBeDefined()\n        //   // it returned s3:// ...\n        //   expect(typeof res).toBe('string')\n        //   expect(res).toBe(`s3://${S3BUCKET}/${key}`)\n\n        //   // getting file immediately after must have same contents\n        //   const getParams = {\n        //     Bucket: S3BUCKET,\n        //     Key: key,\n        //   }\n        //   const getRes = await s3.getObject(getParams).promise()\n        //   const gottenFilecontents = getRes.Body.toString('utf-8')\n\n      //   expect(gottenFilecontents).toBe(filecontents)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "uploader/google/index.js",
    "content": "// const { Storage } = require('@google-cloud/storage')\n\n// const gcloudstorage = new Storage() \n\n// async function uploadGoogle(localpath, bucket, key) {\n//   const res = await gcloudstorage.bucket(bucket).upload(localpath, { \n//     gzip: true,\n//     destination: key,\n//     metadata: {\n//       // Docs: (If the contents will change, use cacheControl: 'no-cache')\n//       // @see https://github.com/googleapis/nodejs-storage/blob/master/samples/uploadFile.js\n//       cacheControl: 'no-cache',\n//     },\n//   })\n\n//   const gsPath = `gs://${bucket}/${key}`\n//   return gsPath\n// }\n\n// module.exports = {\n//   uploadGoogle,\n// }\n"
  },
  {
    "path": "zipper/google/index.js",
    "content": "const _zipdir = require('zip-dir')\nconst fsp = require('fs').promises\nconst path = require('path')\nconst os = require('os')\n/**\n * \n * @param {string} dir \n * @param {string[]} except names of directories or files that will not be included \n * (usually [\"node_modules\", \".git\", \".github\"]) Uses substring check.\n * @returns {string} outpath of the zip\n * \n */\nasync function zipDir(dir, except) {\n  // create tmp dir\n  const outdir = await fsp.mkdtemp(path.join(os.tmpdir(), 'zipDir-zipped-'))\n  const outpath = path.join(outdir, 'deploypackage.zip')\n\n  // The second argument of https://www.npmjs.com/package/zip-dir\n  // Function that is called on every file / dir to determine if it'll be included in the zip\n  const filterFunc = (p, stat) => {\n    for (let i = 0; i < except.length; i += 1) {\n      if (p.includes(except[i])) {\n        console.log(`Excluding ${p}`)\n        return false \n      }\n    }\n    return true \n  }\n\n  const res = await new Promise((resolve, rej) => {\n    _zipdir(dir, {\n      saveTo: outpath,\n      filter: filterFunc,\n\n    },\n    (err, r) => {\n      if (err) {\n        rej(err)\n      } else {\n        resolve(outpath) // resolve with the outpath\n      } \n    })\n  })\n\n  return res \n}\n\nmodule.exports = {\n  zipDir,\n}\n"
  },
  {
    "path": "zipper/index.js",
    "content": "const fsp = require('fs').promises\nconst fs = require('fs')\nconst os = require('os')\nconst path = require('path')\n\nconst { Readable } = require('stream')\n\nconst yazl = require('yazl')\n/**\n * @description Creates a .zip that contains given filecontents, within given filenames. All at the zip root\n * @param {{}} filesobj For instance { 'file.txt': 'abc' }\n * @returns {Promise<string>} Path to the created .zip\n */\nasync function zip(filesobj) {\n  const uid = `${Math.ceil(Math.random() * 10000)}`\n  const zipfile = new yazl.ZipFile()\n\n  console.time(`zip-${uid}`)\n  // create tmp dir\n  const outdir = await fsp.mkdtemp(path.join(os.tmpdir(), 'zipped-'))\n  const outpath = path.join(outdir, 'deploypackage.zip')\n\n  zipfile.outputStream.pipe(fs.createWriteStream(outpath))\n\n  // filesobj is like { 'file.txt': 'abc', 'file2.txt': '123' }\n  // for each such destination file,...\n  const fnames = Object.keys(filesobj)\n  for (let i = 0; i < fnames.length; i += 1) {\n    const fname = fnames[i]\n    const fcontent = filesobj[fname]\n\n    // set up stream\n    const s = new Readable();\n    s._read = () => {};\n    s.push(fcontent);\n    s.push(null);\n\n    // In zip, set last-modified header to 01-01-2020\n    // this way, rezipping identical files is deterministic (gives the same codesha256)\n    // that way we can skip uploading zips that haven't changed\n    const options = {\n      mtime: new Date(1577836),\n    }\n\n    zipfile.addReadStream(s, fname, options); // place code in index.js inside zip\n  //  console.log(`created ${fname} in zip`)\n  }\n\n  zipfile.end()\n  \n  console.timeEnd(`zip-${uid}`)\n  return outpath\n}\n\nmodule.exports = {\n  zip,\n}\n"
  },
  {
    "path": "zipper/index.test.js",
    "content": "describe('zipper', () => {\n  test('completes with multiple files and returns valid path', async () => {\n    const path = require('path')\n    const fs = require('fs')\n    const { zip } = require('./index')\n\n    const inp = {\n      'fileWithinZip.txt': 'abc',\n    }\n\n    let err\n    let res \n    try {\n      res = await zip(inp)\n    } catch (e) {\n      console.log(e)\n      err = e\n    }\n\n    expect(err).not.toBeDefined()\n\n    // expect it's a path \n    // https://stackoverflow.com/a/38974272\n    expect(res === path.basename(res)).toBe(false)\n\n    // expect it ends with '.zip'\n    expect(path.extname(res)).toBe('.zip')\n\n    // expect we can get details about it\n    let statErr  \n    try {\n      fs.statSync(res)\n    } catch (e) {\n      statErr = e\n    }\n\n    expect(statErr).not.toBeDefined()\n  })\n})\n"
  }
]