[
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Want to open a PR on comfy? Thank you! Here are a few things you need to know.\n\n# Project organisation\nThis repository is splitted into a few parts.\n\n- The serverless API\n- The console client\n- The utils & tests\n\nThey all contain their own `makefile` and `package.json`.\n\n```bash\n.\n├── api        # The serverless API (https://comfy.marmelab.com)\n├── cli        # The console client (comfygure, that you can install from npm)\n├── docs       # Built website served by GitHub pages (https://marmelab.com/comfygure)\n└── test       # E2E tests for the API & client\n```\n\n# Installation\n\n```bash\ngit clone git@github.com:marmelab/comfygure.git\ncd comfygure/\nmake install # Install the dependencies of all the projects\nmake -C api install-db  # Create a database into a docker container on port 5432\n```\n\n# Run the project\n\n```bash\nmake -C api run                                        # Run comfy server API on port 3000\n./cli/bin/comfy.js init --origin http://localhost:3000 # Initialize a project on the local API\n```\n\nUse `./cli/bin/comfy.js` instead of the global `comfy` command.\n\n# Testing\n\nNo PR will be merged if the tests don't pass.\n\n```bash\nmake test            # Run ALL the tests (⌐■_■)\n\nmake -C api test     # Run API unit tests only\nmake -C cli test     # Run cli unit tests only\nmake -C test test    # Run E2E tests only\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "Is this issue a question ? Please ask it on StackOverflow with the tag \"comfy\"\nhttps://stackoverflow.com/questions/tagged/comfy/\n\n### Description\n\n[Description of the bug or feature]\n\n### Steps to Reproduce\n\n1. [First Step]\n2. [Second Step]\n3. [and so on...]\n\n**Expected behavior:** [What you expected to happen]\n\n**Actual behavior:** [What actually happened]\n\n**Do this issue happen with the default server (https://comfy.marmelab.com)?:** Yes / No\n\n**If yes, here are my project informations to help you find the logs:**\n\n- Project ID:\n- Exact date when the issue happened:\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nAll versions of comfygure are supported by the security policy.\n\n## Reporting a Vulnerability\n\nTo report a vulnerability, you can :\n- send an plain email to kevin@marmelab.com\n- send an encrypted message on keybase https://keybase.io/kmaschta (then, ping me by mail to be sure I'll read it)\n\nPlease provide all the informations you can, especially the version of comfygure impacted and when the issue happened.\n\nNo bug bounty program is open.\n"
  },
  {
    "path": ".gitignore",
    "content": "cli/node_modules/\n\ntest/node_modules/\ntest/.pm2/\ntest/.env/\n\ndocs/_site/\ndocs/.jekyll-metadata\n\n.comfy/config\n\nsonar-project.properties\n.scannerwork/\n.vscode/\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n    - '8.10'\n    - '9'\n    - '10'\n    - '11'\n    - '12'\n    - '13'\n\nservices:\n    - postgresql\n\naddons:\n    postgresql: '9.4'\n\nbefore_install:\n    - psql -c 'create database comfy;' -U postgres\n    - psql -U postgres comfy < api/var/schema.sql\n\ninstall: make install\nscript: make test\n\ncache:\n    directories:\n        - api/node_modules\n        - cli/node_modules\n        - admin/node_modules\n        - test/node_modules\n\nbranches:\n    only:\n        - master\n        - next\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 marmelab\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: test\n\nPWD = $(shell pwd)\n\ninstall:\n\tmake -C api install\n\tmake -C cli install\n\tmake -C test install\n\nrun:\n\t-make -C api start-db\n\tmake -C api run\n\ntest:\n\tmake -C api test\n\tmake -C cli test\n\tmake -C test test\n\ndeploy:\n\tcd api && NODE_ENV=production make deploy\n\npublish-cli:\n\tnpm publish ./cli\n\nserve-documentation:\n\tdocker run -it --rm \\\n\t\t-p 4000:4000 \\\n\t\t-v \"${PWD}/docs:/usr/src/app\" \\\n\t\tstarefossen/github-pages:onbuild \\\n\t\tjekyll serve \\\n\t\t\t--host=0.0.0.0 \\\n\t\t\t--incremental\n"
  },
  {
    "path": "README.md",
    "content": "[![npm version](https://badge.fury.io/js/comfygure.svg)](https://badge.fury.io/js/comfygure) ![CLI dependencies](https://img.shields.io/david/marmelab/comfygure.svg?label=CLI%20dependencies&path=cli) ![API dependencies](https://img.shields.io/david/marmelab/comfygure.svg?label=API%20dependencies&path=api) [![npm downloads](https://img.shields.io/npm/dt/comfygure.svg)](http://npmjs.com/comfygure) [![docker pulls](https://img.shields.io/docker/pulls/marmelab/comfygure.svg)](https://hub.docker.com/r/marmelab/comfygure) [![Build Status](https://travis-ci.org/marmelab/comfygure.png?branch=master)](https://travis-ci.org/marmelab/comfygure)\n\n# comfygure\n\nEncrypted and versioned configuration storage built with collaboration in mind.\n\n[Source](https://github.com/marmelab/comfygure) - [Releases](https://github.com/marmelab/comfygure/releases) - [Stack Overflow](https://stackoverflow.com/questions/tagged/comfy/)\n\n[![asciicast](https://asciinema.org/a/137703.png)](https://asciinema.org/a/137703)\n\n## Features\n\n-   Simple CLI\n-   End-to-end AES-256 encryption\n-   Multiple formats support (JSON, YAML, environment variables)\n-   Git-like Versioning\n-   Easy to host on your own\n\nComfygure is great to manage application configurations for multiple environments, toggle feature flags quickly, manage A/B testing based on configuration files.\n\nIt is not a [Secret Management Tool](https://gist.github.com/maxvt/bb49a6c7243163b8120625fc8ae3f3cd), it focus on configurations files, their history, and how teams collaborate with them.\n\n## Get Started\n\nOn every server that needs access to the settings of an app, install the `comfy` CLI using `npm`:\n\n```bash\nnpm install -g comfygure\ncomfy help\n```\n\n## Usage\n\nInitialize comfygure in a project directory with `comfy init`:\n\n```bash\n> cd myproject\n> comfy init\n\nInitializing project configuration...\nProject created on comfy server https://comfy.marmelab.com\nConfiguration saved locally in .comfy/config\ncomfy project successfully created\n```\n\nThis creates a unique key to access the settings for `myproject`, and stores the key in `.comfy/config`. You can copy this file to share the credentials with co-workers or other computers.\n\n**Note**: By default, the `comfy` command stores encrypted settings in the `comfy.marmelab.com` server. To host your own comfy server, see [the related documentation](https://marmelab.com/comfygure/HostYourOwn.html).\n\nImport an existing settings file to comfygure using `comfy setall`:\n\n```bash\n> echo '{\"login\": \"admin\", \"password\": \"S3cr3T\"}' > config.json\n> comfy setall development config.json\nGreat! Your configuration was successfully saved.\n```\n\nFrom any computer sharing the same credentials, grab these settings using `comfy get`:\n\n```bash\n> comfy get development\n{\"login\": \"admin\", \"password\": \"S3cr3T\"}\n> comfy get development --envvars\nexport LOGIN='admin';\nexport PASSWORD='S3cr3T';\n```\n\nTo turn settings grabbed from comfygure into environment variables, use the following:\n\n```bash\n> comfy get development --envvars | source /dev/stdin\n> echo $LOGIN\nadmin\n```\n\nSee the [documentation](https://marmelab.com/comfygure/) to know more about how it works and the remote usage.\n\n## License\n\nComfygure is licensed under the [MIT License](https://github.com/marmelab/comfygure/blob/master/LICENSE), sponsored and supported by [marmelab](http://marmelab.com).\n"
  },
  {
    "path": "api/.dockerignore",
    "content": "node_modules/\n"
  },
  {
    "path": "api/.gitignore",
    "content": "node_modules\n.webpack\n.serverless\nconfig/*.js\n!config/database.js\nbuild/\n.env\n"
  },
  {
    "path": "api/Dockerfile",
    "content": "FROM node:10\n\nWORKDIR /usr/src/app\n\nCOPY package*.json ./\nRUN npm ci --only=production\n\nCOPY build ./build\n\nCMD [ \"npm\", \"start\" ]\n"
  },
  {
    "path": "api/babel.config.js",
    "content": "module.exports = {\n    presets: [\n        [\n            '@babel/preset-env',\n            {\n                targets: {\n                    node: 8,\n                },\n            },\n        ],\n    ],\n    plugins: [\n        '@babel/plugin-transform-async-to-generator',\n    ],\n};\n"
  },
  {
    "path": "api/makefile",
    "content": "SERVERLESS := node_modules/.bin/serverless\nDATABASE ?= comfy\n\nexport PGUSER ?= postgres\nexport PGHOST ?= localhost\nexport PGPASSWORD ?= password\n\ninstall:\n\tnpm i\n\ninstall-db:\n\tdocker run \\\n\t\t-e POSTGRES_PASSWORD=${PGPASSWORD} \\\n\t\t--name comfy-db \\\n\t\t-p 5432:5432 \\\n\t\t-d postgres:9.6\n\tsleep 5s\n\tpsql -c \"CREATE DATABASE ${DATABASE}\"\n\tpsql -h localhost -U postgres -d comfy -f ./var/schema.sql\n\nstart-db:\n\tdocker start comfy-db\n\nstop-db:\n\tdocker stop comfy-db\n\nconnect-db:\n\tpsql comfy\n\nrun:\n\t$(SERVERLESS) offline start --host=0.0.0.0 --port=3000\n\ndeploy:\n\tNODE_ENV=production $(SERVERLESS) deploy --stage prod\n\nundeploy:\n\tNODE_ENV=production $(SERVERLESS) remove --stage prod\n\ntest:\n\tNODE_ENV=test ./node_modules/.bin/jest\n\ntest-watch:\n\tNODE_ENV=test ./node_modules/.bin/jest --watch\n"
  },
  {
    "path": "api/migrations/20170524101600-initialisation.js",
    "content": "/* eslint-disable */\n'use strict';\n\nexports.up = function(db, cb) {\n    db.runSql(`\n        CREATE EXTENSION IF NOT EXISTS pgcrypto;\n\n        DO $$\n        BEGIN\n            IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'state') THEN\n                CREATE TYPE state AS ENUM (\n                    'live',\n                    'archived'\n                );\n            END IF;\n            IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'format') THEN\n                CREATE TYPE format AS ENUM (\n                    'json',\n                    'yaml',\n                    'envvars'\n                );\n            END IF;\n\n            CREATE TABLE IF NOT EXISTS project (\n                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n                name TEXT NOT NULL,\n                state state NOT NULL DEFAULT 'live',\n                access_key varchar(20) NOT NULL UNIQUE,\n                read_token varchar(40) NOT NULL,\n                write_token varchar(40) NOT NULL,\n                created_at timestamp with time zone DEFAULT now() NOT NULL,\n                updated_at timestamp with time zone DEFAULT now() NOT NULL\n            );\n\n            CREATE TABLE IF NOT EXISTS environment (\n                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n                project_id UUID NOT NULL REFERENCES project ON DELETE CASCADE,\n                name TEXT NOT NULL,\n                state state NOT NULL DEFAULT 'live',\n                created_at timestamp with time zone DEFAULT now() NOT NULL,\n                updated_at timestamp with time zone DEFAULT now() NOT NULL,\n                UNIQUE(project_id, name)\n            );\n\n            CREATE TABLE IF NOT EXISTS configuration (\n                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n                environment_id UUID NOT NULL REFERENCES environment ON DELETE CASCADE,\n                name TEXT NOT NULL,\n                state state NOT NULL DEFAULT 'live',\n                default_format format,\n                created_at timestamp with time zone DEFAULT now() NOT NULL,\n                updated_at timestamp with time zone DEFAULT now() NOT NULL,\n                UNIQUE(environment_id, name)\n            );\n\n            CREATE TABLE IF NOT EXISTS version (\n                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n                configuration_id UUID NOT NULL REFERENCES configuration ON DELETE CASCADE,\n                hash varchar(64) NOT NULL,\n                previous varchar(64),\n                created_at timestamp with time zone DEFAULT now() NOT NULL,\n                UNIQUE(configuration_id, hash),\n                FOREIGN KEY (configuration_id, previous) REFERENCES version(configuration_id, hash)\n            );\n\n            CREATE TABLE IF NOT EXISTS entry (\n                version_id UUID NOT NULL REFERENCES version ON DELETE CASCADE,\n                key TEXT NOT NULL,\n                value TEXT,\n                UNIQUE(version_id, key)\n            );\n\n            CREATE TABLE IF NOT EXISTS tag (\n                configuration_id UUID NOT NULL REFERENCES configuration ON DELETE CASCADE,\n                version_id UUID NOT NULL REFERENCES version ON DELETE CASCADE,\n                name TEXT NOT NULL,\n                created_at timestamp with time zone DEFAULT now() NOT NULL,\n                updated_at timestamp with time zone DEFAULT now() NOT NULL,\n                UNIQUE(configuration_id, version_id, name)\n            );\n        END$$;\n    `, [], cb);\n};\n\nexports.down = function(db, cb) {\n    db.runSql(`\n        DROP TABLE tag;\n        DROP TABLE entry;\n        DROP TABLE version;\n        DROP TABLE configuration;\n        DROP TABLE environment;\n        DROP TABLE project;\n        DROP TYPE state;\n        DROP TYPE format;\n        DROP EXTENSION pgcrypto;\n    `, [], cb);\n};\n\nexports._meta = {\n    version: 1\n};\n"
  },
  {
    "path": "api/migrations/20170524124810-unique-tag.js",
    "content": "/* eslint-disable */\n'use strict';\n\nexports.up = function(db, cb) {\n    db.runSql(\n`DELETE FROM tag\n    USING version\n    WHERE version.id = tag.version_id AND EXISTS(\n        SELECT *\n        FROM tag t JOIN version v ON t.version_id = v.id\n        WHERE v.created_at > version.created_at AND t.name = tag.name AND t.configuration_id = tag.configuration_id\n    )\n    RETURNING *;\n\nALTER TABLE tag ADD CONSTRAINT unique_tag UNIQUE (configuration_id, name);`,\n    [], cb);\n};\n\nexports.down = function(db, cb) {\n    db.runSql(\n`\nALTER TABLE tag DROP CONSTRAINT unique_tag UNIQUE (configuration_id, name);\n`,\n    [], cb);\n};\n\nexports._meta = {\n  \"version\": 1\n};\n"
  },
  {
    "path": "api/migrations/20200325133153-add-token-table.js",
    "content": "/* eslint-disable */\n\"use strict\";\n\nexports.up = function (db, cb) {\n  db.runSql(\n    `\n    DO $$\n    BEGIN\n      CREATE TYPE token_level AS ENUM (\n        'read',\n        'write'\n      );\n\n      CREATE TABLE token (\n        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n        project_id UUID NOT NULL REFERENCES project ON DELETE CASCADE,\n        name TEXT NOT NULL,\n        level token_level NOT NULL,\n        key varchar(40) NOT NULL,\n        state state NOT NULL DEFAULT 'live',\n        expiry_date timestamp with time zone,\n        created_at timestamp with time zone DEFAULT now() NOT NULL,\n        updated_at timestamp with time zone DEFAULT now() NOT NULL,\n        UNIQUE(project_id, key),\n        UNIQUE(project_id, name)\n      );\n\n      INSERT INTO token (\"project_id\", \"name\", \"level\", \"key\", \"expiry_date\")\n      SELECT p.id, 'root', 'write', p.write_token, NULL\n      FROM project p;\n\n      INSERT INTO token (\"project_id\", \"name\", \"level\", \"key\", \"expiry_date\")\n      SELECT p.id, 'read_only', 'read', p.read_token, NULL\n      FROM project p;\n\n      ALTER TABLE project\n        DROP COLUMN write_token,\n        DROP COLUMN read_token;\n    END$$;\n  `,\n    [],\n    cb\n  );\n};\n\nexports.down = function (db, cb) {\n  db.runSql(\n    `\n    DO $$\n    BEGIN\n      ALTER TABLE project\n        ADD COLUMN write_token varchar(40),\n        ADD COLUMN read_token varchar(40);\n\n      UPDATE project\n      SET write_token = t.\"key\" \n      FROM token t\n      WHERE\n        t.project_id = project.id AND\n        t.\"level\" = 'write' AND\n        t.\"name\" = 'root';\n      \n      UPDATE project\n      SET read_token = t.\"key\" \n      FROM token t\n      WHERE\n        t.project_id = project.id AND\n        t.\"level\" = 'read' AND\n        t.\"name\" = 'read_only';\n\n      DROP TABLE token;\n      DROP TYPE token_level;\n    END$$;\n  `,\n    [],\n    cb\n  );\n};\n\nexports._meta = {\n  version: 1,\n};\n"
  },
  {
    "path": "api/now.json",
    "content": "{\n    \"name\": \"comfygure-api\",\n    \"version\": 2,\n    \"builds\": [\n        {\n            \"src\": \"build/index.js\",\n            \"use\": \"@now/node\"\n        }\n    ],\n    \"routes\": [{ \"src\": \".*\", \"dest\": \"/build\" }],\n    \"env\": {\n        \"PGHOST\": \"@comfy-pghost\",\n        \"PGDATABASE\": \"@comfy-pgdatabase\",\n        \"PGUSER\": \"@comfy-pguser\",\n        \"PGPASSWORD\": \"@comfy-pgpassword\"\n    }\n}\n"
  },
  {
    "path": "api/package.json",
    "content": "{\n    \"name\": \"comfygure\",\n    \"version\": \"1.2.0\",\n    \"license\": \"MIT\",\n    \"private\": true,\n    \"scripts\": {\n        \"start\": \"node build/launcher.js\",\n        \"dev\": \"node --require @babel/register src/launcher.js\",\n        \"build\": \"rm -rf build && babel src -d build\",\n        \"now-build\": \"rm -rf build && babel src -d build\"\n    },\n    \"dependencies\": {\n        \"body-parser\": \"^1.19.0\",\n        \"co\": \"~4.6.0\",\n        \"convict\": \"^5.2.0\",\n        \"date-fns\": \"^2.11.1\",\n        \"express\": \"^4.17.1\",\n        \"knex\": \"^0.20.13\",\n        \"lodash.omit\": \"^4.5.0\",\n        \"object-hash\": \"^2.0.3\",\n        \"pg\": \"^7.18.2\",\n        \"slug\": \"^2.1.1\"\n    },\n    \"devDependencies\": {\n        \"@babel/cli\": \"^7.8.4\",\n        \"@babel/core\": \"^7.9.0\",\n        \"@babel/plugin-transform-async-to-generator\": \"^7.8.3\",\n        \"@babel/preset-env\": \"^7.9.0\",\n        \"@babel/register\": \"^7.9.0\",\n        \"babel-eslint\": \"^10.1.0\",\n        \"babel-jest\": \"^25.2.6\",\n        \"babel-loader\": \"^8.1.0\",\n        \"jest\": \"^25.2.7\",\n        \"json-loader\": \"~0.5.4\",\n        \"request\": \"^2.88.2\",\n        \"serverless\": \"^1.67.0\",\n        \"serverless-offline\": \"^5.12.1\",\n        \"serverless-webpack\": \"^5.3.1\",\n        \"webpack\": \"^4.42.1\"\n    },\n    \"jest\": {\n        \"testEnvironment\": \"node\"\n    }\n}\n"
  },
  {
    "path": "api/serverless.yml",
    "content": "service: comfy\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: prod\n  region: eu-west-1\n\nfunctions:\n  ## PROJECTS\n\n  projectCreate:\n    handler: src/handlers/projects.create\n    description: Create a new project\n    events:\n      - http:\n          method: POST\n          path: projects\n          cors: true\n\n  projectUpdate:\n    handler: src/handlers/projects.update\n    description: Rename a project\n    events:\n      - http:\n          method: PUT\n          path: projects/{id}\n          cors: true\n\n  projectRemove:\n    handler: src/handlers/projects.remove\n    description: Delete a project\n    events:\n      - http:\n          method: DELETE\n          path: projects/{id}\n          cors: true\n\n  ## ENVIRONMENTS\n\n  environmentGet:\n    handler: src/handlers/environments.get\n    description: List environments of a project\n    events:\n      - http:\n          method: GET\n          path: projects/{id}/environments\n          cors: true\n\n  environmentCreate:\n    handler: src/handlers/environments.create\n    description: Add a new environment to a project\n    events:\n      - http:\n          method: POST\n          path: projects/{id}/environments\n          cors: true\n\n  environmentUpdate:\n    handler: src/handlers/environments.update\n    description: Rename environment of a project\n    events:\n      - http:\n          method: PUT\n          path: projects/{id}/environments/{environmentName}\n          cors: true\n\n  environmentRemove:\n    handler: src/handlers/environments.remove\n    description: Delete environment of a project\n    events:\n      - http:\n          method: DELETE\n          path: projects/{id}/environments/{environmentName}\n          cors: true\n\n  ## CONFIGURATIONS\n\n  configurationHistory:\n    handler: src/handlers/configurations.history\n    description: List history of a configuration\n    events:\n      - http:\n          method: GET\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/history\n          cors: true\n      - http:\n          method: GET\n          path: projects/{id}/environments/{environmentName}/configurations/history\n          cors: true\n\n  configurationGet:\n    handler: src/handlers/configurations.get\n    description: Get tag or hash version of a configuration\n    events:\n      - http:\n          method: GET\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}\n          cors: true\n      - http:\n          method: GET\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/{tagName}\n          cors: true\n\n  configurationAdd:\n    handler: src/handlers/configurations.create\n    description: Add a new version of a configuration\n    events:\n      - http:\n          method: POST\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/{tagName}\n          cors: true\n\n  configurationRemove:\n    handler: src/handlers/configurations.remove\n    description: Remove a configuration\n    events:\n      - http:\n          method: POST\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}\n          cors: true\n\n  ## TAGS\n\n  tagAdd:\n    handler: src/handlers/tags.create\n    description: Add a new tag on a configuration\n    events:\n      - http:\n          method: POST\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/tags\n          cors: true\n\n  tagUpdate:\n    handler: src/handlers/tags.update\n    description: Move a tag\n    events:\n      - http:\n          method: PUT\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/tags/{tagName}\n          cors: true\n\n  tagRemove:\n    handler: src/handlers/tags.remove\n    description: Remove a tag\n    events:\n      - http:\n          method: DELETE\n          path: projects/{id}/environments/{environmentName}/configurations/{configName}/tags/{tagName}\n          cors: true\n\n  ## TOKENS\n\n  tokenGet:\n    handler: src/handlers/tokens.get\n    description: List tokens of a project\n    events:\n      - http:\n          method: GET\n          path: projects/{id}/tokens\n          cors: true\n\n  tokenAdd:\n    handler: src/handlers/tokens.create\n    description: Add a new token\n    events:\n      - http:\n          method: POST\n          path: projects/{id}/tokens\n          cors: true\n\n  tokenRemove:\n    handler: src/handlers/tokens.remove\n    description: Remove a token\n    events:\n      - http:\n          method: DELETE\n          path: projects/{id}/tokens/{tokenId}\n          cors: true\nplugins:\n  - serverless-webpack\n  - serverless-offline\n\ncustom:\n  webpack:\n    webpackConfig: \"./webpack.config.babel.js\"\n    includeModules: true\n"
  },
  {
    "path": "api/src/config.js",
    "content": "import convict from \"convict\";\n\nconst config = convict({\n  port: {\n    doc: \"Default port for the comfy API (default : 80)\",\n    format: Number,\n    default: 80,\n    env: \"COMFY_API_PORT\",\n  },\n  logs: {\n    debug: {\n      doc: \"Log level debug (default: false)\",\n      format: Boolean,\n      default: false,\n      env: \"COMFY_LOG_DEBUG\",\n    },\n  },\n  db: {\n    client: {\n      host: {\n        doc: \"PostgreSQL host (default : localhost)\",\n        format: String,\n        default: \"localhost\",\n        env: \"PGHOST\",\n      },\n      port: {\n        doc: \"PostgreSQL port (default : 5432)\",\n        format: Number,\n        default: 5432,\n        env: \"PGPORT\",\n      },\n      database: {\n        doc: \"PostgreSQL database (default : 5432)\",\n        format: String,\n        default: \"comfy\",\n        env: \"PGDATABASE\",\n      },\n      user: {\n        doc: \"PostgreSQL user (default : postgres)\",\n        format: String,\n        default: \"postgres\",\n        env: \"PGUSER\",\n      },\n      password: {\n        doc: \"PostgreSQL password (default : '')\",\n        format: String,\n        default: \"\",\n        env: \"PGPASSWORD\",\n      },\n    },\n    pooling: {\n      min: {\n        doc: \"Minimum number of DB client in a pool (default : 0)\",\n        format: Number,\n        default: 0,\n        env: \"COMFY_DB_MIN_POOLING\",\n      },\n      max: {\n        doc: \"Maximum number of DB client in a pool (default : 2)\",\n        format: Number,\n        default: 2,\n        env: \"COMFY_DB_MAX_POOLING\",\n      },\n    },\n  },\n});\n\nconfig.validate({ allowed: \"strict\" });\n\nexport default config.getProperties();\n"
  },
  {
    "path": "api/src/domain/common/formats.js",
    "content": "const ENVVARS = \"envvars\";\nconst JSON = \"json\";\nconst YAML = \"yaml\";\n\nexport { ENVVARS, JSON, YAML };\n"
  },
  {
    "path": "api/src/domain/common/states.js",
    "content": "const LIVE = \"live\";\nconst ARCHIVED = \"archived\";\n\nexport { LIVE, ARCHIVED };\n"
  },
  {
    "path": "api/src/domain/configurations/__mocks__/tag.js",
    "content": "export const add = jest.fn(() => Promise.resolve({}));\n"
  },
  {
    "path": "api/src/domain/configurations/add.js",
    "content": "import hash from \"object-hash\";\n\nimport entriesQueries from \"../../queries/entries\";\nimport versionsQueries from \"../../queries/versions\";\nimport configurationsQueries from \"../../queries/configurations\";\nimport { ENVVARS } from \"../common/formats\";\n\nimport { get as getVersion } from \"./version\";\nimport { add as addTag, get as getTag, update as updateTag } from \"./tag\";\nimport { getProjectOr404 } from \"../projects\";\nimport { getEnvironmentOr404 } from \"../environments\";\n\nexport default async (\n  projectId,\n  environmentName,\n  configurationName = \"default\",\n  tagName = null,\n  entries = {},\n  format = null\n) => {\n  await getProjectOr404(projectId);\n  const environment = await getEnvironmentOr404(projectId, environmentName);\n\n  let configuration = await configurationsQueries.findOne(\n    projectId,\n    environmentName,\n    configurationName\n  );\n  let newlyCreated = false;\n\n  if (!configuration) {\n    configuration = await configurationsQueries.insertOne({\n      environment_id: environment.id,\n      name: configurationName,\n      default_format: format || ENVVARS,\n    });\n\n    newlyCreated = true;\n  }\n\n  if (!newlyCreated && configuration.default_format !== format) {\n    configuration = await configurationsQueries.updateOne(configuration.id, {\n      ...configuration,\n      default_format: format || ENVVARS,\n    });\n  }\n\n  const currentVersion = await getVersion(\n    projectId,\n    environmentName,\n    configurationName,\n    tagName\n  );\n\n  const versionHash = hash({\n    previous: currentVersion ? currentVersion.hash : null,\n    entries,\n  });\n  // TODO: If the version hash already exist in DB\n  // return a 304 to warn that the version already exists\n\n  const version = await versionsQueries.insertOne({\n    hash: versionHash,\n    configuration_id: configuration.id,\n    previous: currentVersion ? currentVersion.hash : null,\n  });\n\n  if (newlyCreated) {\n    await addTag(configuration.id, version.id, \"latest\");\n  }\n\n  // Create or update the specified tag\n  if (tagName) {\n    const tag = await getTag(configuration.id, tagName);\n\n    if (tag) {\n      await updateTag(tag, { version_id: version.id });\n    } else {\n      await addTag(configuration.id, version.id, tagName);\n    }\n  }\n\n  await Promise.all(\n    Object.keys(entries).map((key) =>\n      entriesQueries.insertOne({\n        key,\n        value: entries[key],\n        version_id: version.id,\n      })\n    )\n  );\n\n  const { id, name, default_format: defaultFormat } = configuration;\n\n  return {\n    id,\n    name,\n    defaultFormat,\n  };\n};\n"
  },
  {
    "path": "api/src/domain/configurations/get.js",
    "content": "import configurationsQueries from \"../../queries/configurations\";\nimport entriesQueries from \"../../queries/entries\";\nimport { NotFoundError } from \"../../domain/errors\";\n\nimport { get as getVersion } from \"./version\";\nimport { get as getTag } from \"./tag\";\n\nimport { checkEnvironmentExistsOrThrow404 } from \"../validation\";\n\nconst entriesToDictionary = (entries) =>\n  entries.reduce(\n    (dictionary, item) => ({\n      ...dictionary,\n      [item.key]: item.value,\n    }),\n    {}\n  );\n\nconst findAloneConfiguration = async (projectId, environmentName) => {\n  const configurations = await configurationsQueries.findAllByEnvironmentName(\n    projectId,\n    environmentName\n  );\n\n  if (configurations.length !== 1) {\n    // TODO: If configurations.length = 0, return a usable error\n    // TODO: If configurations.length > 1, return a usable error\n    throw new Error(\n      \"There is more than one configuration. Please select a configuration by its name.\"\n    );\n  }\n\n  return configurations[0];\n};\n\nexport default async (\n  projectId,\n  environmentName,\n  selector,\n  pathTagOrHashName\n) => {\n  // The `selector` argument can be a configName, a tag, or empty\n  // TODO (Kevin): If needed, move this selector intelligence into its own service\n\n  let configuration;\n  let tagOrHashName = pathTagOrHashName;\n\n  await checkEnvironmentExistsOrThrow404(projectId, environmentName);\n\n  if (selector && tagOrHashName) {\n    configuration = await configurationsQueries.findOne(\n      projectId,\n      environmentName,\n      selector\n    );\n  } else if (!selector) {\n    configuration = await findAloneConfiguration(projectId, environmentName);\n  } else {\n    configuration = await configurationsQueries.findOne(\n      projectId,\n      environmentName,\n      selector\n    );\n  }\n\n  if (!configuration) {\n    configuration = await findAloneConfiguration(projectId, environmentName);\n    tagOrHashName = pathTagOrHashName || selector;\n  }\n\n  let tag;\n  let version;\n  const defaultTag = \"latest\";\n  if (tagOrHashName) {\n    version = await getVersion(\n      projectId,\n      environmentName,\n      configuration.name,\n      tagOrHashName\n    );\n  } else {\n    tag = await getTag(configuration.id, defaultTag);\n    version = await getVersion(\n      projectId,\n      environmentName,\n      configuration.name,\n      tag ? tag.name : \"\"\n    );\n  }\n\n  if (!version) {\n    throw new NotFoundError(\n      `There is no tag or hash with this name: ${\n        tagOrHashName ? tagOrHashName : defaultTag\n      }.`\n    );\n  }\n\n  const entries = entriesToDictionary(\n    await entriesQueries.findByVersion(version.id)\n  );\n\n  return {\n    id: configuration.id,\n    name: configuration.name,\n    tag: tag ? tag.name : \"\",\n    hash: version.hash,\n    previous: version.previous,\n    defaultFormat: configuration.default_format,\n    body: entries,\n    state: configuration.state,\n  };\n};\n"
  },
  {
    "path": "api/src/domain/configurations/history.js",
    "content": "import getConfiguration from \"./get\";\nimport versionsQueries from \"../../queries/versions\";\n\nexport default async (projectId, environmentName, configName, all = false) => {\n  const configuration = await getConfiguration(\n    projectId,\n    environmentName,\n    configName\n  );\n\n  const versions = await versionsQueries.find(configuration.id);\n\n  return versions\n    .filter((version) => (all ? true : version.tags.length)) // TODO: Do this filter in SQL\n    .sort((a, b) => b.created_at - a.created_at)\n    .map((version) => ({\n      name: configuration.name,\n      hash: version.hash,\n      previous: version.previous,\n      tags: version.tags,\n      defaultFormat: configuration.default_format,\n      created_at: version.created_at,\n    }));\n};\n"
  },
  {
    "path": "api/src/domain/configurations/index.js",
    "content": "import add from \"./add\";\nimport get from \"./get\";\nimport history from \"./history\";\nimport update from \"./update\";\n\nexport default {\n  add,\n  get,\n  history,\n  update,\n};\n"
  },
  {
    "path": "api/src/domain/configurations/tag.js",
    "content": "import tagsQueries from \"../../queries/tags\";\n\nexport const add = async (configurationId, versionId, name) =>\n  tagsQueries.insertOne({\n    configuration_id: configurationId,\n    version_id: versionId,\n    name, // TODO: slufigy the tag name or throw if the format is invalid\n  });\n\nexport const get = async (configurationId, name) =>\n  tagsQueries.findOne(configurationId, name);\n\nexport const update = async (tag, attributes) =>\n  tagsQueries.updateOne(tag, attributes);\n"
  },
  {
    "path": "api/src/domain/configurations/update.js",
    "content": "import hash from \"object-hash\";\n\nimport entriesQueries from \"../../queries/entries\";\nimport versionsQueries from \"../../queries/versions\";\nimport configurationsQueries from \"../../queries/configurations\";\nimport tagsQueries from \"../../queries/tags\";\nimport { get as getVersion } from \"./version\";\n\nexport default async (configurationId, tagName = \"latest\") => {\n  const configuration = await configurationsQueries.findOne(configurationId);\n\n  if (!configuration) {\n    return null;\n  }\n\n  const lastVersion = await getVersion(configurationId, tagName);\n\n  const versionHash = hash({\n    previous: lastVersion.hash,\n    entries,\n  });\n\n  const version = await versionsQueries.insertOne({\n    hash: versionHash,\n    previous: lastVersion.hash,\n  });\n\n  await tagsQueries.updateOne(lastTag.id, {\n    version_id: version.id,\n  });\n\n  await Object.keys(entries).map((key) =>\n    entriesQueries.insertOne({\n      key,\n      value: entries[key],\n      version_id: version.id,\n    })\n  );\n};\n"
  },
  {
    "path": "api/src/domain/configurations/version.js",
    "content": "import configurationsQueries from \"../../queries/configurations\";\nimport versionsQueries from \"../../queries/versions\";\nimport tagsQueries from \"../../queries/tags\";\n\nexport const get = async (projectId, environmentName, configName, selector) => {\n  const configuration = await configurationsQueries.findOne(\n    projectId,\n    environmentName,\n    configName\n  );\n  const tag = await tagsQueries.findOne(configuration.id, selector);\n\n  let version;\n  if (!tag) {\n    version = await versionsQueries.findOneByHash(configuration.id, selector);\n  } else {\n    version = await versionsQueries.findOne(tag.version_id);\n  }\n\n  return version;\n};\n\nexport const getDefault = async (projectId, configName, tagName) => {\n  const configuration = await configurationsQueries.findOne(\n    projectId,\n    \"default\",\n    configName\n  );\n\n  let tag = await tagsQueries.findOne(configuration.id, tagName);\n  if (!tag) {\n    tag = await tagsQueries.findOne(configuration.id, \"latest\");\n  }\n\n  const version = await versionsQueries.findOneByTag(configuration.id, tag.id);\n  return version;\n};\n"
  },
  {
    "path": "api/src/domain/environments/__mocks__/get.js",
    "content": "export const getEnvironmentOr404 = jest.fn((id, name) =>\n  Promise.resolve({ name })\n);\n"
  },
  {
    "path": "api/src/domain/environments/add.js",
    "content": "import hash from \"object-hash\";\n\nimport environmentsQueries from \"../../queries/environments\";\nimport configurationsQueries from \"../../queries/configurations\";\nimport versionsQueries from \"../../queries/versions\";\nimport { add as addTag } from \"../configurations/tag\";\n\nimport { LIVE } from \"../common/states\";\nimport { ENVVARS } from \"../common/formats\";\n\nexport default async (projectId, environmentName, configName = \"default\") => {\n  // TODO (Kevin): Check if the environment already exists and return a usable error if it's the case\n  // TODO (Kevin): Factorize the code with domain/configurations/add\n\n  const environment = await environmentsQueries.insertOne({\n    name: environmentName,\n    project_id: projectId,\n    state: LIVE,\n  });\n\n  const configuration = await configurationsQueries.insertOne({\n    environment_id: environment.id,\n    name: configName,\n    default_format: ENVVARS,\n  });\n\n  const version = await versionsQueries.insertOne({\n    configuration_id: configuration.id,\n    hash: hash({ previous: null }),\n    previous: null,\n  });\n\n  await addTag(configuration.id, version.id, \"latest\");\n\n  return {\n    ...environment,\n    configurations: [configuration],\n  };\n};\n"
  },
  {
    "path": "api/src/domain/environments/add.spec.js",
    "content": "import add from \"./add\";\nimport { LIVE } from \"../common/states\";\n\nimport environmentsQueries from \"../../queries/environments\";\nimport configurationsQueries from \"../../queries/configurations\";\n\njest.mock(\"../../queries/environments\");\njest.mock(\"../../queries/configurations\");\njest.mock(\"../../queries/versions\");\njest.mock(\"../configurations/tag\");\n\ndescribe(\"domain/environments/add\", () => {\n  const projectId = 1;\n  const environmentName = \"production\";\n  const configurationName = \"frontend\";\n\n  it(\"should create an environment for the project\", async () => {\n    const environment = await add(projectId, environmentName);\n\n    expect(environmentsQueries.insertOne).toHaveBeenCalledWith({\n      name: environmentName,\n      project_id: projectId,\n      state: LIVE,\n    });\n\n    expect(environment).toBeTruthy();\n\n    expect(environment).toMatchObject({\n      id: 1,\n      name: environmentName,\n      project_id: projectId,\n      state: LIVE,\n    });\n  });\n\n  it(\"should create a configuration for the project and the environment\", async () => {\n    const environment = await add(\n      projectId,\n      environmentName,\n      configurationName\n    );\n\n    expect(configurationsQueries.insertOne).toHaveBeenCalledWith({\n      default_format: \"envvars\",\n      environment_id: 1,\n      name: \"frontend\",\n    });\n\n    expect(environment.configurations.length).toEqual(1);\n    expect(environment.configurations[0].name).toEqual(configurationName);\n  });\n\n  it(\"should create the configuration as `default` if no name is provided\", async () => {\n    const environment = await add(projectId, environmentName);\n\n    expect(configurationsQueries.insertOne).toHaveBeenCalledWith({\n      default_format: \"envvars\",\n      environment_id: 1,\n      name: \"default\",\n    });\n\n    expect(environment.configurations.length).toEqual(1);\n\n    expect(environment.configurations[0].name).toEqual(\"default\");\n  });\n\n  it(\"should return both environment and linked configurations\", async () => {\n    const environment = await add(\n      projectId,\n      environmentName,\n      configurationName\n    );\n\n    expect(environment).toMatchObject({\n      id: 1,\n      name: environmentName,\n      project_id: projectId,\n      state: LIVE,\n    });\n\n    expect(environment.configurations[0].name).toEqual(configurationName);\n  });\n});\n"
  },
  {
    "path": "api/src/domain/environments/get.js",
    "content": "import environmentsQueries from \"../../queries/environments\";\nimport { NotFoundError } from \"../errors\";\n\nexport const getEnvironmentOr404 = async (projectId, environmentName) => {\n  const env = await environmentsQueries.findOne(projectId, environmentName);\n\n  if (!env) {\n    throw new NotFoundError({\n      message: `Unable to find environment \"${environmentName}\" for project \"${projectId}\".`,\n      details: 'Type \"comfy env ls\" to list available environments.',\n    });\n  }\n\n  return env;\n};\n\nexport default async (projectId) =>\n  environmentsQueries.selectByProject(projectId);\n"
  },
  {
    "path": "api/src/domain/environments/get.spec.js",
    "content": "import get, { getEnvironmentOr404 } from \"./get\";\n\nimport environmentsQueries from \"../../queries/environments\";\n\njest.mock(\"../../queries/environments\");\n\ndescribe(\"domain/environments/get\", () => {\n  describe(\"get\", () => {\n    it(\"should call the query with the right arguments\", async () => {\n      const projectId = 1;\n\n      await get(projectId);\n\n      expect(environmentsQueries.selectByProject).toHaveBeenCalledWith(\n        projectId\n      );\n    });\n  });\n\n  describe(\"getEnvironmentOr404\", () => {\n    it(\"should retrieve an environment\", async () => {\n      const projectId = 42;\n      const environmentName = \"prod\";\n      const env = await getEnvironmentOr404(projectId, environmentName);\n\n      expect(environmentsQueries.findOne).toHaveBeenCalledWith(\n        projectId,\n        environmentName\n      );\n      expect(env).not.toBeUndefined();\n    });\n\n    it(\"should throw a NotFoundError if the env does not exist\", async () => {\n      const projectId = 42;\n      const environmentName = \"prod\";\n      environmentsQueries.findOne.mockImplementation(() =>\n        Promise.resolve(null)\n      );\n\n      try {\n        await getEnvironmentOr404(projectId, environmentName);\n      } catch (error) {\n        expect(error.name).toBe(\"NotFoundError\");\n        return;\n      }\n\n      expect(\"The function should throw an error\").toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "api/src/domain/environments/index.js",
    "content": "import add from \"./add\";\nimport get, { getEnvironmentOr404 } from \"./get\";\nimport remove from \"./remove\";\nimport rename from \"./rename\";\n\nexport { getEnvironmentOr404 };\n\nexport default {\n  add,\n  get,\n  getEnvironmentOr404,\n  remove,\n  rename,\n};\n"
  },
  {
    "path": "api/src/domain/environments/remove.js",
    "content": "import environmentsQueries from \"../../queries/environments\";\nimport { ARCHIVED } from \"../common/states\";\nimport { getProjectOr404 } from \"../projects/get\";\nimport { getEnvironmentOr404 } from \"./get\";\n\nexport default async (projectId, environmentName) => {\n  await getProjectOr404(projectId);\n  const environment = await getEnvironmentOr404(projectId, environmentName);\n\n  if (environment.state === ARCHIVED) {\n    return null;\n  }\n\n  return environmentsQueries.updateOne(environment.id, {\n    state: ARCHIVED,\n  });\n};\n"
  },
  {
    "path": "api/src/domain/environments/remove.spec.js",
    "content": "import remove from \"./remove\";\nimport { ARCHIVED } from \"../common/states\";\n\nimport environmentsQueries from \"../../queries/environments\";\nimport { getEnvironmentOr404 } from \"./get\";\n\njest.mock(\"../../queries/environments\");\njest.mock(\"./get\");\njest.mock(\"../projects/get\");\n\ndescribe(\"domain/environments/remove\", () => {\n  const projectId = 1;\n  const environmentName = \"staging\";\n\n  it(\"should try to find the environment by project id and name\", async () => {\n    await remove(projectId, environmentName);\n\n    expect(getEnvironmentOr404).toHaveBeenCalledWith(\n      projectId,\n      environmentName\n    );\n  });\n\n  it(\"should set `ARCHIVED` state on environment\", async () => {\n    await remove(projectId, environmentName);\n\n    expect(environmentsQueries.updateOne).toHaveBeenCalledWith(undefined, {\n      state: ARCHIVED,\n    });\n  });\n});\n"
  },
  {
    "path": "api/src/domain/environments/rename.js",
    "content": "import environmentsQueries from \"../../queries/environments\";\nimport { getProjectOr404 } from \"../projects/get\";\nimport { getEnvironmentOr404 } from \"./get\";\n\nexport default async (projectId, environmentName, newEnvironmentName) => {\n  await getProjectOr404(projectId);\n  const environment = await getEnvironmentOr404(projectId, environmentName);\n\n  if (!environment) {\n    return null;\n  }\n\n  return environmentsQueries.updateOne(environment.id, {\n    name: newEnvironmentName,\n  });\n};\n"
  },
  {
    "path": "api/src/domain/environments/rename.spec.js",
    "content": "import rename from \"./rename\";\n\nimport environmentsQueries from \"../../queries/environments\";\nimport { getEnvironmentOr404 } from \"./get\";\n\njest.mock(\"../../queries/environments\");\njest.mock(\"./get\");\njest.mock(\"../projects/get\");\n\ndescribe(\"domain/environments/rename\", () => {\n  const projectId = 1;\n  const environmentName = \"staging\";\n  const newEnvironmentName = \"integration\";\n\n  it(\"should try to find the environment by project id and name\", async () => {\n    await rename(projectId, environmentName, newEnvironmentName);\n\n    expect(getEnvironmentOr404).toHaveBeenCalledWith(\n      projectId,\n      environmentName\n    );\n  });\n\n  it(\"should change name on environment\", async () => {\n    await rename(projectId, environmentName, newEnvironmentName);\n\n    expect(environmentsQueries.updateOne).toHaveBeenCalledWith(undefined, {\n      name: newEnvironmentName,\n    });\n  });\n});\n"
  },
  {
    "path": "api/src/domain/errors.js",
    "content": "export class NotFoundError extends Error {\n  constructor(args) {\n    super(args);\n\n    if (typeof args === \"string\") {\n      this.message = args;\n    } else {\n      this.message = args.message;\n      this.details = args.details;\n    }\n\n    this.name = this.constructor.name;\n    if (typeof Error.captureStackTrace === \"function\") {\n      Error.captureStackTrace(this, this.constructor);\n    } else {\n      this.stack = new Error(this.message).stack;\n    }\n    this.stack = new Error().stack;\n  }\n}\n\nexport class ValidationError extends Error {\n  constructor(args) {\n    super(args);\n\n    if (typeof args === \"string\") {\n      this.message = args;\n    } else {\n      this.message = args.message;\n      this.details = args.details;\n    }\n\n    this.name = this.constructor.name;\n    if (typeof Error.captureStackTrace === \"function\") {\n      Error.captureStackTrace(this, this.constructor);\n    } else {\n      this.stack = new Error(this.message).stack;\n    }\n    this.stack = new Error().stack;\n  }\n}\n"
  },
  {
    "path": "api/src/domain/permissions.js",
    "content": "import tokenQueries from \"../queries/tokens\";\n\nexport const checkPermission = async (projectId, tokenKey, level) => {\n  const token = await tokenQueries.findValidTokenByKey(projectId, tokenKey);\n\n  if (!token) {\n    throw new Error(\"Project ID or token is invalid.\");\n  }\n\n  let permissionIsValid;\n\n  switch (level) {\n    case \"write\":\n      permissionIsValid = token.level === \"write\";\n      break;\n    case \"read\":\n      permissionIsValid = [\"write\", \"read\"].includes(token.level);\n      break;\n    default:\n      throw new Error(`Level \"${level}\" doesn't exists.`);\n  }\n\n  if (!permissionIsValid) {\n    throw new Error(\"Your token doesn't allow you to perform this action.\");\n  }\n};\n"
  },
  {
    "path": "api/src/domain/projects/__mocks__/get.js",
    "content": "export const getProjectOr404 = jest.fn((id) => Promise.resolve({ id }));\n"
  },
  {
    "path": "api/src/domain/projects/add.js",
    "content": "import { LIVE } from \"../common/states\";\nimport projectsQueries from \"../../queries/projects\";\nimport addEnvironment from \"../environments/add\";\nimport addToken from \"../tokens/add\";\nimport generateRandomString from \"../tokens/generateRandomString\";\n\nexport default async (\n  name,\n  environmentName = \"default\",\n  configurationName = \"default\"\n) => {\n  const project = await projectsQueries.insertOne({\n    name,\n    state: LIVE,\n    access_key: generateRandomString(20, true),\n  });\n\n  const environment = await addEnvironment(\n    project.id,\n    environmentName,\n    configurationName\n  );\n\n  const writeToken = await addToken(project.id, \"root\", \"write\");\n\n  return {\n    ...project,\n    environments: [environment],\n    tokens: [writeToken],\n    // Keep the following keys to not break retro-compatibility\n    writeToken: writeToken.key,\n    readToken: null,\n  };\n};\n"
  },
  {
    "path": "api/src/domain/projects/get.js",
    "content": "import projectsQueries from \"../../queries/projects\";\n\nimport { NotFoundError } from \"../errors\";\n\nexport const getProjectOr404 = async (projectId) => {\n  const project = await projectsQueries.findOne(projectId);\n\n  if (!project) {\n    throw new NotFoundError({\n      message: `Unable to find project \"${projectId}\"`,\n      details: [\n        \"Have you initialized a comfy project in thin directory?\",\n        'Type \"comfy init\" to do so.',\n      ].join(\" \"),\n    });\n  }\n\n  return project;\n};\n"
  },
  {
    "path": "api/src/domain/projects/get.spec.js",
    "content": "import { getProjectOr404 } from \"./get\";\n\nimport projectsQueries from \"../../queries/projects\";\n\njest.mock(\"../../queries/projects\");\n\ndescribe(\"domain/projects/get\", () => {\n  describe(\"getProjectOr404\", () => {\n    it(\"should retrieve an environment\", async () => {\n      const env = await getProjectOr404(42);\n\n      expect(projectsQueries.findOne).toHaveBeenCalledWith(42);\n      expect(env).not.toBeUndefined();\n    });\n\n    it(\"should throw a NotFoundError if the env does not exist\", async () => {\n      projectsQueries.findOne.mockImplementation(() => Promise.resolve(null));\n\n      try {\n        await getProjectOr404(42);\n      } catch (error) {\n        expect(error.name).toBe(\"NotFoundError\");\n        return;\n      }\n\n      expect(\"The function should throw an error\").toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "api/src/domain/projects/index.js",
    "content": "import add from \"./add\";\nimport { getProjectOr404 } from \"./get\";\nimport remove from \"./remove\";\nimport rename from \"./rename\";\n\nexport { getProjectOr404 };\n\nexport default {\n  add,\n  getProjectOr404,\n  remove,\n  rename,\n};\n"
  },
  {
    "path": "api/src/domain/projects/remove.js",
    "content": "import projectsQueries from \"../../queries/projects\";\nimport { ARCHIVED } from \"../common/states\";\n\nexport default async (id) =>\n  projectsQueries.updateOne(id, {\n    state: ARCHIVED,\n  });\n"
  },
  {
    "path": "api/src/domain/projects/rename.js",
    "content": "import projectQueries from \"../../queries/projects\";\n\nexport default async (id, name) =>\n  projectQueries.updateOne(id, {\n    name,\n  });\n"
  },
  {
    "path": "api/src/domain/tags/add.js",
    "content": "import { get as getVersion } from \"../configurations/version\";\nimport tagsQueries from \"../../queries/tags\";\nimport validateTag from \"./validator\";\n\nexport default async (\n  projectId,\n  environmentName,\n  configName,\n  selector,\n  name\n) => {\n  validateTag(name);\n\n  const version = await getVersion(\n    projectId,\n    environmentName,\n    configName,\n    selector\n  );\n  if (!version) {\n    throw new Error(`No configuration found for selector \"${selector}\"`);\n  }\n\n  return tagsQueries.insertOne({\n    configuration_id: version.configuration_id,\n    version_id: version.id,\n    name,\n  });\n};\n"
  },
  {
    "path": "api/src/domain/tags/move.js",
    "content": "import { get as getVersion } from \"../configurations/version\";\n\nimport tagsQueries from \"../../queries/tags\";\nimport configurationsQueries from \"../../queries/configurations\";\nimport validateTag from \"./validator\";\n\nexport default async (\n  projectId,\n  environmentName,\n  configurationName,\n  name,\n  selector\n) => {\n  validateTag(name);\n\n  const configuration = await configurationsQueries.findOne(\n    projectId,\n    environmentName,\n    configurationName\n  );\n  if (!configuration) {\n    throw new Error(`Configuration \"${configurationName}\" doesn't exist`);\n  }\n\n  const tag = await tagsQueries.findOne(configuration.id, name);\n  if (!tag) {\n    throw new Error(`Tag \"${name}\" doesn't exist`);\n  }\n\n  const newVersion = await getVersion(\n    projectId,\n    environmentName,\n    configurationName,\n    selector\n  );\n  if (!newVersion) {\n    throw new Error(`No version found for selector \"${selector}\"`);\n  }\n\n  return tagsQueries.updateOne(\n    {\n      configuration_id: tag.configuration_id,\n      version_id: tag.version_id,\n      name: tag.name,\n    },\n    {\n      version_id: newVersion.id,\n    }\n  );\n};\n"
  },
  {
    "path": "api/src/domain/tags/remove.js",
    "content": "import tagsQueries from \"../../queries/tags\";\nimport configurationsQueries from \"../../queries/configurations\";\nimport validateTag from \"./validator\";\n\nexport default async (projectId, environmentName, configurationName, name) => {\n  validateTag(name);\n\n  const configuration = await configurationsQueries.findOne(\n    projectId,\n    environmentName,\n    configurationName\n  );\n  if (!configuration) {\n    throw new Error(`Configuration \"${configurationName}\" doesn't exist`);\n  }\n\n  const tag = await tagsQueries.findOne(configuration.id, name);\n  if (!tag) {\n    throw new Error(`Tag \"${name}\" doesn't exist`);\n  }\n\n  return tagsQueries.removeOne(tag);\n};\n"
  },
  {
    "path": "api/src/domain/tags/validator.js",
    "content": "import slug from \"slug\";\n\nexport default (name) => {\n  if (slug(name) !== name) {\n    throw new Error(\n      `Tag name \"${name}\" is not valid. It should not contain whitespace or special character.`\n    );\n  }\n};\n"
  },
  {
    "path": "api/src/domain/tokens/add.js",
    "content": "import { addDays } from \"date-fns\";\nimport { ValidationError } from \"../errors\";\nimport tokensQueries from \"../../queries/tokens\";\nimport generateRandomString from \"./generateRandomString\";\n\nexport default async (projectId, name, level, expiresInDays) => {\n  const expiryDate = expiresInDays\n    ? addDays(new Date(), expiresInDays + 1)\n    : null;\n\n  try {\n    return await tokensQueries.insertOne({\n      project_id: projectId,\n      name,\n      level,\n      key: generateRandomString(40),\n      expiry_date: expiryDate,\n    });\n  } catch (error) {\n    if (error.message.includes(\"token_project_id_name_key\")) {\n      throw new ValidationError({\n        message: `A token named \"${name}\" already exists for that project`,\n        details: 'Type \"comfy token list\" to list available tokens.',\n      });\n    }\n\n    throw error;\n  }\n};\n"
  },
  {
    "path": "api/src/domain/tokens/generateRandomString.js",
    "content": "export default (size, upperAlphaOnly = false) => {\n  const numeric = \"0123456789\";\n  const lowerAlpha = \"abcdefghijklmnopqrstuvwxyz\";\n  const upperAlpha = lowerAlpha.toUpperCase();\n\n  const source = upperAlphaOnly\n    ? upperAlpha\n    : numeric + lowerAlpha + upperAlpha;\n\n  let randomlyGeneratedString = \"\";\n\n  while (randomlyGeneratedString.length < size) {\n    const randomIndex = Math.floor(Math.random() * (source.length - 1));\n    randomlyGeneratedString += source[randomIndex];\n  }\n\n  return randomlyGeneratedString;\n};\n"
  },
  {
    "path": "api/src/domain/tokens/get.js",
    "content": "import tokensQueries from \"../../queries/tokens\";\n\nexport default (projectId, all = false) =>\n  tokensQueries.findByProjectId(projectId, all);\n"
  },
  {
    "path": "api/src/domain/tokens/remove.js",
    "content": "import tokensQueries from \"../../queries/tokens\";\nimport { ARCHIVED } from \"../common/states\";\n\nexport default async (tokenId) => {\n  return tokensQueries.updateOne(tokenId, {\n    state: ARCHIVED,\n  });\n};\n"
  },
  {
    "path": "api/src/domain/validation.js",
    "content": "import { getProjectOr404 } from \"./projects/get\";\nimport { getEnvironmentOr404 } from \"./environments/get\";\n\nexport const checkEnvironmentExistsOrThrow404 = async (\n  projectId,\n  environmentName\n) => {\n  await getProjectOr404(projectId);\n  await getEnvironmentOr404(projectId, environmentName);\n};\n"
  },
  {
    "path": "api/src/handlers/configurations.js",
    "content": "import λ from \"./utils/λ\";\nimport {\n  checkAuthorizationOr403,\n  parseAuthorizationToken,\n} from \"./utils/authorization\";\n\nimport getConfiguration from \"../domain/configurations/get\";\nimport getHistory from \"../domain/configurations/history\";\nimport addConfiguration from \"../domain/configurations/add\";\n\nexport const create = λ(async (event) => {\n  const {\n    id: projectId,\n    environmentName,\n    configName,\n    tagName,\n  } = event.pathParameters;\n  const { entries, format } = event.body;\n\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return addConfiguration(\n    projectId,\n    environmentName,\n    configName,\n    tagName,\n    entries,\n    format\n  );\n});\n\nexport const get = λ(async (event) => {\n  const {\n    id: projectId,\n    environmentName,\n    configName: selector,\n    tagName: tagOrHashName,\n  } = event.pathParameters;\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"read\"\n  );\n\n  return getConfiguration(projectId, environmentName, selector, tagOrHashName);\n});\n\nexport const history = λ(async (event) => {\n  const { id: projectId, environmentName, configName } = event.pathParameters;\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"read\"\n  );\n  const all =\n    event.queryStringParameters &&\n    Object.keys(event.queryStringParameters).includes(\"all\");\n\n  return getHistory(projectId, environmentName, configName, all);\n});\n"
  },
  {
    "path": "api/src/handlers/environments.js",
    "content": "import λ from \"./utils/λ\";\nimport {\n  checkAuthorizationOr403,\n  parseAuthorizationToken,\n} from \"./utils/authorization\";\n\nimport addEnvironment from \"../domain/environments/add\";\nimport getEnvironments from \"../domain/environments/get\";\nimport renameEnvironment from \"../domain/environments/rename\";\nimport removeEnvironment from \"../domain/environments/remove\";\n\nexport const get = λ(async (event) => {\n  const { id: projectId } = event.pathParameters || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"read\"\n  );\n\n  return getEnvironments(projectId);\n});\n\nexport const create = λ(async (event) => {\n  const { id: projectId } = event.pathParameters || {};\n  const { name: environmentName } = event.body || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return addEnvironment(projectId, environmentName);\n});\n\nexport const update = λ(async (event) => {\n  const { id: projectId, environmentName } = event.pathParameters || {};\n  const { name: newEnvironmentName } = event.body || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return renameEnvironment(projectId, environmentName, newEnvironmentName);\n});\n\nexport const remove = λ(async (event) => {\n  const { id: projectId, environmentName } = event.pathParameters || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n  return removeEnvironment(projectId, environmentName);\n});\n"
  },
  {
    "path": "api/src/handlers/projects.js",
    "content": "import λ from \"./utils/λ\";\nimport {\n  checkAuthorizationOr403,\n  parseAuthorizationToken,\n} from \"./utils/authorization\";\n\nimport addProject from \"../domain/projects/add\";\nimport renameProject from \"../domain/projects/rename\";\nimport removeProject from \"../domain/projects/remove\";\n\nexport const create = λ(async (event) => {\n  const { name: projectName, environment: environmentName } = event.body || {};\n\n  const project = await addProject(projectName, environmentName);\n\n  return project;\n});\n\nexport const update = λ(async (event) => {\n  const { id: projectId } = event.pathParameters || {};\n  const { name: newProjectName } = event.body || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  const project = await renameProject(projectId, newProjectName);\n\n  return project;\n});\n\nexport const remove = λ(async (event) => {\n  const { id: projectId } = event.pathParameters || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return removeProject(projectId);\n});\n"
  },
  {
    "path": "api/src/handlers/tags.js",
    "content": "import λ from \"./utils/λ\";\nimport {\n  checkAuthorizationOr403,\n  parseAuthorizationToken,\n} from \"./utils/authorization\";\n\nimport addTag from \"../domain/tags/add\";\nimport moveTag from \"../domain/tags/move\";\nimport removeTag from \"../domain/tags/remove\";\n\nexport const create = λ(async (event) => {\n  const { id: projectId, environmentName, configName } = event.pathParameters;\n  const { selector, name } = event.body;\n\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  const tag = await addTag(\n    projectId,\n    environmentName,\n    configName,\n    selector,\n    name\n  );\n\n  return tag;\n});\n\nexport const update = λ(async (event) => {\n  const {\n    id: projectId,\n    environmentName,\n    configName,\n    tagName,\n  } = event.pathParameters;\n  const { selector } = event.body;\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  const project = await moveTag(\n    projectId,\n    environmentName,\n    configName,\n    tagName,\n    selector\n  );\n\n  return project;\n});\n\nexport const remove = λ(async (event) => {\n  const {\n    id: projectId,\n    environmentName,\n    configName,\n    tagName,\n  } = event.pathParameters;\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return removeTag(projectId, environmentName, configName, tagName);\n});\n"
  },
  {
    "path": "api/src/handlers/tokens.js",
    "content": "import λ from \"./utils/λ\";\nimport {\n  checkAuthorizationOr403,\n  parseAuthorizationToken,\n} from \"./utils/authorization\";\nimport getTokens from \"../domain/tokens/get\";\nimport addToken from \"../domain/tokens/add\";\nimport removeToken from \"../domain/tokens/remove\";\n\nexport const get = λ(async (event) => {\n  const { id: projectId } = event.pathParameters || {};\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"read\"\n  );\n\n  const all =\n    event.queryStringParameters &&\n    Object.keys(event.queryStringParameters).includes(\"all\");\n\n  return getTokens(projectId, all);\n});\n\nexport const create = λ(async (event) => {\n  const { id: projectId } = event.pathParameters;\n  const { name, level, expiresInDays } = event.body;\n\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return addToken(projectId, name, level, expiresInDays);\n});\n\nexport const remove = λ(async (event) => {\n  const { id: projectId, tokenId } = event.pathParameters;\n\n  await checkAuthorizationOr403(\n    parseAuthorizationToken(event),\n    projectId,\n    \"write\"\n  );\n\n  return removeToken(tokenId);\n});\n"
  },
  {
    "path": "api/src/handlers/utils/authorization.js",
    "content": "import { checkPermission } from \"../../domain/permissions\";\nimport { HttpError } from \"./errors\";\n\nexport const parseAuthorizationToken = (event) => {\n  const { Authorization: authorizationHeader } = event.headers || {};\n\n  if (!authorizationHeader) {\n    throw new HttpError(401, \"Authorization header should be set.\");\n  }\n\n  const [type, token] = authorizationHeader.split(\" \");\n\n  if (type !== \"Token\" || !token) {\n    throw new HttpError(403, \"Authorization header format is invalid.\");\n  }\n\n  return token;\n};\n\nexport const checkAuthorizationOr403 = async (token, projectId, level) => {\n  try {\n    await checkPermission(projectId, token, level);\n  } catch (e) {\n    throw new HttpError(403, e.message);\n  }\n};\n"
  },
  {
    "path": "api/src/handlers/utils/errors.js",
    "content": "import config from \"../../config\";\nimport { NotFoundError, ValidationError } from \"../../domain/errors\";\n\nexport class HttpError extends Error {\n  constructor(statusCode = 500, message = \"An error occured\", details = null) {\n    super(message);\n    this.message = message;\n    this.details = details;\n    this.statusCode = statusCode;\n    this.name = this.constructor.name;\n    if (typeof Error.captureStackTrace === \"function\") {\n      Error.captureStackTrace(this, this.constructor);\n    } else {\n      this.stack = new Error(message).stack;\n    }\n    this.stack = new Error().stack;\n  }\n}\n\nexport const convertErrorToHttpError = (error) => {\n  if (error instanceof HttpError) {\n    return error;\n  }\n\n  if (error instanceof NotFoundError) {\n    return new HttpError(404, error.message, error.details);\n  }\n\n  if (error instanceof ValidationError) {\n    return new HttpError(400, error.message, error.details);\n  }\n\n  return new HttpError(\n    500,\n    config.logs.debug ? error.message : \"An error occured\",\n    config.logs.debug ? error.details : \"Please contact an administrator\"\n  );\n};\n"
  },
  {
    "path": "api/src/handlers/utils/λ.js",
    "content": "import co from \"co\";\nimport logger from \"../../logger\";\nimport config from \"../../config\";\n\nimport { convertErrorToHttpError } from \"./errors\";\n\nexport default (handler) => {\n  if (!!process.env.SERVERLESS) {\n    return (event, context) => {\n      co(function* () {\n        const body = yield handler({\n          ...event,\n          body: event.body ? JSON.parse(event.body) : null,\n        });\n\n        context.succeed({\n          statusCode: 200,\n          body: JSON.stringify(body),\n        });\n      }).catch((error) => {\n        logger.error(\"ERROR\", error.message);\n        logger.error(\"ERR. STACK\", error.stack);\n\n        const httpError = convertErrorToHttpError(error);\n\n        context.succeed({\n          statusCode: httpError.statusCode || 500,\n          body: JSON.stringify({\n            error: config.logs.debug ? error : null,\n            message: httpError.message,\n            details: httpError.details,\n          }),\n        });\n      });\n    };\n  }\n\n  return async (event) => handler(event);\n};\n"
  },
  {
    "path": "api/src/index.js",
    "content": "import express from \"express\";\nimport bodyParser from \"body-parser\";\n\nimport config from \"./config\";\nimport logger from \"./logger\";\nimport { convertErrorToHttpError } from \"./handlers/utils/errors\";\n\nimport {\n  create as createProject,\n  update as updateProject,\n  remove as removeProject,\n} from \"./handlers/projects\";\n\nimport {\n  get as getEnvironments,\n  create as createEnvironment,\n  update as updateEnvironment,\n  remove as removeEnvironment,\n} from \"./handlers/environments\";\n\nimport {\n  history as getConfigurationHistory,\n  get as getConfiguration,\n  create as createConfiguration,\n  remove as removeConfiguration,\n} from \"./handlers/configurations\";\n\nimport {\n  create as createTag,\n  update as updateTag,\n  remove as removeTag,\n} from \"./handlers/tags\";\n\nconst app = express();\n\nconst handlerToMiddleware = (handler) => async (req, res) => {\n  const event = {\n    pathParameters: req.params,\n    body: req.body,\n    headers: Object.assign({}, req.headers, {\n      Authorization: req.headers.authorization,\n    }),\n  };\n\n  try {\n    const body = await handler(event);\n\n    res.send(body);\n  } catch (error) {\n    logger.error(\"ERROR\", error.message);\n    logger.error(\"ERR. STACK\", error.stack);\n\n    const httpError = convertErrorToHttpError(error);\n\n    res.status(httpError.statusCode || 500).send({\n      error: config.logs.debug ? error : null,\n      message: httpError.message,\n      details: httpError.details,\n    });\n  }\n};\n\napp.use(bodyParser.json());\n\napp.post(\"/projects\", handlerToMiddleware(createProject));\napp.put(\"/projects/:id\", handlerToMiddleware(updateProject));\napp.delete(\"/projects/:id\", handlerToMiddleware(removeProject));\n\napp.get(\"/projects/:id/environments\", handlerToMiddleware(getEnvironments));\napp.post(\"/projects/:id/environments\", handlerToMiddleware(createEnvironment));\napp.put(\n  \"/projects/:id/environments/:environmentName\",\n  handlerToMiddleware(updateEnvironment)\n);\napp.delete(\n  \"/projects/:id/environments/:environmentName\",\n  handlerToMiddleware(removeEnvironment)\n);\n\napp.get(\n  \"/projects/:id/environments/:environmentName/configurations/history\",\n  handlerToMiddleware(getConfigurationHistory)\n);\napp.get(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/history\",\n  handlerToMiddleware(getConfigurationHistory)\n);\napp.get(\n  \"/projects/:id/environments/:environmentName/configurations/:configName\",\n  handlerToMiddleware(getConfiguration)\n);\napp.get(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/:tagName\",\n  handlerToMiddleware(getConfiguration)\n);\napp.post(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/:tagName\",\n  handlerToMiddleware(createConfiguration)\n);\napp.delete(\n  \"/projects/:id/environments/:environmentName/configurations/:configName\",\n  handlerToMiddleware(removeConfiguration)\n);\n\napp.post(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/tags\",\n  handlerToMiddleware(createTag)\n);\napp.put(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/tags/:tagName\",\n  handlerToMiddleware(updateTag)\n);\napp.delete(\n  \"/projects/:id/environments/:environmentName/configurations/:configName/tags/:tagName\",\n  handlerToMiddleware(removeTag)\n);\n\nexport default app;\n"
  },
  {
    "path": "api/src/launcher.js",
    "content": "import app from \"./index\";\nimport config from \"./config\";\n\napp.listen(config.port, () =>\n  console.log(`Comfygure API listening on port ${config.port}.`)\n);\n"
  },
  {
    "path": "api/src/logger.js",
    "content": "/* eslint-disable no-console */\n\nexport default {\n  log: console.log,\n  info: console.info,\n  error: console.error,\n};\n"
  },
  {
    "path": "api/src/mocks.js",
    "content": "import mockQueries from \"./queries/mocks\";\nimport mockDomain from \"./domain/mocks\";\n\nconst restore = () => {\n  mockQueries.restore();\n  mockDomain.restore();\n};\n\nexport default {\n  queries: mockQueries,\n  domain: mockDomain,\n  restore,\n};\n"
  },
  {
    "path": "api/src/queries/__mocks__/configurations.js",
    "content": "const insertOne = jest.fn((entity) => Promise.resolve({ id: 1, ...entity }));\nconst findOne = jest.fn(() => Promise.resolve({}));\n\nexport default {\n  insertOne,\n  findOne,\n};\n"
  },
  {
    "path": "api/src/queries/__mocks__/environments.js",
    "content": "const insertOne = jest.fn((entity) => Promise.resolve({ id: 1, ...entity }));\nconst updateOne = jest.fn((entity) => Promise.resolve(entity));\nconst findOne = jest.fn((id) => Promise.resolve({ id }));\nconst selectByProject = jest.fn(() => Promise.resolve([]));\n\nexport default {\n  insertOne,\n  updateOne,\n  findOne,\n  selectByProject,\n};\n"
  },
  {
    "path": "api/src/queries/__mocks__/projects.js",
    "content": "const findOne = jest.fn((id) => Promise.resolve({ id }));\n\nexport default {\n  findOne,\n};\n"
  },
  {
    "path": "api/src/queries/__mocks__/versions.js",
    "content": "const insertOne = jest.fn((entity) => Promise.resolve({ id: 1, ...entity }));\nconst find = jest.fn(() => Promise.resolve([]));\nconst findOneByHash = jest.fn(() => Promise.resolve({}));\nconst findOneByTag = jest.fn(() => Promise.resolve({}));\n\nexport default {\n  insertOne,\n  find,\n  findOneByHash,\n  findOneByTag,\n};\n"
  },
  {
    "path": "api/src/queries/configurations.js",
    "content": "import omit from \"lodash.omit\";\nimport client, {\n  insertOne as insertOneQuery,\n  updateOne as updateOneQuery,\n} from \"./knex\";\n\nconst table = \"configuration\";\nconst fields = [\n  \"configuration.id\",\n  \"configuration.name\",\n  \"configuration.state\",\n  \"configuration.environment_id\",\n  \"configuration.default_format\",\n  \"configuration.created_at\",\n  \"configuration.updated_at\",\n  \"environment.name as environment_name\",\n  \"environment.project_id as project_id\",\n];\n\nconst findOne = async (projectId, environmentName, configurationName) =>\n  client\n    .select(fields)\n    .from(table)\n    .innerJoin(\"environment\", \"environment.id\", \"configuration.environment_id\")\n    .where({\n      project_id: projectId,\n      \"environment.name\": environmentName,\n      \"configuration.name\": configurationName,\n      \"configuration.state\": \"live\",\n    })\n    .first();\n\nconst findAllByEnvironmentName = async (projectId, environmentName) =>\n  client\n    .select(fields)\n    .from(table)\n    .innerJoin(\"environment\", \"environment.id\", \"configuration.environment_id\")\n    .where({\n      project_id: projectId,\n      \"environment.name\": environmentName,\n      \"configuration.state\": \"live\",\n    });\n\nconst insertOne = async (data) => {\n  const { id } = await insertOneQuery(table, [\"id\"])(data);\n\n  return client\n    .select(fields)\n    .from(table)\n    .innerJoin(\"environment\", \"environment.id\", \"configuration.environment_id\")\n    .where({\n      \"configuration.id\": id,\n    })\n    .first();\n};\n\nconst updateOne = async (id, data) => {\n  await updateOneQuery(table, [\"id\"])(\n    id,\n    omit(data, [\"project_id\", \"environment_name\"])\n  );\n\n  return client\n    .select(fields)\n    .from(table)\n    .innerJoin(\"environment\", \"environment.id\", \"configuration.environment_id\")\n    .where({\n      \"configuration.id\": id,\n    })\n    .first();\n};\n\nexport default {\n  findOne,\n  findAllByEnvironmentName,\n  insertOne,\n  updateOne,\n};\n"
  },
  {
    "path": "api/src/queries/entries.js",
    "content": "import client, { insertOne } from \"./knex\";\n\nconst table = \"entry\";\nconst fields = [\"version_id\", \"key\", \"value\"];\n\nconst findByVersion = async (version_id) =>\n  client.select(fields).from(table).where({ version_id });\n\nexport default {\n  insertOne: insertOne(table, fields),\n  findByVersion,\n};\n"
  },
  {
    "path": "api/src/queries/environments.js",
    "content": "import client, { insertOne, updateOne } from \"./knex\";\n\nconst table = \"environment\";\nconst fields = [\"id\", \"name\"];\n\nconst findOne = async (projectId, environmentName) =>\n  client\n    .select(fields)\n    .from(table)\n    .where({\n      project_id: projectId,\n      state: \"live\",\n      name: environmentName,\n    })\n    .first();\n\nconst selectByProject = async (projectId) => {\n  const environments = await client.select(fields).from(table).where({\n    project_id: projectId,\n    state: \"live\",\n  });\n\n  return environments;\n};\n\nexport default {\n  insertOne: insertOne(table, fields),\n  updateOne: updateOne(table, fields),\n  findOne,\n  selectByProject,\n};\n"
  },
  {
    "path": "api/src/queries/knex.js",
    "content": "import knex from \"knex\";\n\nimport config from \"../config\";\n\nconst client = knex({\n  client: \"pg\",\n  connection: config.db.client,\n  pool: config.db.pooling,\n  debug: false // Toggle this variable to log SQL queries\n});\n\nexport const findOne = (table, fields, primaryKey = \"id\") => async identifier =>\n  client\n    .select(fields)\n    .from(table)\n    .where({ [primaryKey]: identifier })\n    .first();\n\nexport const insertOne = (table, fields) => async row => {\n  const results = await client(table)\n    .insert(row)\n    .returning(fields);\n\n  return results[0]; // Cannot chain .first() on \"insert\" query\n};\n\nexport const updateOne = (table, fields, primaryKey = \"id\") => async (\n  identifier,\n  row\n) => {\n  const results = await client(table)\n    .where({ [primaryKey]: identifier })\n    .update(row)\n    .returning(fields);\n\n  return results[0]; // Cannot chain .first() on \"update\" query\n};\n\nexport default client;\n"
  },
  {
    "path": "api/src/queries/projects.js",
    "content": "import { findOne, insertOne, updateOne } from \"./knex\";\n\nconst table = \"project\";\n\nconst fields = [\"id\", \"name\", \"access_key as accessKey\"];\n\nexport default {\n  findOne: findOne(table, fields),\n  insertOne: insertOne(table, fields),\n  updateOne: updateOne(table, fields)\n};\n"
  },
  {
    "path": "api/src/queries/tags.js",
    "content": "import client, { insertOne } from \"./knex\";\n\nconst table = \"tag\";\nconst fields = [\"configuration_id\", \"version_id\", \"name\"];\n\nconst updateOne = async (tag, { version_id: newVersionId }) => {\n  const results = await client(table)\n    .where(tag)\n    .update({ version_id: newVersionId })\n    .returning(fields);\n\n  return results[0]; // Cannot chain .first() on \"update\" query\n};\n\nconst removeOne = async (tag) => {\n  const results = await client(table).where(tag).del().returning(fields);\n\n  return results[0]; // Cannot chain .first() on \"del\" query\n};\n\nconst batchInsert = async (tags) =>\n  client(table).insert(tags).returning(fields);\n\nconst findOne = async (configurationId, tagName) =>\n  client\n    .select(fields)\n    .from(table)\n    .where({ configuration_id: configurationId, name: tagName })\n    .first();\n\nexport default {\n  updateOne,\n  insertOne: insertOne(table, fields),\n  removeOne,\n  batchInsert,\n  findOne,\n};\n"
  },
  {
    "path": "api/src/queries/tokens.js",
    "content": "import client, { insertOne, updateOne } from \"./knex\";\nimport { LIVE } from \"../domain/common/states\";\n\nconst table = \"token\";\n\n// token.key should not appear in this default list\nconst fields = [\n  \"id\",\n  \"project_id\",\n  \"name\",\n  \"level\",\n  \"expiry_date\",\n  \"created_at\",\n  \"updated_at\",\n];\n\nconst findByProjectId = async (project_id, all = false) => {\n  return client\n    .select(fields)\n    .from(table)\n    .where({\n      project_id,\n      state: LIVE,\n    })\n    .andWhere(function () {\n      if (all) {\n        return this;\n      }\n\n      return this.whereRaw(\"expiry_date > NOW()\").orWhere({\n        expiry_date: null,\n      });\n    })\n    .orderBy(\"created_at\");\n};\n\nconst findValidTokenByKey = async (project_id, key) =>\n  client\n    .select(fields)\n    .from(table)\n    .where({\n      key,\n      project_id,\n      state: LIVE,\n    })\n    .andWhere(function () {\n      return this.whereRaw(\"expiry_date > NOW()\").orWhere({\n        expiry_date: null,\n      });\n    })\n    .first();\n\nexport default {\n  findByProjectId,\n  findValidTokenByKey,\n  insertOne: insertOne(table, [...fields, \"key\"]),\n  updateOne: updateOne(table, fields),\n};\n"
  },
  {
    "path": "api/src/queries/versions.js",
    "content": "import { raw } from \"knex\";\nimport client, { insertOne } from \"./knex\";\n\nconst table = \"version\";\nconst fields = [\"id\", \"configuration_id\", \"hash\", \"previous\", \"created_at\"];\n\nconst prefix = (pre) => (str) => `${pre}.${str}`;\n\nconst selectVersions = async (whereConditions, single = true) => {\n  const versions = await client\n    .select([\n      ...fields.map(prefix(table)),\n      raw(\n        \"case when count(tag.name) = 0 then '[]' else json_agg(tag.name) end as tags\"\n      ),\n    ])\n    .leftJoin(\"tag\", \"tag.version_id\", \"version.id\")\n    .from(table)\n    .where(whereConditions)\n    .groupBy(fields.map(prefix(table)));\n\n  if (single) {\n    return versions[0];\n  }\n\n  return versions;\n};\n\nconst findOne = async (id) => selectVersions({ \"version.id\": id });\n\nconst find = async (configurationId) =>\n  selectVersions(\n    {\n      \"version.configuration_id\": configurationId,\n    },\n    false\n  );\n\nconst findOneByHash = async (configurationId, hash) =>\n  selectVersions({\n    \"version.configuration_id\": configurationId,\n    \"version.hash\": hash,\n  });\n\nconst findOneByTag = async (configurationId, tagId) =>\n  selectVersions({\n    \"version.configuration_id\": configurationId,\n    \"tag.id\": tagId,\n  });\n\nexport default {\n  findOne,\n  insertOne: insertOne(table, fields),\n  find,\n  findOneByHash,\n  findOneByTag,\n};\n"
  },
  {
    "path": "api/var/schema.sql",
    "content": "--\n-- PostgreSQL database dump\n--\n\n-- Dumped from database version 9.6.17\n-- Dumped by pg_dump version 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)\n\nSET statement_timeout = 0;\nSET lock_timeout = 0;\nSET idle_in_transaction_session_timeout = 0;\nSET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSELECT pg_catalog.set_config('search_path', '', false);\nSET check_function_bodies = false;\nSET xmloption = content;\nSET client_min_messages = warning;\nSET row_security = off;\n\n--\n-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -\n--\n\nCREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;\n\n\n--\n-- Name: format; Type: TYPE; Schema: public; Owner: -\n--\n\nCREATE TYPE public.format AS ENUM (\n    'json',\n    'yaml',\n    'envvars'\n);\n\n\n--\n-- Name: state; Type: TYPE; Schema: public; Owner: -\n--\n\nCREATE TYPE public.state AS ENUM (\n    'live',\n    'archived'\n);\n\n\n--\n-- Name: token_level; Type: TYPE; Schema: public; Owner: -\n--\n\nCREATE TYPE public.token_level AS ENUM (\n    'read',\n    'write'\n);\n\n\nSET default_tablespace = '';\n\nSET default_with_oids = false;\n\n--\n-- Name: configuration; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.configuration (\n    id uuid DEFAULT public.gen_random_uuid() NOT NULL,\n    environment_id uuid NOT NULL,\n    name text NOT NULL,\n    state public.state DEFAULT 'live'::public.state NOT NULL,\n    default_format public.format,\n    created_at timestamp with time zone DEFAULT now() NOT NULL,\n    updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: entry; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.entry (\n    version_id uuid NOT NULL,\n    key text NOT NULL,\n    value text\n);\n\n\n--\n-- Name: environment; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.environment (\n    id uuid DEFAULT public.gen_random_uuid() NOT NULL,\n    project_id uuid NOT NULL,\n    name text NOT NULL,\n    state public.state DEFAULT 'live'::public.state NOT NULL,\n    created_at timestamp with time zone DEFAULT now() NOT NULL,\n    updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: project; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.project (\n    id uuid DEFAULT public.gen_random_uuid() NOT NULL,\n    name text NOT NULL,\n    state public.state DEFAULT 'live'::public.state NOT NULL,\n    access_key character varying(20) NOT NULL,\n    created_at timestamp with time zone DEFAULT now() NOT NULL,\n    updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: tag; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.tag (\n    configuration_id uuid NOT NULL,\n    version_id uuid NOT NULL,\n    name text NOT NULL,\n    created_at timestamp with time zone DEFAULT now() NOT NULL,\n    updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: token; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.token (\n    id uuid DEFAULT public.gen_random_uuid() NOT NULL,\n    project_id uuid NOT NULL,\n    name text NOT NULL,\n    level public.token_level NOT NULL,\n    key character varying(40) NOT NULL,\n    state public.state DEFAULT 'live'::public.state NOT NULL,\n    expiry_date timestamp with time zone,\n    created_at timestamp with time zone DEFAULT now() NOT NULL,\n    updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: version; Type: TABLE; Schema: public; Owner: -\n--\n\nCREATE TABLE public.version (\n    id uuid DEFAULT public.gen_random_uuid() NOT NULL,\n    configuration_id uuid NOT NULL,\n    hash character varying(64) NOT NULL,\n    previous character varying(64),\n    created_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n--\n-- Name: configuration configuration_environment_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.configuration\n    ADD CONSTRAINT configuration_environment_id_name_key UNIQUE (environment_id, name);\n\n\n--\n-- Name: configuration configuration_pkey; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.configuration\n    ADD CONSTRAINT configuration_pkey PRIMARY KEY (id);\n\n\n--\n-- Name: entry entry_version_id_key_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.entry\n    ADD CONSTRAINT entry_version_id_key_key UNIQUE (version_id, key);\n\n\n--\n-- Name: environment environment_pkey; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.environment\n    ADD CONSTRAINT environment_pkey PRIMARY KEY (id);\n\n\n--\n-- Name: environment environment_project_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.environment\n    ADD CONSTRAINT environment_project_id_name_key UNIQUE (project_id, name);\n\n\n--\n-- Name: project project_access_key_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.project\n    ADD CONSTRAINT project_access_key_key UNIQUE (access_key);\n\n\n--\n-- Name: project project_pkey; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.project\n    ADD CONSTRAINT project_pkey PRIMARY KEY (id);\n\n\n--\n-- Name: tag tag_configuration_id_version_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.tag\n    ADD CONSTRAINT tag_configuration_id_version_id_name_key UNIQUE (configuration_id, version_id, name);\n\n\n--\n-- Name: token token_pkey; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.token\n    ADD CONSTRAINT token_pkey PRIMARY KEY (id);\n\n\n--\n-- Name: token token_project_id_key_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.token\n    ADD CONSTRAINT token_project_id_key_key UNIQUE (project_id, key);\n\n--\n-- Name: token token_project_id_name_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.token\n    ADD CONSTRAINT token_project_id_name_key UNIQUE (project_id, name);\n\n--\n-- Name: tag unique_tag; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.tag\n    ADD CONSTRAINT unique_tag UNIQUE (configuration_id, name);\n\n\n--\n-- Name: version version_configuration_id_hash_key; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.version\n    ADD CONSTRAINT version_configuration_id_hash_key UNIQUE (configuration_id, hash);\n\n\n--\n-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.version\n    ADD CONSTRAINT version_pkey PRIMARY KEY (id);\n\n\n--\n-- Name: configuration configuration_environment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.configuration\n    ADD CONSTRAINT configuration_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environment(id) ON DELETE CASCADE;\n\n\n--\n-- Name: entry entry_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.entry\n    ADD CONSTRAINT entry_version_id_fkey FOREIGN KEY (version_id) REFERENCES public.version(id) ON DELETE CASCADE;\n\n\n--\n-- Name: environment environment_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.environment\n    ADD CONSTRAINT environment_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.project(id) ON DELETE CASCADE;\n\n\n--\n-- Name: tag tag_configuration_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.tag\n    ADD CONSTRAINT tag_configuration_id_fkey FOREIGN KEY (configuration_id) REFERENCES public.configuration(id) ON DELETE CASCADE;\n\n\n--\n-- Name: tag tag_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.tag\n    ADD CONSTRAINT tag_version_id_fkey FOREIGN KEY (version_id) REFERENCES public.version(id) ON DELETE CASCADE;\n\n\n--\n-- Name: token token_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.token\n    ADD CONSTRAINT token_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.project(id) ON DELETE CASCADE;\n\n\n--\n-- Name: version version_configuration_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.version\n    ADD CONSTRAINT version_configuration_id_fkey FOREIGN KEY (configuration_id) REFERENCES public.configuration(id) ON DELETE CASCADE;\n\n\n--\n-- Name: version version_configuration_id_fkey1; Type: FK CONSTRAINT; Schema: public; Owner: -\n--\n\nALTER TABLE ONLY public.version\n    ADD CONSTRAINT version_configuration_id_fkey1 FOREIGN KEY (configuration_id, previous) REFERENCES public.version(configuration_id, hash);\n\n\n--\n-- PostgreSQL database dump complete\n--\n"
  },
  {
    "path": "api/webpack.config.babel.js",
    "content": "const path = require(\"path\");\nconst webpack = require(\"webpack\");\nconst slsw = require(\"serverless-webpack\");\n\nmodule.exports = {\n  mode: slsw.lib.webpack.isLocal ? \"development\" : \"production\",\n  target: \"node\",\n  entry: slsw.lib.entries,\n  output: {\n    libraryTarget: \"commonjs2\",\n    path: path.resolve(__dirname, \".webpack\"),\n    filename: \"[name].js\"\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        use: \"babel-loader\",\n        exclude: /node_modules/\n      }\n    ],\n    noParse: [\n      /pg\\/lib\\/native/,\n      /knex\\/lib\\/dialects\\/(mssql|mysql|mysql2|sqlite3|oracledb)/\n    ]\n  },\n  externals: [\"aws-sdk\", \"pg-query-stream\"],\n  plugins: [\n    new webpack.DefinePlugin({\n      \"process.env.SERVERLESS\": JSON.stringify(true)\n    })\n  ],\n  optimization: {\n    minimize: false\n  },\n  performance: {\n    hints: false\n  }\n};\n"
  },
  {
    "path": "cli/.eslintrc",
    "content": "{\n    \"env\": {\n        \"jest\": true,\n        \"node\": true,\n        \"es6\": true\n    },\n    \"plugins\": [\"prettier\"],\n    \"extends\": [\"eslint:recommended\", \"prettier\"],\n    \"parserOptions\": {\n        \"ecmaVersion\": 2017\n    },\n    \"rules\": {\n        \"prettier/prettier\": [\n            \"error\",\n            {\n                \"singleQuote\": true,\n                \"tabWidth\": 4,\n                \"trailingComma\": \"es5\",\n                \"printWidth\": 120\n            }\n        ],\n        \"no-unused-vars\": [\n            \"error\",\n            {\n                \"ignoreRestSiblings\": true\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "cli/.npmignore",
    "content": "Makefile\n*.spec.js\n"
  },
  {
    "path": "cli/Makefile",
    "content": "install:\n\tnpm install\n\ntest-unit:\n\t./node_modules/.bin/jest\n\ntest-unit-watch:\n\t./node_modules/.bin/jest --watch\n\ntest: test-unit\n"
  },
  {
    "path": "cli/README.md",
    "content": "[![npm version](https://badge.fury.io/js/comfygure.svg)](https://badge.fury.io/js/comfygure) ![CLI dependencies](https://img.shields.io/david/marmelab/comfygure.svg?label=CLI%20dependencies&path=cli) ![API dependencies](https://img.shields.io/david/marmelab/comfygure.svg?label=API%20dependencies&path=api) [![npm downloads](https://img.shields.io/npm/dt/comfygure.svg)](http://npmjs.com/comfygure) [![docker pulls](https://img.shields.io/docker/pulls/marmelab/comfygure.svg)](https://hub.docker.com/r/marmelab/comfygure) [![Build Status](https://travis-ci.org/marmelab/comfygure.png?branch=master)](https://travis-ci.org/marmelab/comfygure)\n\n# comfygure\n\nEncrypted and versioned configuration storage built with collaboration in mind.\n\n[Source](https://github.com/marmelab/comfygure) - [Releases](https://github.com/marmelab/comfygure/releases) - [Stack Overflow](https://stackoverflow.com/questions/tagged/comfy/)\n\n[![asciicast](https://asciinema.org/a/137703.png)](https://asciinema.org/a/137703)\n\n## Features\n\n-   Simple CLI\n-   End-to-end AES-256 encryption\n-   Multiple formats support (JSON, YAML, environment variables)\n-   Git-like Versioning\n-   Easy to host on your own\n\nComfygure is great to manage application configurations for multiple environments, toggle feature flags quickly, manage A/B testing based on configuration files.\n\nIt is not a [Secret Management Tool](https://gist.github.com/maxvt/bb49a6c7243163b8120625fc8ae3f3cd), it focus on configurations files, their history, and how teams collaborate with them.\n\n## Get Started\n\nOn every server that needs access to the settings of an app, install the `comfy` CLI using `npm`:\n\n```bash\nnpm install -g comfygure\ncomfy help\n```\n\n## Usage\n\nInitialize comfygure in a project directory with `comfy init`:\n\n```bash\n> cd myproject\n> comfy init\n\nInitializing project configuration...\nProject created on comfy server https://comfy.marmelab.com\nConfiguration saved locally in .comfy/config\ncomfy project successfully created\n```\n\nThis creates a unique key to access the settings for `myproject`, and stores the key in `.comfy/config`. You can copy this file to share the credentials with co-workers or other computers.\n\n**Note**: By default, the `comfy` command stores encrypted settings in the `comfy.marmelab.com` server. To host your own comfy server, see [the related documentation](https://marmelab.com/comfygure/HostYourOwn.html#host-your-own-comfy-server).\n\nImport an existing settings file to comfygure using `comfy setall`:\n\n```bash\n> echo '{\"login\": \"admin\", \"password\": \"S3cr3T\"}' > config.json\n> comfy setall development config.json\nGreat! Your configuration was successfully saved.\n```\n\nFrom any computer sharing the same credentials, grab these settings using `comfy get`:\n\n```bash\n> comfy get development\n{\"login\": \"admin\", \"password\": \"S3cr3T\"}\n> comfy get development --envvars\nexport LOGIN='admin';\nexport PASSWORD='S3cr3T';\n```\n\nTo turn settings grabbed from comfygure into environment variables, use the following:\n\n```bash\n> comfy get development --envvars | source /dev/stdin\n> echo $LOGIN\nadmin\n```\n\nSee the [documentation](https://marmelab.com/comfygure/) to know more about how it works and the remote usage.\n\n## License\n\nComfygure is licensed under the [MIT License](https://github.com/marmelab/comfygure/blob/master/LICENSE), sponsored and supported by [marmelab](http://marmelab.com).\n"
  },
  {
    "path": "cli/bin/comfy.js",
    "content": "#!/usr/bin/env node\n\nconst nodeVersion = require('node-version');\n\nif (nodeVersion.major < 6) {\n    console.error('Comfygure requires at least version 6 of Node. Please upgrade!');\n    process.exit(1);\n}\n\nconst co = require('co');\n\nconst ui = require('../src/ui/console');\nconst comfy = require('../src')(ui, process.argv);\n\n// Dunno why I need to use co but I have to. I'll see that later\nco(comfy).catch(error => console.error(error));\n"
  },
  {
    "path": "cli/package.json",
    "content": "{\n    \"name\": \"comfygure\",\n    \"version\": \"1.2.0\",\n    \"description\": \"Encrypted and versioned configuration store built with collaboration in mind\",\n    \"keywords\": [\n        \"configuration\",\n        \"deployment\",\n        \"node\",\n        \"bash\",\n        \"continuous\"\n    ],\n    \"engines\": {\n        \"node\": \">=8\"\n    },\n    \"bin\": {\n        \"comfy\": \"bin/comfy.js\"\n    },\n    \"scripts\": {\n        \"test\": \"make test\"\n    },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/marmelab/comfygure.git\"\n    },\n    \"author\": \"Marmelab <info@marmelab.com> (https://marmelab.com/fr/)\",\n    \"contributors\": [\n        \"Kévin Maschtaler <kevin@marmelab.com>\"\n    ],\n    \"license\": \"MIT\",\n    \"bugs\": {\n        \"url\": \"https://github.com/marmelab/comfygure/issues\"\n    },\n    \"homepage\": \"https://marmelab.com/comfygure\",\n    \"dependencies\": {\n        \"chalk\": \"^3.0.0\",\n        \"cli-table\": \"^0.3.1\",\n        \"co\": \"^4.6.0\",\n        \"humanize-duration\": \"^3.22.0\",\n        \"ini\": \"^1.3.5\",\n        \"js-yaml\": \"^3.13.1\",\n        \"lodash.get\": \"^4.4.2\",\n        \"lodash.set\": \"^4.3.2\",\n        \"minimist\": \"^1.2.5\",\n        \"node-version\": \"^2.0.0\",\n        \"request\": \"^2.88.2\",\n        \"traverse\": \"^0.6.6\"\n    },\n    \"devDependencies\": {\n        \"eslint\": \"^6.8.0\",\n        \"eslint-config-prettier\": \"^6.10.1\",\n        \"eslint-plugin-prettier\": \"^3.1.2\",\n        \"jest\": \"^25.2.7\",\n        \"prettier\": \"^1.19.1\"\n    }\n}\n"
  },
  {
    "path": "cli/src/client.js",
    "content": "module.exports = request => {\n    const defaultHeaders = {\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n    };\n\n    const parseResponse = callback => (err, response) => {\n        if (err) {\n            callback(err);\n            return;\n        }\n\n        if (response.statusCode < 200 || response.statusCode >= 300) {\n            const error = new Error(`The API call returned a ${response.statusCode} HTTP error code`);\n            error.code = response.statusCode;\n            error.body = response.body && JSON.parse(response.body);\n            callback(error);\n            return;\n        }\n\n        callback(null, JSON.parse(response.body));\n    };\n\n    const get = (url, headers = {}) => cb =>\n        request(url, { headers: Object.assign({}, defaultHeaders, headers) }, parseResponse(cb));\n\n    const post = (url, body, headers = {}) => cb => {\n        const options = {\n            method: 'POST',\n            headers: Object.assign({}, defaultHeaders, headers),\n            body: JSON.stringify(body),\n        };\n\n        return request(url, options, parseResponse(cb));\n    };\n\n    const put = (url, body, headers = {}) => cb => {\n        const options = {\n            method: 'PUT',\n            headers: Object.assign({}, defaultHeaders, headers),\n            body: JSON.stringify(body),\n        };\n\n        return request(url, options, parseResponse(cb));\n    };\n\n    const remove = (url, headers = {}) => cb => {\n        const options = {\n            method: 'DELETE',\n            headers: Object.assign({}, defaultHeaders, headers),\n        };\n\n        return request(url, options, parseResponse(cb));\n    };\n\n    const buildAuthorization = project => ({ Authorization: `Token ${project.secretToken}` });\n\n    return {\n        get,\n        post,\n        put,\n        delete: remove,\n        buildAuthorization,\n    };\n};\n"
  },
  {
    "path": "cli/src/commands/admin.js",
    "content": "const exec = require('child_process').exec;\nconst minimist = require('minimist');\n\nconst moduleAvailable = name => {\n    try {\n        require.resolve(name);\n        return true;\n    } catch (e) {} // eslint-disable-line no-empty\n    return false;\n};\n\nconst runCommand = cmd =>\n    new Promise((resolve, reject) => {\n        exec(cmd, error => {\n            if (error) {\n                reject(error);\n                return;\n            }\n\n            resolve();\n        });\n    });\n\nconst help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy admin - Run the comfy admin web application\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} admin [<options>]\n\n${bold('OPTIONS')}\n        -p, --port      The port used to serve the admin (defaults to 3000)\n        -h, --help      Show this very help message\n\n${bold('EXAMPLE')}\n        ${cyan('comfy admin -p 8080')}\n`);\n};\n\nmodule.exports = ui =>\n    function* admin(rawOptions) {\n        const options = minimist(rawOptions);\n        const port = options.p || options.port || 3000;\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (moduleAvailable('comfy-admin')) {\n            yield runCommand(`comfy-admin -p ${port}`);\n            return;\n        }\n        ui.print('You need to install comfy-admin: npm install -g comfy-admin');\n        const response = yield ui.input.text('Do you want us to install it for you ? y/n');\n\n        if (response.toLowerCase() !== 'y') {\n            return;\n        }\n\n        try {\n            yield runCommand('npm install -g comfy');\n        } catch (error) {\n            if (error.message.match('Please try running this command again as root/Administrator.')) {\n                ui.print(`\n    Uh oh, it looks like npm need administrator rights to install package globally on your machine.\n    Either run the command with sudo and trust us, or look on internet to see how to configure your\n    environment so that sudo is no longer required to install global packages (which is a lot better).\n            `);\n                return;\n            }\n            throw error;\n        }\n        yield runCommand(`comfy-admin -p ${port}`);\n    };\n"
  },
  {
    "path": "cli/src/commands/diff.js",
    "content": "const fs = require('fs');\nconst minimist = require('minimist');\nconst { exec } = require('child_process');\n\nconst help = ui => {\n    const { bold, cyan, dim } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy diff - Show diff of two configurations for a given environment\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} diff <environment> <hash|tag> [<hash|tag>]\n\n${bold('OPTIONS')}\n        <environment>   Name of the environment (must already exist in project)\n        <hash|tag>      Hash or tag of your configuration to diff it\n        --json          Output the configurations as a JSON file\n        --envvars       Output the configurations as a sourceable bash file\n        --yml           Output the configurations as a YAML file\n        --js            Output the configurations as a JavaScript script\n        -w              Ignore all whitespaces\n        \n\n${bold('EXAMPLES')}\n        ${dim('# Diff from the latest version to the `stable` one')}\n        ${cyan('comfy diff development stable')}\n        ${dim('# Diff from one version to another')}\n        ${cyan('comfy diff development stable latest')}\n`);\n};\n\nmodule.exports = (ui, modules) =>\n    function* diff(rawOptions) {\n        const { red, bold, green } = ui.colors;\n        const options = minimist(rawOptions);\n        const [env, ...hashes] = options._;\n        const { w } = options;\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (!env || hashes.length === 0 || hashes.length > 2) {\n            ui.error(red('Not enough or too much arguments.'));\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} diff <environment> <hash|tag> [<hash|tag>]\n\nType ${green('comfy diff --help')} for details`);\n            return ui.exit(0);\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n\n        const firstConfigOutput = yield modules.config.getAndFormat(project, env, hashes[0], undefined, options);\n        const secondConfigOutput = yield modules.config.getAndFormat(\n            project,\n            env,\n            hashes[1] || 'latest',\n            undefined,\n            options\n        );\n\n        const firstConfigName = `/tmp/comfy-${hashes[0]}`;\n        const secondConfigName = `/tmp/comfy-${hashes[1] || 'latest'}`;\n\n        yield cb => fs.writeFile(firstConfigName, firstConfigOutput + '\\n', { flag: 'w' }, cb);\n        yield cb => fs.writeFile(secondConfigName, secondConfigOutput + '\\n', { flag: 'w' }, cb);\n\n        const code = yield cb =>\n            exec(`diff ${firstConfigName} ${secondConfigName} -u ${w ? ' -w' : ''}`, (error, stdout, stderr) => {\n                if (error && error.code && error.code !== 1) {\n                    ui.error(red('Failed to use the `diff` util. Is it installed on your host?'));\n                    return cb(null, 1);\n                }\n\n                stderr && ui.error(stderr);\n                stdout && ui.print(stdout);\n                cb(null, 0);\n            });\n\n        yield cb => fs.unlink(firstConfigName, cb);\n        yield cb => fs.unlink(secondConfigName, cb);\n\n        ui.exit(code);\n    };\n"
  },
  {
    "path": "cli/src/commands/env.js",
    "content": "const help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy env - Manage configuration environments\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} env <command> [<options>]\n\n${bold('COMMANDS')}\n        list                List all environments\n        add <environment>   Create the environment <environment>\n\n${bold('OPTIONS')}\n        <environment>   Name of the environment\n        -h, --help      Show this very help message\n\n${bold('EXAMPLES')}\n        ${cyan('comfy env ls')}\n        ${cyan('comfy env add production')}\n`);\n};\n\nconst list = (ui, modules) =>\n    function*() {\n        const project = yield modules.project.retrieveFromConfig();\n        const environments = yield modules.environment.list(project);\n\n        for (const environment of environments) {\n            ui.print(environment.name);\n        }\n\n        ui.exit();\n    };\n\nconst add = (ui, modules, options) =>\n    function*() {\n        const { red, bold, green } = ui.colors;\n\n        if (!options.length) {\n            ui.error(`${red('No environment specified.')}`);\n        }\n\n        if (options.length > 1) {\n            ui.error(`${red('Invalid environment format. The environment name should be one word.')}`);\n        }\n\n        if (options.length !== 1) {\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} env add <environment>\n\nType ${green('comfy env --help')} for details`);\n\n            return ui.exit(0);\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n        const environment = yield modules.environment.add(project, options[0]);\n        const addCommand = `comfy setall ${environment.name}`;\n\n        ui.print(`${bold(green('Environment successfully created'))}`);\n        ui.print(`You can now set a configuration for this environment using ${bold(addCommand)}`);\n        ui.exit();\n    };\n\nmodule.exports = (ui, modules) =>\n    function*([command, ...options]) {\n        switch (command) {\n            case 'list':\n            case 'ls':\n                yield list(ui, modules);\n                break;\n            case 'add':\n                yield add(ui, modules, options);\n                break;\n            default:\n                help(ui);\n        }\n    };\n"
  },
  {
    "path": "cli/src/commands/get.js",
    "content": "const minimist = require('minimist');\n\nconst help = ui => {\n    const { bold, cyan, dim } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy get - Retrieve the configuration for a given environment\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} get <environment> [<selector>] [<options>]\n\n${bold('OPTIONS')}\n        <environment>   Name of the environment (must already exist in project)\n        <selector>      Get only a subset of the config (dot separated)\n        --json          Output the configuration as a JSON file\n        --envvars       Output the configuration as a sourceable bash file\n        --yml           Output the configuration as a YAML file\n        --js            Output the configuration as a JavaScript script\n        -t, --tag=<tag> Get a tag for this config version (default: stable)\n        --hash=<hash>   Get a specific hash for this config version (ignore tag then)\n        -h, --help      Show this very help message\n\n${bold('EXAMPLES')}\n        ${dim('# Get the development configuration as json')}\n        ${cyan('comfy get development')}\n        ${dim('# Get the staging configuration for the next tag in yaml')}\n        ${cyan('comfy get staging -t next --yml > config/staging.yaml')}\n        ${dim('# Get the staging configuration for a specific hash in yaml')}\n        ${cyan('comfy get staging --hash=5eb9f3ea5cf01384333115007cf7606f --yml > config/staging.yaml')}\n        ${dim('# Get the production configuration and set it as environment variables')}\n        ${cyan('comfy get production --envvars | source /dev/stdin')}\n\n        ${dim('# Get only a field of the config')}\n        ${dim('config.json: { \"admin\": { \"user\": \"Admin\", \"pass\": \"1234\" } }')}\n        ${cyan('comfy get production admin.user')} // Admin\n`);\n};\n\nmodule.exports = (ui, modules) =>\n    function* get(rawOptions) {\n        const { bold, green, red } = ui.colors;\n        const options = minimist(rawOptions);\n        const [env, selector] = options._;\n        const tag = options.tag || options.t || 'latest';\n        const hash = options.hash;\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (!env) {\n            ui.error(red('No environment specified.'));\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} get <environment> [<selector>] [<options>]\n\nType ${green('comfy get --help')} for details`);\n            return ui.exit(0);\n        }\n\n        if ([options.json, options.yml, options.js].filter(x => x).length > 1) {\n            ui.error(`${red('You need to chose either --json, --yml or --js')}`);\n            help(ui);\n            return ui.exit(1);\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n\n        const output = yield modules.config.getAndFormat(project, env, hash || tag, selector, options);\n\n        ui.print(output);\n        ui.exit();\n    };\n"
  },
  {
    "path": "cli/src/commands/help.js",
    "content": "module.exports = ui => () => {\n    const { bold, dim, gray, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy - Store and deploy settings across development, test, and production environments, using an encrypted key-value store.\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} <command> [<options>]\n\n${bold('COMMANDS')}\n        help        Show this very help message\n        init        Initialize comfy for a directory\n        setall      Add a new configuration version\n        set         Replace a single entry in an existing configuration\n        get         Retrieve a configuration\n        diff        Diff two configuration versions\n        env         Manage configuration environments\n        tag         Manage configuration tags\n        log         List all configuration versions\n        project     Manage or deploy the current project\n        version     Output CLI version information and exit\n\n${bold('EXAMPLES')}\n        ${dim('# Display the help')}\n        ${cyan('comfy help')}\n        ${dim('# Initialize comfy in the current directory')}\n        ${cyan(\"comfy init --origin 'http://mycomfy.mydomain.com'\")}\n        ${dim('# Set a new configuration version')}\n        ${cyan('comfy setall development config/api.json')}\n        ${dim('# Add production environment')}\n        ${cyan('comfy env add production')}\n        ${dim('# Set a new configuration version for the next tag')}\n        ${cyan('comfy setall production -t next config/api_prod.json')}\n        ${dim('# List all configuration versions in production')}\n        ${cyan('comfy log production')}\n        ${dim('# Retrieve the latest development configuration and use it to set env vars')}\n        ${cyan('comfy get development --envvars | source /dev/stdin')}\n        ${dim('# Diff from your latest version to the stable one')}\n        ${cyan('comfy diff development stable')}\n\n${bold('ABOUT')}\n        ${bold('comfy')} is licensed under the MIT Licence, sponsored and supported by marmelab.\n        ${gray('-')} ${cyan('https://marmelab.com')}\n`);\n    ui.exit(0);\n};\n"
  },
  {
    "path": "cli/src/commands/init.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst minimist = require('minimist');\n\nconst help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy init - Initialize a comfy configuration for the current directory\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} init [<options>]\n\n${bold('OPTIONS')}\n        --name=<name>      The configuration name (defaults to the current directory name)\n        --env=<env>        The first environment to create (defaults to 'development')\n        --origin=<origin>  URL of the comfy server (defaults to https://comfy.marmelab.com)\n        -g, --nogitignore  Do not add .comfy directory to .gitignore\n        -h, --help         Show this very help message\n\n${bold('EXAMPLES')}\n        ${cyan('comfy init')}\n        ${cyan(\"comfy init --name foo --env 'development' --origin 'http://mycomfy.mydomain.com'\")}\n`);\n};\n\nmodule.exports = (ui, modules) =>\n    function*(rawOptions) {\n        const { bold, dim, yellow, green } = ui.colors;\n        const options = minimist(rawOptions);\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        const CONFIG_PATH = modules.project.CONFIG_PATH;\n        const checkAlreadyInitialized = fs.existsSync(`${process.cwd()}${path.sep}${CONFIG_PATH}`);\n        const isGitDirectory = fs.existsSync(`${process.cwd()}${path.sep}.git`);\n        const gitignore = `${process.cwd()}${path.sep}.gitignore`;\n\n        if (checkAlreadyInitialized) {\n            ui.error(\n                `${yellow('comfy is already initialized!')}` +\n                    `\\nYou can update your configuration by editing ${dim(CONFIG_PATH)}.`\n            );\n            return ui.exit(1);\n        }\n\n        const folders = process.cwd().split(path.sep);\n        const defaultProjectName = folders[folders.length - 1];\n        const projectName = options.name || defaultProjectName;\n        const environment = options.env || process.env.NODE_ENV || 'development';\n        const privateKey = modules.project.generateNewPrivateKey();\n        const hmacKey = modules.project.generateNewHmacKey();\n\n        ui.print('\\nInitializing project configuration...');\n\n        const project = yield modules.project.create(projectName, environment, options.origin);\n        yield modules.project.saveToConfig(project, privateKey, hmacKey, options.origin);\n        const { origin } = yield modules.project.retrieveFromConfig();\n\n        if (isGitDirectory && !options.g) {\n            const gitignoreContent = fs.readFileSync(gitignore);\n\n            if (!gitignoreContent.includes(CONFIG_PATH)) {\n                fs.appendFileSync(gitignore, `${CONFIG_PATH}\\n`);\n            }\n        }\n\n        ui.print(`Project created on comfy server ${dim(origin)}`);\n        ui.print(`Configuration saved locally in ${dim(CONFIG_PATH)}`);\n        ui.print(`${bold(green('comfy project successfully created'))}`);\n\n        ui.exit();\n    };\n"
  },
  {
    "path": "cli/src/commands/log.js",
    "content": "const minimist = require('minimist');\n\nconst help = ui => {\n    const { bold } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy log - List all configuration versions\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} log <environment> [<options>]\n\n${bold('OPTIONS')}\n        <environment>   Name of the environment (must already exist in project)\n        -t, --tags      Show only tagged versions\n        -h, --help      Show this very help message\n`);\n};\n\nconst formatDate = dateStr => {\n    const date = new Date(dateStr);\n\n    return date.toLocaleString();\n};\n\nmodule.exports = (ui, modules) =>\n    function*(rawOptions) {\n        const { bold, red, yellow, gray, green } = ui.colors;\n        const options = minimist(rawOptions);\n        const env = options._[0];\n        const onlyTags = options.t || options.tags;\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (!env) {\n            ui.error(`${red('No environment specified.')}`);\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} log <environment> [<options>]\n\nType ${green('comfy log --help')} for details`);\n            return ui.exit(0);\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n        const configs = yield modules.config.list(project, env, 'default', !onlyTags);\n\n        const noTag = gray('no tag');\n\n        for (const config of configs) {\n            const tags = config.tags.length > 0 ? config.tags.map(tag => yellow(tag)).join(', ') : noTag;\n\n            ui.print(`${formatDate(config.created_at)}\\t${env}\\t${config.hash}\\t${tags}`);\n        }\n    };\n"
  },
  {
    "path": "cli/src/commands/project.js",
    "content": "const minimist = require('minimist');\n\nconst help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy project - Manage your comfy project\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} project <command>\n\n${bold('COMMANDS')}\n        info            Display available infos of the current project\n        deploy          Show instructions to deploy your configurations on a server\n        delete          Permanently delete the current project from the store\n\n${bold('EXAMPLES')}\n        ${cyan('comfy project info')}\n        ${cyan('comfy project deploy')}\n        ${cyan('comfy project delete')}\n`);\n};\n\nconst info = function*(ui, modules) {\n    const project = yield modules.project.retrieveFromConfig();\n    const folder = modules.project.getConfigFolder();\n    const path = modules.project.getConfigPath();\n\n    ui.table([\n        ['Project ID', project.id],\n        ['Origin', project.origin],\n        ['Config. Folder', folder],\n        ['Config. File', path],\n        ['API Access Key', project.accessKey],\n    ]);\n};\n\nconst deploy = (ui, modules) => {\n    const { CREDENTIALS_VARIABLE, toEncodedCredentials } = modules.project;\n    const credentials = toEncodedCredentials();\n\n    const { dim } = ui.colors;\n\n    ui.print(\n        `Here are the instructions to install comfy on an remote server:\n\n    1. Install comfygure\n    2. Export the following environment variable\n    3. Retrieve your config in the format of your choice\n\n    ${dim('npm install -g comfygure')}\n    ${dim(`export ${CREDENTIALS_VARIABLE}=${credentials}`)}\n    ${dim('comfy get production --json')}\n`\n    );\n    ui.exit(0);\n};\n\nconst del = function*(ui, modules, rawOptions) {\n    const project = yield modules.project.retrieveFromConfig();\n    const options = minimist(rawOptions);\n    const { black, bgRedBright, cyan, bold, green } = ui.colors;\n\n    if (options.permanently === true && options.id === project.id) {\n        yield modules.project.permanentlyDelete();\n        ui.print(`${bold(green('comfy project successfully deleted'))}`);\n        ui.exit();\n        return;\n    }\n\n    const environments = yield modules.environment.list(project);\n\n    ui.print(`\n${bgRedBright(black('DANGER ZONE: This action is irreversible!'))}\n\nYou are about to delete your comfy project.\n\nThis process will delete the following informations from the origin (${project.origin}):\n    All configurations and their precedent versions\n    All environments (${environments.map(env => env.name).join(', ')})\n    All access keys and access logs\n    All available informations about the project \"${project.id}\"\n\nIf you are sure, please type the following command:\n${cyan('comfy project delete --permanently --id=<project-id>')}\n`);\n    ui.exit();\n};\n\nmodule.exports = (ui, modules) =>\n    function*([command, ...options]) {\n        switch (command) {\n            case 'i':\n            case 'info':\n                yield info(ui, modules);\n                break;\n            case 'deploy':\n                deploy(ui, modules);\n                break;\n            case 'delete':\n                yield del(ui, modules, options);\n                break;\n            default:\n                help(ui);\n        }\n    };\n"
  },
  {
    "path": "cli/src/commands/set.js",
    "content": "const minimist = require('minimist');\nconst set = require('lodash.set');\n\nconst { parseFlat } = require('../format');\n\nconst help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy set - Replace an entry of an existing configuration\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} set <environment> <selector> <value> [<options>]\n\n${bold('OPTIONS')}\n        <environment>     Name of the environment (must already exist in project)\n        <selector>        Select the entry of the config (dot separated)\n        <value>           The replacement value\n        -t, --tag=<tag>   Set a tag for this config version (default: stable)\n        -h, --help        Show this very help message\n\n${bold('EXAMPLES')}\n        ${cyan('comfy set development admin.user \"SuperUser\"')}\n        ${cyan('comfy set development admin.pass \"S3cret\" -t next')}\n`);\n};\n\nmodule.exports = (ui, modules) =>\n    function* setCommand(rawOptions) {\n        const { red, green, bold, dim } = ui.colors;\n        const options = minimist(rawOptions);\n        const env = options._[0];\n        const selector = options._[1];\n        const value = options._[2];\n        const tag = options.tag || options.t || 'latest';\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (!env) {\n            ui.error(red('No environment specified.'));\n        }\n\n        if (!selector) {\n            ui.error(red('No selector specified.'));\n        }\n\n        if (!value) {\n            ui.error(red('No value specified.'));\n        }\n\n        if (!env || !selector || !value) {\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} set <environment> <selector> <value> [<options>]\n\nType ${dim('comfy set --help')} for details`);\n            return ui.exit(0);\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n        const config = yield modules.config.get(project, env, {\n            configName: 'default',\n            tag,\n        });\n\n        const sanitizedSelector = selector.toLowerCase();\n        const updatedConfig = set(parseFlat(config.body), sanitizedSelector, value);\n\n        yield modules.config.add(project, env, updatedConfig, {\n            tag,\n            configName: 'default',\n            format: config.defaultFormat,\n        });\n\n        ui.print(`${bold(green('comfy configuration successfully saved'))}`);\n        return ui.exit();\n    };\n"
  },
  {
    "path": "cli/src/commands/setall.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst minimist = require('minimist');\nconst { parseYAML, guessFormat } = require('../format');\n\nconst help = ui => {\n    const { bold, cyan } = ui.colors;\n\n    ui.print(`\n${bold('NAME')}\n        comfy setall - Replace the configuration for a given environment\n\n${bold('SYNOPSIS')}\n        ${bold('comfy')} setall <environment> <path> [<options>]\n\n${bold('OPTIONS')}\n        <environment>     Name of the environment (must already exist in project)\n        <path>            Path to a configuration file (accepts json and yml formats)\n        -t, --tag=<tag>   Set a tag for this config version (default: stable)\n        -h, --help        Show this very help message\n\n${bold('EXAMPLES')}\n        ${cyan('comfy setall development config/comfy.json')}\n        ${cyan('comfy setall production config/api.yml -t next')}\n`);\n};\n\nmodule.exports = (ui, modules) =>\n    function* setall(rawOptions) {\n        const { red, green, bold } = ui.colors;\n        const options = minimist(rawOptions);\n        const env = options._[0];\n        const configPath = options._[1];\n        const tag = options.tag || options.t || 'latest';\n\n        if (options.help || options.h || options._.includes('help')) {\n            help(ui);\n            return ui.exit(0);\n        }\n\n        if (!env) {\n            ui.error(red('No environment specified.'));\n        }\n\n        if (!configPath) {\n            ui.error(red('No config file specified.'));\n        }\n\n        if (!env || !configPath) {\n            ui.print(`${bold('SYNOPSIS')}\n        ${bold('comfy')} setall <environment> <path> [<options>]\n\nType ${green('comfy setall --help')} for details`);\n            return ui.exit(0);\n        }\n\n        const filename = configPath.startsWith(path.sep)\n            ? path.normalize(configPath)\n            : path.normalize(`${process.cwd()}${path.sep}${configPath}`);\n\n        if (!fs.existsSync(filename)) {\n            ui.error(`The file ${red(configPath)} doesn't exist.`);\n            return ui.exit(1);\n        }\n\n        const stats = fs.statSync(filename);\n        if (!stats.isFile()) {\n            ui.error(`The object located at ${red(configPath)} is not a file.`);\n            return ui.exit(1);\n        }\n\n        const file = fs.readFileSync(filename, 'utf-8');\n\n        let parsedContent;\n\n        try {\n            parsedContent = parseYAML(file);\n        } catch (err) {\n            ui.error(red(`Failed to parse ${configPath}`));\n        }\n\n        const project = yield modules.project.retrieveFromConfig();\n\n        yield modules.config.add(project, env, parsedContent, {\n            tag,\n            configName: 'default',\n            format: guessFormat(path.extname(filename)),\n        });\n\n        ui.print(`${bold(green('comfy configuration successfully saved'))}`);\n        return ui.exit();\n    };\n"
  },
  {
    "path": "cli/src/commands/tag.js",
    "content": "const minimist = require(\"minimist\");\n\nconst help = (ui) => {\n  const { bold, dim, cyan } = ui.colors;\n\n  ui.print(`\n${bold(\"NAME\")}\n        comfy tag - Manage configuration tags\n\n${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} tag <command> [<options>]\n\n${bold(\"COMMANDS\")}\n        add <environment> <tag> <hash>      Add a new tag\n        list <environment>                  List tags\n        move <environment> <tag> <hash>     Move a tag to a new version\n        delete <environment> <tag>          Delete a tag\n\n${bold(\"OPTIONS\")}\n        <environment>   Name of the environment (must already exist in project)\n        <tag>           Name of the tag (e.g. \"stable\")\n        <hash>          Name of the hash (e.g. \"0b49fc8766d432fdd7422d948836d32f9632d72a\")\n        -h, --help      Show this very help message\n\n${bold(\"EXAMPLES\")}\n        ${dim('# Add a new \"experimental\" tag for development configuration')}\n        ${cyan(\n          \"comfy tag add development experimental 517ac071cec80340c8fc08cdb7eeefefbaf1dbba\"\n        )}\n        ${dim(\"# List tags for development configuration\")}\n        ${cyan(\"comfy tag list development\")}\n        ${dim('# Move the \"stable\" tag to another hash')}\n        ${cyan(\n          \"comfy tag move development stable 964e51df37c0fe2a518998fb6457b461c4013d28\"\n        )}\n        ${dim('# Delete the \"experimental\" tag in production')}\n        ${cyan(\"comfy tag delete production experimental\")}\n\n${bold(\"HINT\")}\n        To list tags, you can type ${bold(\"comfy log <environment>\")}\n`);\n};\n\nconst add = function*(ui, modules, options) {\n  const { green, red, bold } = ui.colors;\n  if (options._.length < 3) {\n    ui.error(red(\"Missing environment, tag, or hash\"));\n  }\n\n  if (options._.length > 3) {\n    ui.error(red(\"Too many arguments\"));\n  }\n\n  if (options._.length !== 3) {\n    ui.print(`${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} tag add <environment> <tag> <hash>\n\nType ${green(\"comfy tag --help\")} for details`);\n    return ui.exit(0);\n  }\n\n  const environment = options._[0];\n  const tag = options._[1];\n  const hash = options._[2];\n\n  const project = yield modules.project.retrieveFromConfig();\n  yield modules.tag.add(project, environment, \"default\", tag, hash);\n\n  ui.print(`${bold(green(\"Tag successfully created\"))}`);\n  ui.exit();\n};\n\nconst formatDate = (dateStr) => {\n  const date = new Date(dateStr);\n\n  return date.toLocaleString();\n};\n\nconst list = function*(ui, modules, options) {\n  const { green, red, bold, yellow, gray } = ui.colors;\n  if (options._.length === 0) {\n    ui.error(red(\"Missing environment\"));\n  }\n\n  if (options._.length > 1) {\n    ui.error(red(\"Too many arguments\"));\n  }\n\n  if (options._.length !== 1) {\n    ui.print(`${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} tag ls <environment>\n\nType ${green(\"comfy tag --help\")} for details`);\n    return ui.exit(0);\n  }\n\n  const env = options._[0];\n\n  const project = yield modules.project.retrieveFromConfig();\n  const configs = yield modules.config.list(project, env, \"default\", false);\n\n  const noTag = gray(\"no tag\");\n  for (const config of configs) {\n    const tags =\n      config.tags.length > 0\n        ? config.tags.map((tag) => yellow(tag)).join(\", \")\n        : noTag;\n\n    ui.print(\n      `${formatDate(config.created_at)}\\t${env}\\t${config.hash}\\t${tags}`\n    );\n  }\n\n  ui.exit();\n};\n\nconst move = function*(ui, modules, options) {\n  const { green, red, bold } = ui.colors;\n  if (options._.length < 3) {\n    ui.error(red(\"Missing environment, tag, or hash\"));\n  }\n\n  if (options._.length > 3) {\n    ui.error(red(\"Too many arguments\"));\n  }\n\n  if (options._.length !== 3) {\n    ui.print(`${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} tag move <environment> <tag> <hash>\n\nType ${green(\"comfy tag --help\")} for details`);\n    return ui.exit(0);\n  }\n\n  const environment = options._[0];\n  const tag = options._[1];\n  const hash = options._[2];\n\n  if (tag.toLowerCase() === \"latest\") {\n    ui.error(red(\"The tag `latest` cannot be moved\"));\n    return ui.exit(1);\n  }\n\n  const project = yield modules.project.retrieveFromConfig();\n  yield modules.tag.move(project, environment, \"default\", tag, hash);\n\n  ui.print(`${bold(green(\"Tag successfully moved\"))}`);\n  ui.exit();\n};\n\nconst remove = function*(ui, modules, options) {\n  const { green, red, bold } = ui.colors;\n  if (options._.length < 2) {\n    ui.error(red(\"Missing environment, or tag\"));\n  }\n\n  if (options._.length > 2) {\n    ui.error(red(\"Too many arguments\"));\n  }\n\n  if (options._.length !== 2) {\n    ui.print(`${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} tag delete <environment> <tag>\n\nType ${green(\"comfy tag --help\")} for details`);\n    return ui.exit(0);\n  }\n\n  const environment = options._[0];\n  const tag = options._[1];\n\n  if (tag.toLowerCase() === \"latest\") {\n    ui.error(red(\"The tag `latest` cannot be deleted\"));\n    return ui.exit(1);\n  }\n\n  const project = yield modules.project.retrieveFromConfig();\n  yield modules.tag.remove(project, environment, \"default\", tag);\n\n  ui.print(`${bold(green(\"Tag successfully deleted\"))}`);\n  ui.exit();\n};\n\nmodule.exports = (ui, modules) =>\n  function*([command, ...rawOptions]) {\n    const options = minimist(rawOptions);\n\n    if (options.help || options.h || options._.includes(\"help\")) {\n      help(ui);\n      return ui.exit(0);\n    }\n\n    switch (command) {\n      case \"add\":\n      case \"create\":\n        yield add(ui, modules, options);\n        break;\n      case \"list\":\n      case \"ls\":\n        yield list(ui, modules, options);\n        break;\n      case \"move\":\n      case \"mv\":\n        yield move(ui, modules, options);\n        break;\n      case \"delete\":\n      case \"remove\":\n      case \"rm\":\n        yield remove(ui, modules, options);\n        break;\n      default:\n        help(ui);\n    }\n  };\n"
  },
  {
    "path": "cli/src/commands/token.js",
    "content": "const minimist = require(\"minimist\");\nconst humanize = require(\"humanize-duration\");\n\nconst help = (ui) => {\n  const { bold, dim, cyan } = ui.colors;\n\n  ui.print(`\n${bold(\"NAME\")}\n      comfy token - Manage authentication tokens\n\n${bold(\"SYNOPSIS\")}\n      ${bold(\"comfy\")} token <command> [<options>]\n\n${bold(\"COMMANDS\")}\n      list [options]          List authentication tokens\n      add <name> [options]    Create a new token (default: read only, never expire)\n      delete <name>           Delete a token\n\n${bold(\"OPTIONS\")}\n      -a, --all               List expired tokens too\n      --full-access           Create a token with full access permissions\n      -e, --expires-in        Create a token with an expiry date (number of days)\n      -h, --help              Show this very help message\n\n${bold(\"EXAMPLES\")}\n      ${dim(\"# List all tokens, including expired ones\")}\n      ${cyan(\"comfy token list --all\")}\n`);\n};\n\nconst formatDate = (dateStr) => {\n  const date = new Date(dateStr);\n\n  return date.toLocaleString();\n};\n\nconst renderTokenLevel = ({ level }) => {\n  if (level === \"read\") {\n    return \"read only\".padEnd(16, \" \");\n  }\n\n  return \"full permissions\";\n};\n\nconst renderTokenState = (ui, { expiry_date }) => {\n  const { dim, green, red } = ui.colors;\n\n  if (!expiry_date) {\n    return `${green(\"active\")} ${dim(\"(never expire)\")}`;\n  }\n\n  const expiryDate = new Date(expiry_date);\n  const duration = Math.abs(new Date() - expiryDate);\n\n  if (expiryDate > new Date()) {\n    return `${green(\"active\")} ${dim(\n      `(expires in ${humanize(duration, { largest: 1 })})`\n    )}`;\n  }\n\n  return `${red(\"expired\")} ${dim(\n    `${humanize(duration, { largest: 1 })} ago`\n  )}`;\n};\n\nconst list = function*(ui, modules, options) {\n  const all = options.a || options.all;\n  const project = yield modules.project.retrieveFromConfig();\n  const tokens = yield modules.token.list(project, all);\n\n  const maxNameLength = tokens\n    .map((token) => token.name)\n    .reduce((max, name) => {\n      if (name.length > max) {\n        return name.length;\n      }\n\n      return max;\n    }, 0);\n\n  for (const token of tokens) {\n    const name = token.name.padEnd(maxNameLength, \" \");\n\n    ui.print(\n      `${formatDate(token.created_at)}\\t${name}\\t${renderTokenLevel(\n        token\n      )}\\t${renderTokenState(ui, token)}`\n    );\n  }\n\n  ui.exit();\n};\n\nconst parseName = (ui, options) => {\n  const { red, bold, green } = ui.colors;\n\n  if (options._.length < 1) {\n    ui.error(red(\"Missing token name\"));\n  }\n\n  if (options._.length > 1) {\n    ui.error(red(\"Too many arguments\"));\n  }\n\n  if (options._.length !== 1) {\n    ui.print(`${bold(\"SYNOPSIS\")}\n        ${bold(\"comfy\")} token add <name> --full-access=<false> --expires-in=<0>\n\nType ${green(\"comfy token --help\")} for details`);\n    return ui.exit(0);\n  }\n\n  return options._[0];\n};\n\nconst add = function*(ui, modules, options) {\n  const { bold, green } = ui.colors;\n\n  const name = parseName(ui, options);\n  const level = options[\"full-access\"] ? \"write\" : \"read\";\n  const expiresInDays = options.e || options[\"expires-in\"] || null;\n\n  const project = yield modules.project.retrieveFromConfig();\n  const token = yield modules.token.add(project, name, level, expiresInDays);\n\n  ui.print(`${bold(green(\"Token successfully created\"))}`);\n  ui.print(\n    \"Make sure to copy your new access token now. You won't be able to see it again!\"\n  );\n  ui.print(`${green(\"✓\")} ${token.key}`);\n  ui.exit();\n};\n\nconst remove = function*(ui, modules, options) {\n  const { green, bold, red, dim, bgRedBright, black, cyan } = ui.colors;\n  const name = parseName(ui, options);\n\n  const project = yield modules.project.retrieveFromConfig();\n  const tokens = yield modules.token.list(project, true);\n\n  const token = tokens.find((token) => token.name === name);\n\n  if (!token) {\n    ui.error(red(`Cannot find a token named \"${name}\"`));\n    ui.error(\n      `Type ${dim(\"comfy token list --all\")} to list all available tokens`\n    );\n    return ui.exit(1);\n  }\n\n  const isLastFullAccessToken =\n    token.level === \"write\" &&\n    tokens.filter((t) => t.level === \"write\").length === 1;\n\n  if (isLastFullAccessToken && !options.permanently) {\n    ui.print(`\n${bgRedBright(black(\"DANGER ZONE: This action is irreversible!\"))}\n\nYou are about to delete your last write token for this project.\n\nYou will no longer be able to update configurations on this project.\nIf there are available read tokens, they'll be able to retrieve configurations until they expires.\n\nIf you want to delete your project instead, you can type:\n${cyan(`comfy project delete`)}\n\nIf you are sure, please type the following command:\n${cyan(`comfy token delete --permanently ${name}`)}\n`);\n    ui.exit();\n  }\n\n  yield modules.token.remove(project, token.id);\n\n  ui.print(`${bold(green(\"Token successfully deleted\"))}`);\n  ui.exit();\n};\n\nmodule.exports = (ui, modules) =>\n  function*([command, ...rawOptions]) {\n    const options = minimist(rawOptions);\n\n    if (options.help || options.h || options._.includes(\"help\")) {\n      help(ui);\n      return ui.exit(0);\n    }\n\n    switch (command) {\n      case \"ls\":\n      case \"list\":\n        yield list(ui, modules, options);\n        break;\n      case \"add\":\n      case \"create\":\n        yield add(ui, modules, options);\n        break;\n      case \"delete\":\n      case \"remove\":\n      case \"rm\":\n        yield remove(ui, modules, options);\n        break;\n      default:\n        help(ui);\n    }\n  };\n"
  },
  {
    "path": "cli/src/commands/version.js",
    "content": "const printVersion = require('../domain/printVersion');\n\nmodule.exports = ui => {\n    printVersion();\n    ui.exit();\n};\n"
  },
  {
    "path": "cli/src/crypto/index.js",
    "content": "const crypto = require('crypto');\nconst { serialize, unserialize } = require('./serialization');\nconst { sign, isSignatureValid } = require('./signature');\n\nconst ALGORITHM = 'aes-256-ctr';\nconst KEY_BYTE_LENGTH = 32;\nconst IV_LENGTH = 16;\nconst HMAC_KEY_LENGTH = 32;\n\nconst hexToBuffer = hex => Buffer.from(hex, 'hex');\nconst bufferToHex = buffer => buffer.toString('hex');\n\nconst castKeyToBuffer = (key, castToBuffer = true) => {\n    if (Buffer.isBuffer(key)) {\n        if (key.length === KEY_BYTE_LENGTH) {\n            return key;\n        }\n\n        throw new Error(`The \"key\" argument must be a ${KEY_BYTE_LENGTH} bytes Buffer`);\n    }\n\n    if (castToBuffer) {\n        return castKeyToBuffer(hexToBuffer(key), false);\n    }\n\n    throw new Error('The \"key\" argument is must be a Buffer or a hexadecimal-encoded string');\n};\n\nconst encrypt = (value, privateKey, hmacKey) => {\n    const key = castKeyToBuffer(privateKey);\n    const serializedValue = serialize(value);\n    const iv = crypto.randomBytes(IV_LENGTH);\n\n    const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n    const cipherText = Buffer.concat([cipher.update(serializedValue, 'utf-8'), cipher.final()]);\n    const signature = sign(cipherText, iv, hmacKey);\n\n    return `${ALGORITHM}:${iv.toString('hex')}:${cipherText.toString('hex')}:${signature}`;\n};\n\nconst decrypt = (entry, privatekey, hmacKey) => {\n    const key = castKeyToBuffer(privatekey);\n\n    const [algorithm, hexIV, cipherText, signature] = entry.split(':');\n\n    if (algorithm !== ALGORITHM) {\n        throw new Error(`Unsupported algorithm: ${algorithm}`);\n    }\n\n    const iv = hexToBuffer(hexIV);\n    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n\n    if (!isSignatureValid(hexToBuffer(cipherText), iv, hmacKey, signature)) {\n        throw new Error('An encrypted value has been tampered. Aborting decryption.');\n    }\n\n    const decipherText = Buffer.concat([decipher.update(cipherText, 'hex'), decipher.final()]);\n\n    return unserialize(decipherText.toString('utf-8'));\n};\n\nconst generateNewPrivateKey = () => bufferToHex(crypto.randomBytes(KEY_BYTE_LENGTH));\nconst generateNewHmacKey = () => bufferToHex(crypto.randomBytes(HMAC_KEY_LENGTH));\n\nmodule.exports = {\n    ALGORITHM,\n    IV_LENGTH,\n    KEY_BYTE_LENGTH,\n    encrypt,\n    decrypt,\n    generateNewPrivateKey,\n    generateNewHmacKey,\n};\n"
  },
  {
    "path": "cli/src/crypto/index.spec.js",
    "content": "const { encrypt, decrypt, generateNewPrivateKey, generateNewHmacKey } = require('./');\n\ndescribe('Crypto Features', () => {\n    it('should keep the consistancy between encryption & decryption', () => {\n        const data = 'SOME VERY PRIVATE INFO';\n        const privateKey = generateNewPrivateKey();\n        const hmacKey = generateNewHmacKey();\n        const encryptedData = encrypt(data, privateKey, hmacKey);\n\n        expect(encryptedData).not.toBe(data);\n\n        const decryptedData = decrypt(encryptedData, privateKey, hmacKey);\n\n        expect(decryptedData).toBe(data);\n    });\n\n    it('should not return an identical signature twice for the same given entry and private key', () => {\n        const data = 'SOME VERY PRIVATE INFO';\n        const privateKey = generateNewPrivateKey();\n        const hmacKey = generateNewHmacKey();\n\n        const encryptedData = encrypt(data, privateKey, hmacKey);\n        const encryptedData2 = encrypt(data, privateKey, hmacKey);\n\n        expect(encryptedData).not.toBe(encryptedData2);\n\n        const decryptedData = decrypt(encryptedData, privateKey, hmacKey);\n        const decryptedData2 = decrypt(encryptedData2, privateKey, hmacKey);\n\n        expect(decryptedData).toBe(data);\n        expect(decryptedData2).toBe(data);\n    });\n\n    it('should throw an error if the data is tampered', () => {\n        const data = 'SOME VERY PRIVATE INFO';\n        const privateKey = generateNewPrivateKey();\n        const hmacKey = generateNewHmacKey();\n        const encryptedData = encrypt(data, privateKey, hmacKey);\n\n        const [algorithm, cipherText, iv, signature] = encryptedData.split(':');\n        const tamperedData = `${algorithm}:${cipherText}:${iv}:${signature}tampered`;\n\n        expect(() => {\n            decrypt(tamperedData, privateKey, hmacKey);\n        }).toThrow(/tampered/);\n\n        expect(() => {\n            decrypt(encryptedData, privateKey, hmacKey);\n        }).not.toThrow();\n    });\n});\n"
  },
  {
    "path": "cli/src/crypto/serialization.js",
    "content": "const serialize = JSON.stringify;\n\nconst unserialize = JSON.parse;\n\nmodule.exports = { serialize, unserialize };\n"
  },
  {
    "path": "cli/src/crypto/serialization.spec.js",
    "content": "const { serialize, unserialize } = require('./serialization');\n\ndescribe('Serialization', () => {\n    it('should keep the value of the serialized entry', () => {\n        const entry = 'entry';\n        const unserializedEntry = unserialize(serialize(entry));\n        expect(unserializedEntry).toEqual(entry);\n    });\n\n    it('should keep the type of the serialized entry', () => {\n        const entry = false;\n        expect(typeof entry).toEqual('boolean');\n\n        const serializedEntry = serialize(entry);\n        expect(typeof serializedEntry).toEqual('string');\n\n        const unseriazedEntry = unserialize(serializedEntry);\n        expect(typeof unseriazedEntry).toEqual('boolean');\n    });\n\n    it('should keep `null` intact', () => {\n        const entry = null;\n        const unserializedEntry = unserialize(serialize(entry));\n        expect(unserializedEntry).toEqual(entry);\n    });\n});\n"
  },
  {
    "path": "cli/src/crypto/signature.js",
    "content": "const crypto = require('crypto');\n\nconst ALGORITHM = 'SHA256';\n\nconst sign = (cipherText, iv, hmacKey) => {\n    const hmac = crypto.createHmac(ALGORITHM, hmacKey);\n    hmac.update(cipherText);\n    hmac.update(iv);\n\n    return hmac.digest('hex');\n};\n\nconst isSignatureValid = (cipherText, iv, hmacKey, signature) => {\n    const control = sign(cipherText, iv, hmacKey);\n\n    return control === signature;\n};\n\nmodule.exports = { sign, isSignatureValid };\n"
  },
  {
    "path": "cli/src/domain/config.js",
    "content": "const { parseFlat, toJSON, toYAML, toEnvVars, toJavascript, toFlat } = require('../format');\nconst { JSON, YAML, JAVASCRIPT } = require('../format/constants');\n\nconst { encrypt, decrypt } = require('../crypto');\n\nmodule.exports = (client, ui) => {\n    const list = function*(project, env, config, all = false) {\n        let url = config\n            ? `${project.origin}/projects/${project.id}/environments/${env}/configurations/${config}/history`\n            : `${project.origin}/projects/${project.id}/environments/${env}/configurations/history`;\n\n        if (all) {\n            url += '?all';\n        }\n\n        try {\n            return yield client.get(url, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            return ui.exit(1);\n        }\n    };\n\n    const add = function*(project, env, content, { configName, tag, format }) {\n        const entries = toFlat(content);\n\n        Object.keys(entries).forEach(key => {\n            entries[key] = encrypt(entries[key], project.privateKey, project.hmacKey);\n        });\n\n        const url = `${project.origin}/projects/${project.id}/environments/${env}/configurations/${configName}/${tag}`;\n\n        const body = {\n            entries,\n            format,\n        };\n\n        try {\n            yield client.post(url, body, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    const get = function*(project, env, { tag, hash }) {\n        let url = `${project.origin}/projects/${project.id}/environments/${env}/configurations/default`;\n        const hashOrTag = hash || tag;\n\n        if (hashOrTag) {\n            url += `/${hashOrTag}`;\n        }\n\n        let response;\n        try {\n            response = yield client.get(url, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n\n        const { body, defaultFormat } = response;\n\n        Object.keys(body).forEach(key => {\n            body[key] = decrypt(body[key], project.privateKey, project.hmacKey);\n        });\n\n        return { body, defaultFormat };\n    };\n\n    const getAndFormat = function*(project, env, hash, selector, options = {}) {\n        const config = yield get(project, env, { hash });\n\n        let format = config.defaultFormat;\n        if (options.json) format = JSON;\n        if (options.yml) format = YAML;\n        if (options.js) format = JAVASCRIPT;\n\n        let entries = config.body;\n        if (selector) {\n            const sanitizedSelector = selector.toLowerCase();\n            const entry = entries[sanitizedSelector] || entries[selector];\n\n            if (entry) {\n                // @TODO Support subset getter for nested entries\n                return entry;\n            }\n\n            entries = Object.entries(entries)\n                .map(([key, value]) => [key.toLowerCase(), value])\n                .filter(([key]) => key.startsWith(sanitizedSelector))\n                .reduce(\n                    (newEntries, [key, value]) =>\n                        Object.assign({}, newEntries, {\n                            [options.envvars || format === 'envvars'\n                                ? key\n                                : key.replace(`${sanitizedSelector}.`, '')]: value,\n                        }),\n                    {}\n                );\n        }\n\n        if (options.envvars) {\n            return toEnvVars(entries);\n        }\n\n        const body = parseFlat(entries);\n\n        switch (format) {\n            case YAML:\n                return toYAML(body);\n            case JAVASCRIPT:\n                return toJavascript(body);\n            default:\n                return toJSON(body);\n        }\n    };\n\n    return { list, add, get, getAndFormat };\n};\n"
  },
  {
    "path": "cli/src/domain/constants.js",
    "content": "const CONFIG_FOLDER = '.comfy';\nconst CONFIG_PATH = '.comfy/config';\nconst DEFAULT_ORIGIN = 'https://comfy.marmelab.com';\nconst CREDENTIALS_VARIABLE = 'COMFY_CREDENTIALS';\n\nmodule.exports = {\n    CONFIG_FOLDER,\n    CONFIG_PATH,\n    DEFAULT_ORIGIN,\n    CREDENTIALS_VARIABLE\n};\n"
  },
  {
    "path": "cli/src/domain/environment.js",
    "content": "module.exports = (client, ui) => {\n    const list = function*(project) {\n        const url = `${project.origin}/projects/${project.id}/environments`;\n\n        try {\n            return yield client.get(url, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    const add = function*(project, environmentName) {\n        const url = `${project.origin}/projects/${project.id}/environments`;\n        const data = { name: environmentName };\n\n        try {\n            return yield client.post(url, data, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    return { list, add };\n};\n"
  },
  {
    "path": "cli/src/domain/printVersion.js",
    "content": "const { name, version } = require('../../package.json');\n\nmodule.exports = () => {\n    console.log(`${name} ${version}`); // eslint-disable-line no-console\n};\n"
  },
  {
    "path": "cli/src/domain/project.js",
    "content": "const fs = require('fs');\nconst ini = require('ini');\nconst path = require('path');\nconst { CONFIG_FOLDER, CONFIG_PATH, DEFAULT_ORIGIN, CREDENTIALS_VARIABLE } = require('./constants');\nconst { generateNewPrivateKey, generateNewHmacKey } = require('../crypto');\n\nconst getConfigFolder = () => `${process.cwd()}${path.sep}${CONFIG_FOLDER}`;\nconst getConfigPath = () => `${process.cwd()}${path.sep}${CONFIG_PATH}`;\n\n// The function `fs.mkdir(folder, { recursive: true })` doesn't exist in Node <10\nconst mkdirRecursive = function*(folder) {\n    try {\n        yield cb => fs.mkdir(folder, cb);\n    } catch (error) {\n        if (error.code !== 'EEXIST') {\n            throw error;\n        }\n    }\n};\n\nmodule.exports = (client, ui) => {\n    const create = function*(name, environment, origin = DEFAULT_ORIGIN) {\n        const url = `${origin}/projects`;\n\n        try {\n            return yield client.post(url, { name, environment });\n        } catch (error) {\n            ui.printRequestError(error);\n            return ui.exit(1);\n        }\n    };\n\n    const saveToConfig = function*(project, privateKey, hmacKey, origin = DEFAULT_ORIGIN) {\n        const config = ini.stringify(\n            {\n                projectId: project.id,\n                accessKey: project.accessKey,\n                secretToken: project.writeToken,\n                origin,\n                privateKey,\n                hmacKey,\n            },\n            { section: 'project' }\n        );\n\n        const filename = getConfigPath();\n        yield mkdirRecursive(getConfigFolder());\n        yield cb => fs.writeFile(filename, config, { flag: 'w' }, cb);\n    };\n\n    const checkProjectInfos = ({ id, accessKey, secretToken, privateKey, hmacKey, origin }) => {\n        const errors = [];\n        const { red, bold } = ui.colors;\n\n        if (!id) {\n            errors.push(`Unable to locate the ${red('project identifier')}.`);\n        }\n\n        if (!accessKey) {\n            errors.push(`Unable to locate the ${red('access key')}.`);\n        }\n\n        if (!secretToken) {\n            errors.push(`Unable to locate the ${red('secret token')}.`);\n        }\n\n        if (!privateKey) {\n            errors.push(`Unable to locate the ${red('private key')} to decrypt your configs.`);\n        }\n\n        if (!hmacKey) {\n            errors.push(`Unable to locate the ${red('hmac key')} to sign and verify your configs.`);\n        }\n\n        if (!origin) {\n            errors.push(`Unable to locate the ${red('server origin')} to decrypt your configs.`);\n        }\n\n        if (errors.length > 0) {\n            ui.error(`${errors.join('\\n')}\n\nHave you exported the ${bold(CREDENTIALS_VARIABLE)} environment variable?\n\nHave you tried to initialize comfy in this folder?\nType ${bold('comfy init')} to do so.`);\n            ui.exit(1);\n        }\n    };\n\n    const retrieveFromConfig = () => {\n        if (process.env[CREDENTIALS_VARIABLE]) {\n            try {\n                const buffer = Buffer.from(process.env[CREDENTIALS_VARIABLE], 'base64');\n                const credentials = JSON.parse(buffer.toString('utf8'));\n                checkProjectInfos(credentials);\n                return credentials;\n            } catch (error) {\n                ui.error(`The credentials encoded in ${CREDENTIALS_VARIABLE} are invalid`);\n                ui.exit(1);\n            }\n        }\n\n        const envs = {\n            id: process.env.COMFY_PROJECT_ID,\n            accessKey: process.env.COMFY_ACCESS_KEY,\n            secretToken: process.env.COMFY_SECRET_TOKEN,\n            privateKey: process.env.COMFY_PRIVATE_KEY,\n            hmacKey: process.env.COMFY_HMAC_KEY,\n            origin: process.env.COMFY_ORIGIN,\n        };\n\n        const filename = getConfigPath();\n\n        if (!fs.existsSync(filename)) {\n            checkProjectInfos(envs);\n            return envs;\n        }\n\n        const file = fs.readFileSync(filename, 'utf8');\n        const config = ini.parse(file);\n\n        const projectInfos = Object.assign({}, envs, {\n            id: config.project.projectId,\n            accessKey: config.project.accessKey,\n            secretToken: config.project.secretToken,\n            privateKey: config.project.privateKey,\n            hmacKey: config.project.hmacKey,\n            origin: config.project.origin,\n        });\n\n        checkProjectInfos(projectInfos);\n        return projectInfos;\n    };\n\n    const toEncodedCredentials = () => {\n        const project = retrieveFromConfig();\n        const buffer = Buffer.from(JSON.stringify(project), 'utf8');\n        return buffer.toString('base64');\n    };\n\n    const permanentlyDelete = function*() {\n        const project = retrieveFromConfig();\n        const url = `${project.origin}/projects/${project.id}`;\n\n        try {\n            yield client.delete(url, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n\n        yield cb => fs.unlink(getConfigPath(), cb);\n        yield cb => fs.rmdir(getConfigFolder(), cb);\n    };\n\n    return {\n        create,\n        retrieveFromConfig,\n        saveToConfig,\n        CONFIG_FOLDER,\n        CONFIG_PATH,\n        CREDENTIALS_VARIABLE,\n        generateNewPrivateKey,\n        generateNewHmacKey,\n        getConfigFolder,\n        getConfigPath,\n        permanentlyDelete,\n        toEncodedCredentials,\n    };\n};\n"
  },
  {
    "path": "cli/src/domain/project.spec.js",
    "content": "const projectFactory = require('./project');\nconst { CREDENTIALS_VARIABLE } = require('./constants');\n\nconst client = null;\nconst ui = {\n    colors: { red: jest.fn(), bold: jest.fn() },\n    error: jest.fn(),\n    exit: jest.fn(),\n};\n\ndescribe('Project Domain', () => {\n    const credentials = {\n        id: 'id',\n        accessKey: 'accessKey',\n        secretToken: 'secretToken',\n        privateKey: 'privateKey',\n        hmacKey: 'hmacKey',\n        origin: 'origin',\n    };\n\n    const removeCredentialsFromEnvironment = () => {\n        delete process.env.COMFY_PROJECT_ID;\n        delete process.env.COMFY_ACCESS_KEY;\n        delete process.env.COMFY_SECRET_TOKEN;\n        delete process.env.COMFY_PRIVATE_KEY;\n        delete process.env.COMFY_HMAC_KEY;\n        delete process.env.COMFY_ORIGIN;\n    };\n\n    beforeEach(() => {\n        process.env.COMFY_PROJECT_ID = credentials.id;\n        process.env.COMFY_ACCESS_KEY = credentials.accessKey;\n        process.env.COMFY_SECRET_TOKEN = credentials.secretToken;\n        process.env.COMFY_PRIVATE_KEY = credentials.privateKey;\n        process.env.COMFY_HMAC_KEY = credentials.hmacKey;\n        process.env.COMFY_ORIGIN = credentials.origin;\n    });\n\n    it('should build an hex encoded string with existing credentials', () => {\n        const project = projectFactory(client, ui);\n        const encodedCredentials = project.toEncodedCredentials();\n\n        expect(encodedCredentials).toBe(\n            'eyJpZCI6ImlkIiwiYWNjZXNzS2V5IjoiYWNjZXNzS2V5Iiwic2VjcmV0VG9rZW4iOiJzZWNyZXRUb2tlbiIsInByaXZhdGVLZXkiOiJwcml2YXRlS2V5IiwiaG1hY0tleSI6ImhtYWNLZXkiLCJvcmlnaW4iOiJvcmlnaW4ifQ=='\n        );\n    });\n\n    it('should be able to restore a project configuration via an encoded credentials', () => {\n        process.env[CREDENTIALS_VARIABLE] = projectFactory(client, ui).toEncodedCredentials();\n\n        expect(projectFactory(client, ui).retrieveFromConfig()).toEqual(credentials);\n    });\n\n    afterEach(() => {\n        removeCredentialsFromEnvironment();\n        delete process.env[CREDENTIALS_VARIABLE];\n    });\n});\n"
  },
  {
    "path": "cli/src/domain/tag.js",
    "content": "module.exports = (client, ui) => {\n    const add = function*(project, environment, configName, name, selector) {\n        const url = `${project.origin}/projects/${\n            project.id\n        }/environments/${environment}/configurations/${configName}/tags`;\n        const body = {\n            name,\n            selector,\n        };\n\n        try {\n            return yield client.post(url, body, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    const move = function*(project, environment, configName, name, selector) {\n        const url = `${project.origin}/projects/${\n            project.id\n        }/environments/${environment}/configurations/${configName}/tags/${name}`;\n        const body = { selector };\n\n        try {\n            return yield client.put(url, body, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    const remove = function*(project, environment, configName, name) {\n        const url = `${project.origin}/projects/${\n            project.id\n        }/environments/${environment}/configurations/${configName}/tags/${name}`;\n\n        try {\n            return yield client.delete(url, client.buildAuthorization(project));\n        } catch (error) {\n            ui.printRequestError(error);\n            ui.exit(1);\n        }\n    };\n\n    return { add, move, remove };\n};\n"
  },
  {
    "path": "cli/src/domain/token.js",
    "content": "module.exports = (client, ui) => {\n  const list = function*(project, all = false) {\n    let url = `${project.origin}/projects/${project.id}/tokens`;\n\n    if (all) {\n      url += \"?all\";\n    }\n\n    try {\n      return yield client.get(url, client.buildAuthorization(project));\n    } catch (error) {\n      ui.printRequestError(error);\n      ui.exit(1);\n    }\n  };\n\n  const add = function*(project, name, level = \"read\", expiresInDays = null) {\n    const url = `${project.origin}/projects/${project.id}/tokens`;\n    const body = {\n      name,\n      level,\n      expiresInDays,\n    };\n\n    try {\n      return yield client.post(url, body, client.buildAuthorization(project));\n    } catch (error) {\n      ui.printRequestError(error);\n      ui.exit(1);\n    }\n  };\n\n  const remove = function*(project, id) {\n    const url = `${project.origin}/projects/${project.id}/tokens/${id}`;\n\n    try {\n      return yield client.delete(url, client.buildAuthorization(project));\n    } catch (error) {\n      ui.printRequestError(error);\n      ui.exit(1);\n    }\n  };\n\n  return { list, add, remove };\n};\n"
  },
  {
    "path": "cli/src/format/constants.js",
    "content": "const ENVVARS = 'envvars';\nconst JSON = 'json';\nconst YAML = 'yaml';\nconst JAVASCRIPT = 'javascript';\n\nmodule.exports = {\n    ENVVARS,\n    JSON,\n    YAML,\n    JAVASCRIPT,\n};\n"
  },
  {
    "path": "cli/src/format/guessFormat.js",
    "content": "const { JSON, YAML, ENVVARS } = require('./constants');\n\nmodule.exports = ext => {\n    switch ((ext || '').toLowerCase()) {\n        case '.json':\n            return JSON;\n        case '.yml':\n        case '.yaml':\n            return YAML;\n        default:\n            return ENVVARS;\n    }\n};\n"
  },
  {
    "path": "cli/src/format/guessFormat.spec.js",
    "content": "const { JSON, YAML, ENVVARS } = require('./constants');\nconst guessFormat = require('./guessFormat');\n\ndescribe('Format guessFormat', () => {\n    it.each([\n        ['.json', JSON],\n        ['.yml', YAML],\n        ['.yaml', YAML],\n        ['.unknown', ENVVARS],\n        [undefined, ENVVARS],\n        [null, ENVVARS],\n    ])('should transform extention \"%s\" into the format \"%s\"', (ext, format) => {\n        expect(guessFormat(ext)).toBe(format);\n    });\n});\n"
  },
  {
    "path": "cli/src/format/index.js",
    "content": "const deepSet = require('lodash.set');\nconst yaml = require('js-yaml');\nconst toFlat = require('./toFlat');\nconst guessFormat = require('./guessFormat');\n\nconst parseJSON = content => JSON.parse(content);\n\nconst toJSON = content => JSON.stringify(content, null, 4);\n\nconst parseYAML = content => yaml.safeLoad(content);\n\nconst toYAML = content => {\n    if (!content || Object.keys(content).length === 0) return '';\n\n    return yaml.safeDump(content);\n};\n\nconst parseFlat = content => {\n    const body = {};\n    const keys = Object.keys(content).sort();\n\n    for (const key of keys) {\n        deepSet(body, key, content[key]);\n    }\n\n    return body;\n};\n\nconst toEnvVars = flatContent => {\n    let source = '';\n\n    for (const key of Object.keys(flatContent).sort()) {\n        const value = flatContent[key] ? flatContent[key].toString() : '';\n\n        // Replace each ' by '\"'\"' in the value\n        // @see http://stackoverflow.com/a/1250279/3868326\n        const escapedValue = value.replace(\"'\", \"'\\\"'\\\"'\");\n\n        const envVar = key\n            .replace(/\\./g, '_')\n            .replace(/\\[/g, '_')\n            .replace(/\\]/g, '')\n            .toUpperCase();\n\n        source += `export ${envVar}='${escapedValue}';\\n`;\n    }\n\n    return source;\n};\n\nconst toJavascript = content => `window.COMFY = ${JSON.stringify(content)};`;\n\nmodule.exports = {\n    toJavascript,\n    parseJSON,\n    toJSON,\n    parseYAML,\n    toYAML,\n    parseFlat,\n    toEnvVars,\n    toFlat,\n    guessFormat,\n};\n"
  },
  {
    "path": "cli/src/format/index.spec.js",
    "content": "const { toEnvVars, toJavascript } = require('./');\n\ndescribe('Format', () => {\n    describe('toEnvVars', () => {\n        it('should handle null value', () => {\n            expect(toEnvVars({ key: '' })).toEqual(\"export KEY='';\\n\");\n            expect(toEnvVars({ key: null })).toEqual(\"export KEY='';\\n\");\n            expect(toEnvVars({ key: undefined })).toEqual(\"export KEY='';\\n\");\n        });\n\n        it('should handle multiple level of children', () => {\n            expect(toEnvVars({ 'key.a': 'value' })).toEqual(\"export KEY_A='value';\\n\");\n            expect(toEnvVars({ 'key.a.b': 'value' })).toEqual(\"export KEY_A_B='value';\\n\");\n        });\n\n        it('should handle (nested) lists', () => {\n            expect(toEnvVars({ 'key[0]': 'value' })).toEqual(\"export KEY_0='value';\\n\");\n            expect(toEnvVars({ 'key[0].a[0].b': 'value' })).toEqual(\"export KEY_0_A_0_B='value';\\n\");\n        });\n    });\n\n    describe('toJavascript', () => {\n        it('should transform a config into a javascript object', () => {\n            const config = {\n                key: undefined,\n                nullable: null,\n                admin: 'admin',\n                password: 'S3cret!',\n                permissions: ['read', 'write'],\n                attributes: { size: 42 },\n            };\n\n            expect(toJavascript(config)).toEqual(\n                'window.COMFY = {\"nullable\":null,\"admin\":\"admin\",\"password\":\"S3cret!\",\"permissions\":[\"read\",\"write\"],\"attributes\":{\"size\":42}};'\n            );\n        });\n    });\n});\n"
  },
  {
    "path": "cli/src/format/toFlat.js",
    "content": "const deepGet = require('lodash.get');\n\nconst traverse = (obj, parentKey = '') => {\n    const list = [];\n\n    if (Array.isArray(obj)) {\n        obj.forEach((item, i) => {\n            const traversed = traverse(item, `${parentKey}[${i}]`);\n            traversed.forEach(t => {\n                list.push(t);\n            });\n        });\n    } else if (obj && obj.toString() === '[object Object]') {\n        for (const key of Object.keys(obj)) {\n            const pKey = parentKey ? `${parentKey}.` : '';\n            const objectList = traverse(obj[key], `${pKey}${key}`);\n\n            for (const objectItem of objectList) {\n                list.push(objectItem);\n            }\n        }\n    } else {\n        list.push(parentKey);\n    }\n\n    return list;\n};\n\nconst toFlat = body => {\n    const content = {};\n    const keyList = traverse(body);\n\n    for (const key of keyList.sort()) {\n        content[key] = deepGet(body, key);\n    }\n\n    return content;\n};\n\nmodule.exports = toFlat;\n"
  },
  {
    "path": "cli/src/index.js",
    "content": "/* eslint-disable global-require */\nconst clientFactory = require(\"./client\");\nconst projectModuleFactory = require(\"./domain/project\");\nconst environmentModuleFactory = require(\"./domain/environment\");\nconst configModuleFactory = require(\"./domain/config\");\nconst tagModuleFactory = require(\"./domain/tag\");\nconst tokenModuleFactory = require(\"./domain/token\");\n\nconst printVersion = require(\"./domain/printVersion\");\n\nconst main = (ui, evt) => {\n  const commands = {\n    help: require(\"./commands/help\"),\n    init: require(\"./commands/init\"),\n    env: require(\"./commands/env\"),\n    setall: require(\"./commands/setall\"),\n    set: require(\"./commands/set\"),\n    get: require(\"./commands/get\"),\n    diff: require(\"./commands/diff\"),\n    log: require(\"./commands/log\"),\n    tag: require(\"./commands/tag\"),\n    version: require(\"./commands/version\"),\n    project: require(\"./commands/project\"),\n    token: require(\"./commands/token\")\n  };\n\n  return function* mainCommand() {\n    const request = ui.digestEvent(evt);\n\n    if (request.command === \"-V\" || request.arguments.includes(\"-V\")) {\n      printVersion();\n      ui.exit();\n    }\n\n    let command = commands.help;\n    if (request.command) {\n      command = commands[request.command];\n    }\n\n    if (!command) {\n      const { red, green } = ui.colors;\n\n      ui.error(\n        `The command ${red(request.command)} doesn't exist.` +\n          `\\nType ${green(\"comfy help\")} to see the available commands.`\n      );\n      ui.exit(1);\n    }\n\n    const client = clientFactory(require(\"request\"));\n\n    const modules = {\n      project: projectModuleFactory(client, ui),\n      environment: environmentModuleFactory(client, ui),\n      config: configModuleFactory(client, ui),\n      tag: tagModuleFactory(client, ui),\n      token: tokenModuleFactory(client, ui)\n    };\n\n    if (typeof command === \"function\") {\n      yield command(ui, modules)(request.arguments);\n    }\n  };\n};\n\nmodule.exports = main;\n"
  },
  {
    "path": "cli/src/ui/console.js",
    "content": "const chalk = require(\"chalk\");\nconst readline = require(\"readline\");\nconst Table = require(\"cli-table\");\n\nconst print = console.log; // eslint-disable-line no-console\nconst { error } = console; // eslint-disable-line no-console\n\n// eslint-disable-next-line no-unused-vars\nconst digestEvent = ([bin, file, command, ...args]) => ({\n  command,\n  arguments: args,\n});\n\nconst exit = (code = 0) => {\n  process.exit(code);\n};\n\nconst colors = chalk;\n\nconst input = {\n  text: (question) => (callback) => {\n    const reader = readline.createInterface({\n      input: process.stdin,\n      output: process.stdout,\n    });\n\n    reader.question(`${question} `, (answer) => {\n      reader.close();\n      callback(null, answer);\n    });\n  },\n};\n\nconst printRequestError = (err) => {\n  const { red, cyan, dim } = colors;\n\n  switch (err.code) {\n    case \"ECONNREFUSED\":\n    case \"ECONNRESET\":\n    case \"ETIMEDOUT\":\n    case \"ENETUNREACH\":\n    case \"ENOTFOUND\":\n      error(`${dim(err.message)}\n${red(\"Unable to reach the comfy server.\")}\nPlease check your connection and try again.`);\n      break;\n    case 403:\n      error(`${red(\"You are not allowed to perform this action.\")}\nPlease check your read or write token.`);\n      break;\n    case 400:\n    case 404:\n      if (err.body && err.body.message) {\n        error(red(err.body.message));\n        if (err.body.details) {\n          error(err.body.details);\n        }\n        break;\n      }\n    // eslint-disable-next-line no-fallthrough\n    default:\n      error(`${dim(err.message)}\n${dim(err.stack)}\n${red(\"Unknown error, command aborted.\")}\nIf the error persists, please report it at ${cyan(\n        \"https://github.com/marmelab/comfygure/issues\"\n      )}`);\n  }\n};\n\nconst table = (rows) => {\n  const t = new Table();\n  t.push(...rows);\n\n  print(t.toString());\n};\n\nmodule.exports = {\n  colors,\n  print,\n  error,\n  exit,\n  input,\n  table,\n  digestEvent,\n  printRequestError,\n};\n"
  },
  {
    "path": "docs/HostYourOwn.md",
    "content": "---\nlayout: default\ntitle: 'Host Your Own Comfygure Origin Server'\n---\n\n# Host Your Own Comfygure Origin Server\n\nMarmelab hosts the default comfygure server at `https://comfy.marmelab.com`. You can use it for free, for your tests, with no warranties of availability. We reserve the right to suspend usage in case of abuse.\n\nIn production, you'll probably want to host your own comfygure server. Fortunately, the comfygure server code is open-source and [available on GitHub](https://github.com/marmelab/comfygure).\n\nOnce your server is configured, use the standard `comfy` client to initialize your project, and pass your server URL in the `--origin` option:\n\n```bash\n# The origin will be stored in .comfy/config\n# Feel free to edit this file if you want to change your origin server\ncomfy init --origin https://my.custom.host\n```\n\nNow, how to setup an origin server? There are a few options.\n\n## With The Docker Image\n\nThe comfygure API is published as a docker image : [marmelab/comfygure](https://hub.docker.com/r/marmelab/comfygure).\n\nIt requires a [PostgreSQL instance](https://hub.docker.com/_/postgres) to store the configs, so let's start by that :\n\n```bash\n# Create a docker network in order to let comfygure reach the postgres container\ndocker network create comfy-network\n\n# Grab the initial database schema on the repo\nwget https://raw.githubusercontent.com/marmelab/comfygure/master/api/var/schema.sql\n\n# Start the postgres container with the initial schema\ndocker run --name comfy-postgres \\\n    -e POSTGRES_PASSWORD=mysecretpassword \\\n    -v `pwd`/schema.sql:/docker-entrypoint-initdb.d/schema.sql \\\n    --network comfy-network \\\n    -d  postgres\n\n# Run comfygure container and expose its port\ndocker run --name comfygure-api \\\n    -e PGHOST=comfy-postgres \\\n    -e PGDATABASE=postgres \\\n    -e PGPASSWORD=mysecretpassword \\\n    --network comfy-network \\\n    -p 3000:80 \\\n    -d marmelab/comfygure\n```\n\nYour comfygure API container is now up and running at http://localhost:3000, and you can use it with the comfy CLI to manage your configs.\n\n```bash\nnpm install -g comfygure\ncomfy init --origin http://localhost:3000\n\nInitializing project configuration...\nProject created on comfy server http://localhost:3000\nConfiguration saved locally in .comfy/config\ncomfy project successfully created\n```\n\n## With ZEIT's Now\n\nNow make deployment easy. Just like with a docker container, you have to configure your postgres instance.\n\nBut if you have to deploy the API, feel free to clone the GitHub repository and use pre-filled `now.json` file.\n\n```bash\ngit clone git@github.com:marmelab/comfygure.git\ncd comfygure/api\n```\n\nEdit `now.json` to add your environment variables\n\n```json\n{\n    \"name\": \"comfygure\",\n    \"version\": 2,\n    \"builds\": [\n        {\n            \"src\": \"build/index.js\",\n            \"use\": \"@now/node\"\n        }\n    ],\n    \"routes\": [{ \"src\": \".*\", \"dest\": \"/build\" }],\n    \"env\": {\n        \"PGHOST\": \"@comfy-pghost\",\n        \"PGDATABASE\": \"@comfy-pgdatabase\",\n        \"PGUSER\": \"@comfy-pguser\",\n        \"PGPASSWORD\": \"@comfy-pgpassword\"\n    }\n}\n```\n\nYou can then starting the server locally by running `now dev` or deploying directly with `now`.\n\nDon't forget to write down your environment variables in a `.env` file, or with [`now secret`](https://zeit.co/docs/v2/deployments/environment-variables-and-secrets).\n\n## Environment Variables\n\nThe comfygure API doesn't require much configuration other than to plug the postgres, here are all the environment variables you can use to tune it.\n\nTo be noted that the container has a configuration validation at the server start : if one of these environment variables is invalid, the container will crash at start.\n\n**`COMFY_LOG_DEBUG`** (default: false)\n\n**`PGHOST`** (default: localhost)\n\n**`PGPORT`** (default: 5432)\n\n**`PGDATABASE`** (default: comfy)\n\n**`PGUSER`** (default: postgres)\n\n**`PGPASSWORD`** (default: '')\n\nIt is **highly** recommended to set a default root password.\n"
  },
  {
    "path": "docs/HowItWorks.md",
    "content": "---\nlayout: default\ntitle: 'How It Works'\n---\n\n## The Problem: Managing Application Settings\n\nHow do you store the settings of a web application?\n\nMost developers use configuration files (`config.json`, `parameters.yaml`, `root.xml`, etc). But these files should not be committed to source control (git), because they contain sensible information (db passwords, service credentials, etc), and because they can change with each developer.\n\nSo developers usually commit fake config files (like `parameters.yml-dist`), and keep a real configuration file locally (ignored by source control). This leads to problems when one developer adds a new setting in the file, but doesn't tell other developers.\n\nAnother solution is to use environment variables. This makes sharing the settings between developers and between environments even harder.\n\n## The Solution\n\nComfygure proposes to solve that problem by storing settings on a remote server (like a remote git), encrypted. Comfygure clients know how to read and write from that remote server, and decrypt the settings to dump it locally.\n\n![comfygure workflow](./img/workflow.png)\n\nUltimately, this lets you execute the following command from any server:\n\n```bash\n> comfy get development --envvars\nexport LOGIN='admin';\nexport PASSWORD='S3cr3T';\n```\n\n## Storage\n\nBy default, comy stores the encrypted settings in the [comfy.marmelab.com](https://comfy.marmelab.com) server (run by marmelab). The comfygure project contains [the code](https://github.com/marmelab/comfygure) to let you host your own comfygure server (see the [Custom Server documentation](./HostYourOwn.html#host-your-own-comfy-server).\n\n## Security\n\nFrom a security standpoint, if the remote server is owned, the attacker can only access encrypted data (AES-256). Since the server never has access to the decryption key, the attacker can't decrypt the settings.\n\nDevelopers store the decryption key locally, allowing them to decrypt and/or update the app settings. In a similar fashion, CI servers can also check out the app settings with a simple decryption key, then build an artifact to be deployed to production.\n"
  },
  {
    "path": "docs/Usage.md",
    "content": "---\nlayout: default\ntitle: 'Usage'\n---\n\n## Initialization\n\nInitialize comfygure in a project directory with `comfy init`:\n\n```bash\n> cd dev/my-project\n> comfy init\n\nInitializing project configuration...\nProject created on comfy server https://comfy.marmelab.com\nConfiguration saved locally in .comfy/config\ncomfy project successfully created\n```\n\nBy default, the `comfy` command stores encrypted data in the `comfy.marmelab.com` server. To host your own comfy server, see [the related documentation](./HostYourOwn.html).\n\n### `.comfy/` Folder\n\nThe initialization command creates:\n\n-   A `.comfy/config` file containing all identification and credentials about the current project, in order to sync with the comfygure origin server\n-   A new line on your `.gitignore` in order to avoid committing this file (if a `.git` folder is found in the current folder)\n\nHere is how the comfygure config file looks like.\n\n```bash\n> cat .comfy/config\n\n[project]\n# Your project ID to identify your project, useful to debug\nprojectId=1111111111-1111-1111-1111-1111111111111\n# Your credentials to access to the comfy origin server\naccessKey=XXXXXXXXXXXXXXXX\nsecretToken=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY\n# The comfy server URI\norigin=https://comfy.marmelab.com\n# The private key used to encrypt your configuration, never sent to the server\nprivateKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n# The HMAC key used to sign and verify the integrity of your configuration, never sent to the server\nhmacKey=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\n```\n\nThe comfy server don't have access to your private and HMAC keys, ever. Be sure to keep these informations safe and secure.\n\n**Warning**: If you lose this file, you will no longer be able to retrieve your settings, and no one will be able to help you, not even the server administrators.\n\n## Managing Environments\n\nBy default, comfy creates one single environment, called `development`. You can choose a different name during initialization:\n\n```bash\ncomfy init --env production\n```\n\nAt any time, you can list environments, or create a new environment:\n\n```bash\n# list environments\n> comfy env ls\ndevelopment\n# create a \"production\" environment\n> comfy env add production\nEnvironment successfully created\nYou can now set a configuration for this environment using comfy setall production\n> comfy env ls\ndevelopment\nproduction\n```\n\n## Adding A New Version Of Settings\n\nWhen you initialize comfygure on an app, it starts with no settings.\n\n```bash\n> comfy get development\n{}\n```\n\nIn order to add a new version of the settings, you have to use the `setall` command, with a file containing your settings.\n\n```\n> cat config.json\n{ \"login\": \"admin\", \"password\": \"S3cret!\" }\n\n> comfy setall development config.json\ncomfy configuration successfully saved\n```\n\nOr your can use the `set` command to add or update a single entry in your config:\n\n```\n> comfy set development version \"0.1\"\n> comfy get development version\n0.1\n```\n\n## Retrieving Configuration\n\nTo retrieve a configuration, use `comfy get`:\n\n```bash\n> comfy get development\n{\n    \"login\": \"admin\",\n    \"password\": \"S3cret!\"\n}\n```\n\nOptionally, you can format the configuration as a YAML, or as environment variables:\n\n```bash\n> comfy get development --yml\nlogin: admin\npassword: S3cret!\n\n> comfy get development --envvars\nexport LOGIN='admin';\nexport PASSWORD='S3cret!';\n```\n\nYou can then use the standard output to create a new file, or source your environment variables.\n\n```bash\n> comfy get development --yml > src/config/development.yml\n> cat src/config/development.yml\nlogin: admin\npassword: S3cret!\n\n> comfy get development --envvars | source /dev/stdin\n> echo $LOGIN\nadmin\n```\n\n## Collaborating With A Team\n\nTo retrieve the settings of an app, comfygure needs all the information from the `.comfy/config` file for that app.\n\nIf you want to give the ability to Bob, your co-worker, to fetch the settings usinf comfygure, just give him this file.\n\n```bash\nscp .comfy/config bob@bob-workstation:~/repository/.comfy/config\n```\n\nYou and Bob will now be able to share the settings. If bob edits a setting, you and other team members can retrieve it immediately.\n\n## Deployment\n\nThe `.comfy/config` file is convenient for tests and development, but not for real deployment.\n\nTo this end, if comfy doesn't find `.comfy/config` from the current folder, it looks for the credentials in environment variables.\n\nInstructions to retrieve your configurations from a remote server are available by running `comfy project deploy`.\n\n```\n> comfy project deploy\nHere are the instructions to install comfy on an remote server:\n\n    1. Install comfygure\n    2. Export the following environment variable\n    3. Retrieve your config in the format of your choice\n\n    npm install -g comfygure\n    export COMFY_CREDENTIALS=<TOKEN>\n    comfy get production --json\n```\n\nThe `COMFY_CREDENTIALS` environment variable is generated using your credentials in `.comfy/config`. It contains your comfy credentials in a JSON string encoded in base64, is is not encrypted. **Do not share this token.**\n\nAlternatively, you can specify all environment variable one by one, if you need to fine tune your comfy CLI. Say, you have the following `.comfy/config` file:\n\n```ini\n[project]\nprojectId=1111111111-1111-1111-1111-1111111111111\naccessKey=XXXXXXXXXXXXXXXX\nsecretToken=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY\norigin=https://comfy.marmelab.com\nprivateKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nhmacKey=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\n```\n\nYou can specify the following environment variables to replace it:\n\n```bash\n# All of these are included in COMFY_CREDENTIALS\nexport COMFY_PROJECT_ID=1111111111-1111-1111-1111-1111111111111;\nexport COMFY_ACCESS_KEY=XXXXXXXXXXXXXXXX;\nexport COMFY_SECRET_TOKEN=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY;\nexport COMFY_ORIGIN=https://comfy.marmelab.com;\nexport COMFY_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;\nexport COMFY_HMAC_KEY=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy;\n\ncomfy get production\n```\n\nSet the environment variable(s) in your CI configuration, code builder, or any continuous delivery system to let them use your configurations.\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "name: comfygure documentation\nmarkdown: kramdown\nkramdown:\n  input: GFM\nhighlighter: rouge\n"
  },
  {
    "path": "docs/_layouts/default.html",
    "content": "<!DOCTYPE HTML>\n<html lang=\"en-US\">\n\n<head>\n    <title>Comfygure - {{ page.title }}</title>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n    <meta name=\"description\" content=\"{{ page.description }}\">\n    <meta name=\"HandheldFriendly\" content=\"true\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n    <link rel=\"stylesheet\" href=\"css/normalize.css\">\n    <link rel=\"stylesheet\" href=\"css/style.css\">\n    <link rel=\"stylesheet\" href=\"css/syntax.css\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/octicons/4.4.0/font/octicons.css\">\n</head>\n\n<body>\n    <div class=\"book with-summary font-size-2 font-family-1\">\n        <div class=\"book-summary\">\n            <nav role=\"navigation\">\n                <ul class=\"summary\">\n                    <li class=\"header\">Table of Contents</li>\n                    <li class=\"chapter {% if page.path == 'index.md' %}active{% endif %}\">\n                        <a href=\"./index.html\">\n                            <b>1.</b> Comfygure\n                        </a>\n                    </li>\n                    <li class=\"chapter {% if page.path == 'HowItWorks.md' %}active{% endif %}\">\n                        <a href=\"./HowItWorks.html\">\n                            <b>2.</b> How It Works\n                        </a>\n                        <ul class=\"articles\" {% if page.path != 'HowItWorks.md' %}style=\"display:none\" {% endif %}>\n                            <li class=\"chapter\">\n                                <a href=\"#the-problem-managing-application-configuration\">\n                                    The Problem: Managing Application Configuration\n                                </a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#the-solution\">The Solution</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#storage\">Storage</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#Security\">Security</a>\n                            </li>\n                        </ul>\n                    </li>\n                    <li class=\"chapter {% if page.path == 'Usage.md' %}active{% endif %}\">\n                        <a href=\"./Usage.html\">\n                            <b>3.</b> Basic Usage\n                        </a>\n                        <ul class=\"articles\" {% if page.path != 'Usage.md' %}style=\"display:none\" {% endif %}>\n                            <li class=\"chapter\">\n                                <a href=\"#installation\">\n                                    Installation\n                                </a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#project-initialization\">Project Initialization</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#managing-environments\">Managing Environments</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#add-a-configuration-version\">Add a configuration version</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#retrieve-a-configuration\">Retrieve a configuration</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#collaborate-with-your-team\">Collaborate with your team</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#deployment\">Deployment</a>\n                            </li>\n                        </ul>\n                    </li>\n                    <li class=\"chapter {% if path.path == 'HostYourOwn.md' %}active{% endif %}\">\n                        <a href=\"./HostYourOwn.html\">\n                            <b>4.</b> Host Your Own\n                        </a>\n                        <ul class=\"articles\" {% if page.path != 'HostYourOwn.md' %}style=\"display:none\" {% endif %}>\n                            <li class=\"chapter\">\n                                <a href=\"#with-the-docker-image\">With the Docker Image</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#with-zeits-now\">With ZEIT's Now</a>\n                            </li>\n                            <li class=\"chapter\">\n                                <a href=\"#environment-variables\">Environment Variables</a>\n                            </li>\n                        </ul>\n                    </li>\n                </ul>\n            </nav>\n        </div>\n        <div class=\"book-body\">\n            <div class=\"body-inner\">\n                <div class=\"page-wrapper\" tabindex=\"-1\" role=\"main\">\n                    <div class=\"page-inner\">\n\n                        <div id=\"book-search-results\">\n                            <div class=\"search-noresults\">\n\n                                <section class=\"normal markdown-section\">\n\n                                    {{ content }}\n\n                                </section>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n        <script>\n            (function (i, s, o, g, r, a, m) {\n                i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {\n                    (i[r].q = i[r].q || []).push(arguments)\n                }, i[r].l = 1 * new Date(); a = s.createElement(o),\n                    m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)\n            })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');\n\n            ga('create', 'UA-46201426-1', 'auto');\n            ga('send', 'pageview');\n        </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "docs/css/normalize.css",
    "content": "/*! normalize.css v4.2.0 | MIT License | github.com/necolas/normalize.css */\n\n/**\n * 1. Change the default font family in all browsers (opinionated).\n * 2. Correct the line height in all browsers.\n * 3. Prevent adjustments of font size after orientation changes in IE and iOS.\n */\n\n/* Document\n   ========================================================================== */\n\nhtml {\n  font-family: sans-serif; /* 1 */\n  line-height: 1.15; /* 2 */\n  -ms-text-size-adjust: 100%; /* 3 */\n  -webkit-text-size-adjust: 100%; /* 3 */\n}\n\n/* Sections\n   ========================================================================== */\n\n/**\n * Remove the margin in all browsers (opinionated).\n */\n\nbody {\n  margin: 0;\n}\n\n/**\n * Add the correct display in IE 9-.\n */\n\narticle,\naside,\nfooter,\nheader,\nnav,\nsection {\n  display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n * 1. Add the correct display in IE.\n */\n\nfigcaption,\nfigure,\nmain { /* 1 */\n  display: block;\n}\n\n/**\n * Add the correct margin in IE 8.\n */\n\nfigure {\n  margin: 1em 40px;\n}\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * 1. Remove the gray background on active links in IE 10.\n * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\n */\n\na {\n  background-color: transparent; /* 1 */\n  -webkit-text-decoration-skip: objects; /* 2 */\n}\n\n/**\n * Remove the outline on focused links when they are also active or hovered\n * in all browsers (opinionated).\n */\n\na:active,\na:hover {\n  outline-width: 0;\n}\n\n/**\n * 1. Remove the bottom border in Firefox 39-.\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\n */\n\nb,\nstrong {\n  font-weight: inherit;\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font style in Android 4.3-.\n */\n\ndfn {\n  font-style: italic;\n}\n\n/**\n * Add the correct background and color in IE 9-.\n */\n\nmark {\n  background-color: #ff0;\n  color: #000;\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n */\n\naudio,\nvideo {\n  display: inline-block;\n}\n\n/**\n * Add the correct display in iOS 4-7.\n */\n\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n\n/**\n * Remove the border on images inside links in IE 10-.\n */\n\nimg {\n  border-style: none;\n}\n\n/**\n * Hide the overflow in IE.\n */\n\nsvg:not(:root) {\n  overflow: hidden;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers (opinionated).\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: sans-serif; /* 1 */\n  font-size: 100%; /* 1 */\n  line-height: 1.15; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n  text-transform: none;\n}\n\n/**\n * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n *    controls in Android 4.\n * 2. Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\nhtml [type=\"button\"], /* 1 */\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button; /* 2 */\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Change the border, margin, and padding in all browsers (opinionated).\n */\n\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * 1. Add the correct display in IE 9-.\n * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  display: inline-block; /* 1 */\n  vertical-align: baseline; /* 2 */\n}\n\n/**\n * Remove the default vertical scrollbar in IE.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10-.\n * 2. Remove the padding in IE 10-.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding and cancel buttons in Chrome and Safari on OS X.\n */\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/* Interactive\n   ========================================================================== */\n\n/*\n * Add the correct display in IE 9-.\n * 1. Add the correct display in Edge, IE, and Firefox.\n */\n\ndetails, /* 1 */\nmenu {\n  display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n  display: list-item;\n}\n\n/* Scripting\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n */\n\ncanvas {\n  display: inline-block;\n}\n\n/**\n * Add the correct display in IE.\n */\n\ntemplate {\n  display: none;\n}\n\n/* Hidden\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 10-.\n */\n\n[hidden] {\n  display: none;\n}\n"
  },
  {
    "path": "docs/css/style.css",
    "content": ".book-summary {\n    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n    position: absolute;\n    top: 0;\n    left: -300px;\n    bottom: 0;\n    z-index: 1;\n    overflow-y: auto;\n    width: 300px;\n    color: #364149;\n    background: #fafafa;\n    border-right: 1px solid rgba(0, 0, 0, 0.07);\n    -webkit-transition: left 250ms ease;\n    -moz-transition: left 250ms ease;\n    -o-transition: left 250ms ease;\n    transition: left 250ms ease;\n}\n.book-summary ul.summary {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n    -webkit-transition: top 0.5s ease;\n    -moz-transition: top 0.5s ease;\n    -o-transition: top 0.5s ease;\n    transition: top 0.5s ease;\n}\n.book-summary ul.summary li {\n    list-style: none;\n}\n.book-summary ul.summary li.header {\n    padding: 10px 15px;\n    padding-top: 20px;\n    text-transform: uppercase;\n    color: #939da3;\n}\n.book-summary ul.summary li.divider {\n    height: 1px;\n    margin: 7px 0;\n    overflow: hidden;\n    background: rgba(0, 0, 0, 0.07);\n}\n.book-summary ul.summary li i.fa-check {\n    display: none;\n    position: absolute;\n    right: 9px;\n    top: 16px;\n    font-size: 9px;\n    color: #3c3;\n}\n.book-summary ul.summary li.done > a {\n    color: #364149;\n    font-weight: 400;\n}\n.book-summary ul.summary li.done > a i {\n    display: inline;\n}\n.book-summary ul.summary li a,\n.book-summary ul.summary li span {\n    display: block;\n    padding: 10px 15px;\n    border-bottom: none;\n    color: #364149;\n    background: 0 0;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    position: relative;\n}\n.book-summary ul.summary li a:hover {\n    text-decoration: underline;\n}\n.book-summary ul.summary li a:focus {\n    outline: 0;\n}\n.book-summary ul.summary li.active > a {\n    color: #008cff;\n    background: 0 0;\n    text-decoration: none;\n}\n.book-summary ul.summary li ul {\n    padding-left: 20px;\n}\n@media (max-width: 600px) {\n    .book-summary {\n        width: calc(100% - 60px);\n        bottom: 0;\n        left: -100%;\n    }\n}\n.book.with-summary .book-summary {\n    left: 0;\n}\n.book.without-animation .book-summary {\n    -webkit-transition: none !important;\n    -moz-transition: none !important;\n    -o-transition: none !important;\n    transition: none !important;\n}\n.book {\n    position: relative;\n    width: 100%;\n    height: 100%;\n}\n@media (min-width: 600px) {\n    .book.with-summary .book-body {\n        left: 300px;\n    }\n}\n@media (max-width: 600px) {\n    .book.with-summary {\n        overflow: hidden;\n    }\n    .book.with-summary .book-summary {\n        width: 0;\n    }\n}\n.book.without-animation .book-body {\n    -webkit-transition: none !important;\n    -moz-transition: none !important;\n    -o-transition: none !important;\n    transition: none !important;\n}\n.book-body {\n    position: absolute;\n    top: 0;\n    right: 0;\n    left: 0;\n    bottom: 0;\n    overflow-y: auto;\n    color: #000;\n    background: #fff;\n    -webkit-transition: left 250ms ease;\n    -moz-transition: left 250ms ease;\n    -o-transition: left 250ms ease;\n    transition: left 250ms ease;\n}\n.book-body .body-inner {\n    position: absolute;\n    top: 0;\n    right: 0;\n    left: 0;\n    bottom: 0;\n    overflow-y: auto;\n}\n@media (max-width: 1240px) {\n    .book-body {\n        -webkit-transition: -webkit-transform 250ms ease;\n        -moz-transition: -moz-transform 250ms ease;\n        -o-transition: -o-transform 250ms ease;\n        transition: transform 250ms ease;\n        padding-bottom: 20px;\n    }\n    .book-body .body-inner {\n        position: static;\n        min-height: calc(100% - 50px);\n    }\n}\n.page-wrapper {\n    position: relative;\n    outline: 0;\n}\n.page-inner {\n    position: relative;\n    max-width: 800px;\n    margin: 0 auto;\n    padding: 20px 15px 40px 15px;\n}\n.page-inner .btn-group .btn {\n    border-radius: 0;\n    background: #eee;\n    border: 0;\n}\n.buttons:after,\n.buttons:before {\n    content: ' ';\n    display: table;\n    line-height: 0;\n}\n.buttons:after {\n    clear: both;\n}\n.button {\n    border: 0;\n    background-color: transparent;\n    background: #eee;\n    color: #666;\n    width: 100%;\n    text-align: center;\n    float: left;\n    line-height: 1.42857143;\n    padding: 8px 4px;\n}\n.button:hover {\n    color: #444;\n}\n.button:focus,\n.button:hover {\n    outline: 0;\n}\n.button.size-2 {\n    width: 50%;\n}\n.button.size-3 {\n    width: 33%;\n}\n.markdown-section {\n    display: block;\n    word-wrap: break-word;\n    overflow: hidden;\n    color: #333;\n    line-height: 1.7;\n    text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    -webkit-text-size-adjust: 100%;\n    -moz-text-size-adjust: 100%;\n}\n.markdown-section * {\n    box-sizing: border-box;\n    -webkit-box-sizing: border-box;\n    font-size: inherit;\n}\n.markdown-section > :first-child {\n    margin-top: 0 !important;\n}\n.markdown-section > :last-child {\n    margin-bottom: 0 !important;\n}\n.markdown-section blockquote,\n.markdown-section code,\n.markdown-section figure,\n.markdown-section img,\n.markdown-section pre,\n.markdown-section table,\n.markdown-section tr {\n    page-break-inside: avoid;\n}\n.markdown-section h2,\n.markdown-section h3,\n.markdown-section h4,\n.markdown-section h5,\n.markdown-section p {\n    orphans: 3;\n    widows: 3;\n}\n.markdown-section h1,\n.markdown-section h2,\n.markdown-section h3,\n.markdown-section h4,\n.markdown-section h5 {\n    page-break-after: avoid;\n}\n.markdown-section b,\n.markdown-section strong {\n    font-weight: 700;\n}\n.markdown-section em {\n    font-style: italic;\n}\n.markdown-section blockquote,\n.markdown-section dl,\n.markdown-section ol,\n.markdown-section p,\n.markdown-section table,\n.markdown-section ul {\n    margin-top: 0;\n    margin-bottom: 0.85em;\n}\n.markdown-section a {\n    color: #4183c4;\n    text-decoration: none;\n    background: 0 0;\n}\n.markdown-section a:active,\n.markdown-section a:focus,\n.markdown-section a:hover {\n    outline: 0;\n    text-decoration: underline;\n}\n.markdown-section img {\n    max-width: 90%;\n    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);\n    border-radius: 2px;\n    margin: 2rem;\n}\n.markdown-section img.no-margin {\n    margin: 0;\n    box-shadow: none;\n}\n.markdown-section hr {\n    height: 4px;\n    padding: 0;\n    margin: 1.7em 0;\n    overflow: hidden;\n    background-color: #e7e7e7;\n    border: none;\n}\n.markdown-section hr:after,\n.markdown-section hr:before {\n    display: table;\n    content: ' ';\n}\n.markdown-section hr:after {\n    clear: both;\n}\n.markdown-section h1,\n.markdown-section h2,\n.markdown-section h3 {\n    margin-top: 1.275em;\n    margin-bottom: 0.85em;\n    font-weight: 700;\n}\n.markdown-section h1 {\n    font-size: 2em;\n}\n.markdown-section h2 {\n    font-size: 1.75em;\n}\n.markdown-section h3 {\n    margin-top: 0.85em;\n    font-size: 1em;\n}\n.markdown-section code,\n.markdown-section pre {\n    font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n    direction: ltr;\n    margin: 0;\n    padding: 0;\n    border: none;\n    color: inherit;\n}\n.markdown-section pre {\n    overflow: auto;\n    word-wrap: normal;\n    margin: 0;\n    padding: 0.85em 1em;\n    margin-bottom: 1.275em;\n    background: #f7f7f7;\n}\n.markdown-section pre > code {\n    display: inline;\n    max-width: initial;\n    padding: 0;\n    margin: 0;\n    overflow: initial;\n    line-height: inherit;\n    font-size: 0.85em;\n    white-space: pre;\n    background: 0 0;\n}\n.markdown-section pre > code:after,\n.markdown-section pre > code:before {\n    content: normal;\n}\n.markdown-section code {\n    padding: 0.2em;\n    margin: 0;\n    font-size: 0.85em;\n    background-color: #f7f7f7;\n}\n.markdown-section code:after,\n.markdown-section code:before {\n    letter-spacing: -0.2em;\n    content: '\\00a0';\n}\n.markdown-section table {\n    display: table;\n    width: 100%;\n    border-collapse: collapse;\n    border-spacing: 0;\n    overflow: auto;\n}\n.markdown-section table td,\n.markdown-section table th {\n    padding: 6px 13px;\n    border: 1px solid #ddd;\n}\n.markdown-section table tr {\n    background-color: #fff;\n    border-top: 1px solid #ccc;\n}\n.markdown-section table tr:nth-child(2n) {\n    background-color: #f8f8f8;\n}\n.markdown-section table th {\n    font-weight: 700;\n}\n.markdown-section ol,\n.markdown-section ul {\n    padding: 0;\n    margin: 0;\n    margin-bottom: 0.85em;\n    padding-left: 2em;\n}\n.markdown-section ol ol,\n.markdown-section ol ul,\n.markdown-section ul ol,\n.markdown-section ul ul {\n    margin-top: 0;\n    margin-bottom: 0;\n}\n.markdown-section ol ol {\n    list-style-type: lower-roman;\n}\n.markdown-section blockquote {\n    margin: 0;\n    margin-bottom: 0.85em;\n    padding: 0 15px;\n    color: #858585;\n    border-left: 4px solid #e5e5e5;\n}\n.markdown-section blockquote:first-child {\n    margin-top: 0;\n}\n.markdown-section blockquote:last-child {\n    margin-bottom: 0;\n}\n.markdown-section dl {\n    padding: 0;\n}\n.markdown-section dl dt {\n    padding: 0;\n    margin-top: 0.85em;\n    font-style: italic;\n    font-weight: 700;\n}\n.markdown-section dl dd {\n    padding: 0 0.85em;\n    margin-bottom: 0.85em;\n}\n.markdown-section dd {\n    margin-left: 0;\n}\n.markdown-section .glossary-term {\n    cursor: help;\n    text-decoration: underline;\n}\n.navigation {\n    position: absolute;\n    top: 50px;\n    bottom: 0;\n    margin: 0;\n    max-width: 150px;\n    min-width: 90px;\n    display: flex;\n    justify-content: center;\n    align-content: center;\n    flex-direction: column;\n    font-size: 40px;\n    color: #ccc;\n    text-align: center;\n    -webkit-transition: all 350ms ease;\n    -moz-transition: all 350ms ease;\n    -o-transition: all 350ms ease;\n    transition: all 350ms ease;\n}\n.navigation:hover {\n    text-decoration: none;\n    color: #444;\n}\n.navigation.navigation-next {\n    right: 0;\n}\n.navigation.navigation-prev {\n    left: 0;\n}\n@media (max-width: 1240px) {\n    .navigation {\n        position: static;\n        top: auto;\n        max-width: 50%;\n        width: 50%;\n        display: inline-block;\n        float: left;\n    }\n    .navigation.navigation-unique {\n        max-width: 100%;\n        width: 100%;\n    }\n}\n.book .book-header .font-settings .font-enlarge {\n    line-height: 30px;\n    font-size: 1.4em;\n}\n.book .book-header .font-settings .font-reduce {\n    line-height: 30px;\n    font-size: 1em;\n}\n.book.font-size-2 .book-body .page-inner section {\n    font-size: 1.6rem;\n}\n.book.font-family-1 {\n    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n}\n* {\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n    -webkit-overflow-scrolling: touch;\n    -webkit-tap-highlight-color: transparent;\n    -webkit-text-size-adjust: none;\n    -webkit-touch-callout: none;\n    -webkit-font-smoothing: antialiased;\n}\na {\n    text-decoration: none;\n}\nbody,\nhtml {\n    height: 100%;\n}\nhtml {\n    font-size: 62.5%;\n}\nbody {\n    text-rendering: optimizeLegibility;\n    font-smoothing: antialiased;\n    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n    font-size: 14px;\n    letter-spacing: 0.2px;\n    text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    -webkit-text-size-adjust: 100%;\n}\n\n.asciicast {\n    width: 676px;\n    margin: 0 auto !important;\n}\n"
  },
  {
    "path": "docs/css/syntax.css",
    "content": ".highlight .c {\n    /* Comment */\n    color: #999988;\n    font-style: italic;\n}\n.highlight .err {\n    /* Error */\n    color: #a61717;\n    background-color: #e3d2d2;\n}\n.highlight .k {\n    /* Keyword */\n    font-weight: bold;\n}\n.highlight .o {\n    /* Operator */\n    font-weight: bold;\n}\n.highlight .cm {\n    /* Comment.Multiline */\n    color: #999988;\n    font-style: italic;\n}\n.highlight .cp {\n    /* Comment.Preproc */\n    color: #999999;\n    font-weight: bold;\n}\n.highlight .c1 {\n    /* Comment.Single */\n    color: #999988;\n    font-style: italic;\n}\n.highlight .cs {\n    /* Comment.Special */\n    color: #999999;\n    font-weight: bold;\n    font-style: italic;\n}\n.highlight .gd {\n    /* Generic.Deleted */\n    color: #000000;\n    background-color: #ffdddd;\n    .highlight .x {\n        /* Generic.Deleted.Specific */\n        color: #000000;\n        background-color: #ffaaaa;\n    }\n}\n.highlight .ge {\n    /* Generic.Emph */\n    font-style: italic;\n}\n.highlight .gr {\n    /* Generic.Error */\n    color: #aa0000;\n}\n.highlight .gh {\n    /* Generic.Heading */\n    color: #999999;\n}\n.highlight .gi {\n    /* Generic.Inserted */\n    color: #000000;\n    background-color: #ddffdd;\n    .highlight .x {\n        /* Generic.Inserted.Specific */\n        color: #000000;\n        background-color: #aaffaa;\n    }\n}\n.highlight .go {\n    /* Generic.Output */\n    color: #888888;\n}\n.highlight .gp {\n    /* Generic.Prompt */\n    color: #555555;\n}\n.highlight .gs {\n    /* Generic.Strong */\n    font-weight: bold;\n}\n.highlight .gu {\n    /* Generic.Subheading */\n    color: #aaaaaa;\n}\n.highlight .gt {\n    /* Generic.Traceback */\n    color: #aa0000;\n}\n.highlight .kc {\n    /* Keyword.Constant */\n    font-weight: bold;\n}\n.highlight .kd {\n    /* Keyword.Declaration */\n    font-weight: bold;\n}\n.highlight .kp {\n    /* Keyword.Pseudo */\n    font-weight: bold;\n}\n.highlight .kr {\n    /* Keyword.Reserved */\n    font-weight: bold;\n}\n.highlight .kt {\n    /* Keyword.Type */\n    color: #445588;\n    font-weight: bold;\n}\n.highlight .m {\n    /* Literal.Number */\n    color: #009999;\n}\n.highlight .s {\n    /* Literal.String */\n    color: #d14;\n}\n.highlight .na {\n    /* Name.Attribute */\n    color: #008080;\n}\n.highlight .nb {\n    /* Name.Builtin */\n    color: #0086B3;\n}\n.highlight .nc {\n    /* Name.Class */\n    color: #445588;\n    font-weight: bold;\n}\n.highlight .no {\n    /* Name.Constant */\n    color: #008080;\n}\n.highlight .ni {\n    /* Name.Entity */\n    color: #800080;\n}\n.highlight .ne {\n    /* Name.Exception */\n    color: #990000;\n    font-weight: bold;\n}\n.highlight .nf {\n    /* Name.Function */\n    color: #990000;\n    font-weight: bold;\n}\n.highlight .nn {\n    /* Name.Namespace */\n    color: #555555;\n}\n.highlight .nt {\n    /* Name.Tag */\n    color: #000080;\n}\n.highlight .nv {\n    /* Name.Variable */\n    color: #008080;\n}\n.highlight .ow {\n    /* Operator.Word */\n    font-weight: bold;\n}\n.highlight .w {\n    /* Text.Whitespace */\n    color: #bbbbbb;\n}\n.highlight .mf {\n    /* Literal.Number.Float */\n    color: #009999;\n}\n.highlight .mh {\n    /* Literal.Number.Hex */\n    color: #009999;\n}\n.highlight .mi {\n    /* Literal.Number.Integer */\n    color: #009999;\n}\n.highlight .mo {\n    /* Literal.Number.Oct */\n    color: #009999;\n}\n.highlight .sb {\n    /* Literal.String.Backtick */\n    color: #d14;\n}\n.highlight .sc {\n    /* Literal.String.Char */\n    color: #d14;\n}\n.highlight .sd {\n    /* Literal.String.Doc */\n    color: #d14;\n}\n.highlight .s2 {\n    /* Literal.String.Double */\n    color: #d14;\n}\n.highlight .se {\n    /* Literal.String.Escape */\n    color: #d14;\n}\n.highlight .sh {\n    /* Literal.String.Heredoc */\n    color: #d14;\n}\n.highlight .si {\n    /* Literal.String.Interpol */\n    color: #d14;\n}\n.highlight .sx {\n    /* Literal.String.Other */\n    color: #d14;\n}\n.highlight .sr {\n    /* Literal.String.Regex */\n    color: #009926;\n}\n.highlight .s1 {\n    /* Literal.String.Single */\n    color: #d14;\n}\n.highlight .ss {\n    /* Literal.String.Symbol */\n    color: #990073;\n}\n.highlight .bp {\n    /* Name.Builtin.Pseudo */\n    color: #999999;\n}\n.highlight .vc {\n    /* Name.Variable.Class */\n    color: #008080;\n}\n.highlight .vg {\n    /* Name.Variable.Global */\n    color: #008080;\n}\n.highlight .vi {\n    /* Name.Variable.Instance */\n    color: #008080;\n}\n.highlight .il {\n    /* Literal.Number.Integer.Long */\n    color: #009999;\n}\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: default\ntitle: 'Documentation'\n---\n\n<p><a href=\"https://badge.fury.io/js/comfygure\"><img class=\"no-margin\" src=\"https://badge.fury.io/js/comfygure.svg\" alt=\"npm version\" /></a> <img  class=\"no-margin\" src=\"https://img.shields.io/david/marmelab/comfygure.svg?label=CLI%20dependencies&amp;path=cli\" alt=\"CLI dependencies\" /> <img  class=\"no-margin\" src=\"https://img.shields.io/david/marmelab/comfygure.svg?label=API%20dependencies&amp;path=api\" alt=\"API dependencies\" /> <a href=\"http://npmjs.com/comfygure\"><img  class=\"no-margin\" src=\"https://img.shields.io/npm/dt/comfygure.svg\" alt=\"npm downloads\" /></a> <a href=\"https://hub.docker.com/r/marmelab/comfygure\"><img  class=\"no-margin\" src=\"https://img.shields.io/docker/pulls/marmelab/comfygure.svg\" alt=\"docker pulls\" /></a> <a href=\"https://travis-ci.org/marmelab/comfygure\"><img  class=\"no-margin\" src=\"https://travis-ci.org/marmelab/comfygure.png?branch=master\" alt=\"Build Status\" /></a></p>\n# comfygure\n\nEncrypted and versioned configuration storage built with collaboration in mind.\n\n<div style=\"text-align: center\" markdown=\"1\">\n<i class=\"octicon octicon-mark-github\"></i> [Source](https://github.com/marmelab/comfygure) -\n<i class=\"octicon octicon-megaphone\"></i> [Releases](https://github.com/marmelab/comfygure/releases) -\n<i class=\"octicon octicon-comment-discussion\"></i> [Stack Overflow](https://stackoverflow.com/questions/tagged/comfy/)\n</div>\n\n<script type=\"text/javascript\" src=\"https://asciinema.org/a/137703.js\" id=\"asciicast-137703\" async></script>\n\n## Features\n\n-   Simple CLI\n-   End-to-end AES-256 encryption\n-   Multiple formats support (JSON, YAML, environment variables)\n-   Git-like Versioning\n-   Easy to host on your own\n\nComfygure is great to manage application configurations for multiple environments, toggle feature flags quickly, manage A/B testing based on configuration files.\n\nIt is not a [Secret Management Tool](https://gist.github.com/maxvt/bb49a6c7243163b8120625fc8ae3f3cd), it focus on configurations files, their history, and how teams collaborate with them.\n\n## Get Started\n\nOn every server that needs access to the settings of an app, install the `comfy` CLI using `npm`:\n\n```bash\nnpm install -g comfygure\ncomfy help\n```\n\n## Usage\n\nInitialize comfygure in a project directory with `comfy init`:\n\n```bash\n> cd myproject\n> comfy init\n\nInitializing project configuration...\nProject created on comfy server https://comfy.marmelab.com\nConfiguration saved locally in .comfy/config\ncomfy project successfully created\n```\n\nThis creates a unique key to access the settings for `myproject`, and stores the key in `.comfy/config`. You can copy this file to share the credentials with co-workers or other computers.\n\n**Note**: By default, the `comfy` command stores encrypted settings in the `comfy.marmelab.com` server. To host your own comfy server, see [the related documentation](https://marmelab.com/comfygure/HostYourOwn.html#host-your-own-comfy-server).\n\nImport an existing settings file to comfygure using `comfy setall`:\n\n```bash\n> echo '{\"login\": \"admin\", \"password\": \"S3cr3T\"}' > config.json\n> comfy setall development config.json\nGreat! Your configuration was successfully saved.\n```\n\nFrom any computer sharing the same credentials, grab these settings using `comfy get`:\n\n```bash\n> comfy get development\n{\"login\": \"admin\", \"password\": \"S3cr3T\"}\n> comfy get development --envvars\nexport LOGIN='admin';\nexport PASSWORD='S3cr3T';\n```\n\nTo turn settings grabbed from comfygure into environment variables, use the following:\n\n```bash\n> comfy get development --envvars | source /dev/stdin\n> echo $LOGIN\nadmin\n```\n\nSee the [documentation](https://marmelab.com/comfygure/) to know more about how it works and the remote usage.\n\n## License\n\nComfygure is licensed under the [MIT License](https://github.com/marmelab/comfygure/blob/master/LICENSE), sponsored and supported by [marmelab](http://marmelab.com).\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"comfygure\",\n  \"version\": \"1.2.0\",\n  \"description\": \"Configure without the pain\",\n  \"main\": \"index.js\",\n  \"private\": true,\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"bin\": {\n    \"comfy\": \"cli/bin/comfy.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/marmelab/comfygure.git\"\n  },\n  \"author\": \"Marmelab\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/marmelab/comfygure/issues\"\n  },\n  \"homepage\": \"https://github.com/marmelab/comfygure#readme\",\n  \"dependencies\": {},\n  \"devDependencies\": {}\n}\n"
  },
  {
    "path": "test/Makefile",
    "content": "export CI ?= false\nexport PM2_HOME ?= .pm2\n\ninstall:\n\tnpm install\n\nrun-test-api:\nifeq (${CI},false)\n\techo 'Starting test database...'\n\t-cd ../api && make install-db\n\tcd ../api && make start-db\nendif\n\t@echo 'Starting test API...'\n\t./node_modules/.bin/pm2 start pm2_configuration.json\n\t@echo 'Waiting for webpack...'\n\t@sleep 10\n\nstop-test-api:\n\t@echo 'Stopping test API...'\n\t./node_modules/.bin/pm2 delete pm2_configuration.json\n\tif [ \"${CI}\" = \"false\" ]; then \\\n\t\techo 'Stopping test database...'; \\\n\t\tcd ../api && make stop-db; \\\n\tfi\n\nsetup-test: run-test-api\n\tmkdir -p ./.env\n\trm -rf ./.env/*\n\nteardown-test: stop-test-api\n\t./node_modules/.bin/pm2 kill\n\ntest: setup-test\n\t@echo 'Starting test suite...'\n\t./node_modules/.bin/mocha --timeout 5000 --require co-mocha \\\n\t\t./setup.js \\\n\t\t'./specs/**/*.js'\n\tmake teardown-test\n\ntest-watch:\n\t@echo 'Starting test suite...'\n\t./node_modules/.bin/mocha --timeout 5000 --require co-mocha --watch \\\n\t\t./setup.js \\\n\t\t'./specs/**/*.js'\n\tmake teardown-test\n\nlogs:\n\t./node_modules/.bin/pm2 logs -f\n"
  },
  {
    "path": "test/cli.js",
    "content": "const { exec } = require('child_process');\n\nconst COMFY_BIN = '../../cli/bin/comfy.js';\nconst DEFAULT_CWD = './.env/';\nconst DEFAULT_ORIGIN = 'http://localhost:3000';\n\nconst run = command => (callback) => {\n    // Replace `comfy` by its binary location\n    const commandToExecute = command.startsWith('comfy')\n        ? `${COMFY_BIN}${command.substr(5)}`\n        : command;\n\n    exec(commandToExecute, { cwd: DEFAULT_CWD }, (error, stdout, stderr) => {\n        if (error) {\n            callback(error);\n            return;\n        }\n\n        callback(null, { stdout, stderr });\n    });\n};\n\nconst createProject = function* () {\n    yield run(`comfy init --origin '${DEFAULT_ORIGIN}'`);\n};\n\nmodule.exports = { run, createProject };\n"
  },
  {
    "path": "test/package.json",
    "content": "{\n  \"name\": \"comfygure-test\",\n  \"private\": true,\n  \"devDependencies\": {\n    \"co-mocha\": \"^1.2.2\",\n    \"expect\": \"^25.2.7\",\n    \"js-yaml\": \"^3.13.1\",\n    \"mocha\": \"^7.1.1\",\n    \"pm2\": \"^4.2.3\"\n  }\n}\n"
  },
  {
    "path": "test/pm2_configuration.json",
    "content": "{\n    \"apps\": [\n        {\n            \"name\": \"comfy-api\",\n            \"cwd\": \"../api\",\n            \"script\": \"node_modules/.bin/serverless\",\n            \"args\": \"offline --host=0.0.0.0 --port=3000\",\n            \"env\": {\n                \"SLS_DEBUG\": \"*\",\n                \"PGPASSWORD\": \"password\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "test/setup.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst deleteFolderContent = function* (dir, deleteFolder = false) {\n    if (fs.existsSync(dir)) {\n        const entries = yield cb => fs.readdir(dir, cb);\n\n        for (const entry of entries) {\n            const entryPath = path.join(dir, entry);\n\n            const stats = yield cb => fs.lstat(entryPath, cb);\n            if (stats.isDirectory()) {\n                yield deleteFolderContent(entryPath, true);\n            } else {\n                yield cb => fs.unlink(entryPath, cb);\n            }\n        }\n\n        if (deleteFolder) {\n            yield cb => fs.rmdir(dir, cb);\n        }\n    }\n};\n\n\nafterEach(function* () {\n    // Delete all .env folder content\n    yield deleteFolderContent('./.env/');\n});\n"
  },
  {
    "path": "test/specs/basicUsage.js",
    "content": "const expect = require('expect');\nconst { run, createProject } = require('../cli');\n\ndescribe('Basic Usages', () => {\n    beforeEach(createProject);\n\n    describe('accessors', () => {\n        it('should be able to add and read a basic JSON file', function* () {\n            const config = { login: 'admin', password: 'S3cret' };\n\n            yield run(`echo '${JSON.stringify(config)}' > test.json`);\n            yield run('comfy setall development test.json');\n\n            const { stdout } = yield run('comfy get development');\n            expect(JSON.parse(stdout)).toEqual(config);\n        });\n\n        it('should retrieve the latest version by default', function* () {\n            const config = { login: 'admin', password: 'S3cret' };\n            const latestConfig = { login: 'admin' };\n\n            yield run(`echo '${JSON.stringify(config)}' > test.json`);\n            yield run('comfy setall development test.json');\n\n            const { stdout } = yield run('comfy get development');\n            expect(JSON.parse(stdout)).toEqual(config);\n\n            yield run(`echo '${JSON.stringify(latestConfig)}' > test.json`);\n            yield run('comfy setall development test.json');\n\n            const { stdout: latestStdout } = yield run('comfy get development');\n            expect(JSON.parse(latestStdout)).toEqual(latestConfig);\n        });\n\n        it('should keep the default format of guessed by the file extension', function* () {\n            const config = 'login: admin';\n\n            yield run(`echo '${config}' > test.yml`);\n            yield run('comfy setall development test.yml');\n\n            const { stdout } = yield run('comfy get development');\n            expect(stdout.trim()).toEqual(config);\n        });\n    });\n\n    describe('environments', () => {\n        it('should be able to list environments', function* () {\n            const { stdout } = yield run('comfy env ls');\n\n            expect(stdout).toContain('development');\n        });\n\n        it('should be able to create an environment', function* () {\n            yield run('comfy env add production');\n            const { stdout } = yield run('comfy env ls');\n\n            expect(stdout).toContain('development');\n            expect(stdout).toContain('production');\n        });\n    });\n\n    describe('log', () => {\n        it('should be able to list all config versions for a given environment', function* () {\n            const { stdout } = yield run('comfy log development');\n\n            const lines = stdout.trim().split('\\n');\n            const [dateStr, environment, configurationSha, tags] = lines[0].split('\\t');\n\n            expect(Date.parse(dateStr)).not.toBeNaN();\n            expect(lines.length).toBe(1);\n            expect(environment).toBe('development');\n            expect(configurationSha.length).toBe(40);\n            expect(tags).toContain('latest');\n        });\n    });\n});\n"
  },
  {
    "path": "test/specs/commands.js",
    "content": "const expect = require(\"expect\");\nconst { run, createProject } = require(\"../cli\");\n\ndescribe(\"Commands\", () => {\n  beforeEach(createProject);\n\n  describe(\"setall\", () => {\n    it(\"should accept relative path for config\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development test.json\");\n\n      const { stdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(stdout)).toEqual(config);\n    });\n\n    it(\"should accept absolute path for config\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      const { stdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(stdout)).toEqual(config);\n    });\n\n    it(\"should display a readable error if the environment does not exist\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n\n      try {\n        yield run(\"comfy setall donotexist $PWD/test.json\");\n      } catch (error) {\n        expect(error.message).toContain(\n          'Unable to find environment \"donotexist\"'\n        );\n        return;\n      }\n\n      expect(\"This command should fail\").toBe(false);\n    });\n  });\n\n  describe(\"get\", () => {\n    it(\"should display a readable error if the environment does not exist\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      try {\n        yield run(\"comfy get donotexist\");\n      } catch (error) {\n        expect(error.message).toContain(\n          'Unable to find environment \"donotexist\"'\n        );\n        return;\n      }\n\n      expect(\"This command should fail\").toBe(false);\n    });\n\n    it(\"should be able to select a subset of the config\", function* () {\n      const config = {\n        admin: \"Admin\",\n        password: \"S3cret!\",\n        nested: {\n          a: {\n            a: 3,\n            b: 4,\n          },\n          b: 2,\n        },\n      };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      const { stdout: admin } = yield run(\"comfy get development admin\");\n      expect(admin.trim()).toBe(\"Admin\");\n\n      const { stdout: nested } = yield run(\"comfy get development nested.a\");\n      expect(JSON.parse(nested)).toEqual({\n        a: 3,\n        b: 4,\n      });\n\n      const { stdout: nestedValue } = yield run(\n        \"comfy get development nested.a.b\"\n      );\n      expect(nestedValue.trim()).toBe(\"4\");\n\n      const { stdout: envvars } = yield run(\n        \"comfy get development nested.a --envvars\"\n      );\n      expect(envvars.trim()).toBe(\n        \"export NESTED_A_A='3';\\nexport NESTED_A_B='4';\"\n      );\n    });\n\n    it(\"should be able to select a subset of the config with an uppercase selector\", function* () {\n      const config = {\n        version: 3,\n        id: \"id\",\n        address: \"address\",\n        Crypto: {\n          ciphertext: \"ciphertext\",\n          cipherparams: { iv: \"iv\" },\n          cipher: \"cipher\",\n        },\n      };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      const { stdout: cipher } = yield run(\n        \"comfy get development Crypto.cipher\"\n      );\n      expect(cipher).toBe(\"cipher\\n\");\n    });\n\n    it(\"should get config with hash name\", function* () {\n      const configV1 = { version: \"1\", login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(configV1)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      const { stdout } = yield run(\"comfy log development\");\n\n      const lines = stdout.trim().split(\"\\n\");\n      const [dateStr, environment, configurationSha, tags] = lines[0].split(\n        \"\\t\"\n      );\n\n      const configV2 = {\n        version: \"2\",\n        login: \"super-admin\",\n        password: \"S3cret\",\n      };\n\n      yield run(`echo '${JSON.stringify(configV2)}' > test.json`);\n      yield run(\"comfy setall development $PWD/test.json\");\n\n      const { stdout: version } = yield run(\n        `comfy get development --hash=${configurationSha} version`\n      );\n\n      expect(version).toContain(\"1\");\n    });\n  });\n\n  describe(\"set\", () => {\n    it(\"should change the value of a direct entry of the config\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development test.json\");\n\n      const { stdout: originalStdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(originalStdout)).toEqual(config);\n\n      yield run(\"comfy set development login user\");\n\n      const expectedConfig = { login: \"user\", password: \"S3cret\" };\n      const { stdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(stdout)).toEqual(expectedConfig);\n    });\n\n    it(\"should change the value of a subset of the config\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development test.json\");\n\n      const { stdout: originalStdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(originalStdout)).toEqual(config);\n\n      yield run(\"comfy set development nested.test yolo\");\n\n      const expectedConfig = {\n        login: \"admin\",\n        password: \"S3cret\",\n        nested: { test: \"yolo\" },\n      };\n      const { stdout } = yield run(\"comfy get development\");\n      expect(JSON.parse(stdout)).toEqual(expectedConfig);\n    });\n\n    it(\"should display a readable error if the environment does not exist\", function* () {\n      try {\n        yield run(\"comfy set donotexist login user\");\n      } catch (error) {\n        expect(error.message).toContain(\n          'Unable to find environment \"donotexist\"'\n        );\n        return;\n      }\n\n      expect(\"This command should fail\").toBe(false);\n    });\n  });\n\n  describe(\"project\", () => {\n    it(\"should allow to permanently delete the current project\", function* () {\n      const { stdout: warning } = yield run(\"comfy project delete\");\n      expect(warning).toContain(\"This action is irreversible\");\n\n      const { stdout: currentConfig } = yield run(\"comfy get development\");\n      expect(JSON.parse(currentConfig)).toEqual({});\n\n      const { stdout: projectId } = yield run(\n        'cat .comfy/config | grep projectId | sed \"s/projectId=//\"'\n      );\n\n      const { stdout: confirmation } = yield run(\n        `comfy project delete --permanently --id=${projectId}`\n      );\n      expect(confirmation).toContain(\"successfully deleted\");\n\n      try {\n        yield run(\"comfy get development\");\n      } catch (error) {\n        expect(error.message).toContain(\n          \"Unable to locate the project identifier\"\n        );\n        return;\n      }\n\n      expect(\"The last command should not work\").toBe(false);\n    });\n  });\n\n  const parseHash = (line) => line.split(\"\\t\")[2];\n\n  describe(\"tag\", () => {\n    it(\"should allow to list tags\", function* () {\n      const { stdout } = yield run(\"comfy tag list development\");\n\n      expect(stdout).toContain(\"latest\");\n    });\n\n    it(\"should allow to create a new tag\", function* () {\n      const { stdout: log } = yield run(\"comfy tag list development\");\n      const hash = parseHash(log);\n\n      const { stdout: success } = yield run(\n        `comfy tag add development newtag ${hash}`\n      );\n      expect(success).toContain(\"Tag successfully created\");\n\n      const { stdout } = yield run(\"comfy tag list development\");\n      expect(stdout).toContain(`${hash}\\tlatest, newtag`);\n    });\n\n    it(\"should allow to move an existing tag\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development test.json\");\n\n      const { stdout: log } = yield run(\"comfy log development\");\n      const [firstHash, secondHash] = log.split(\"\\n\").map(parseHash);\n\n      yield run(`comfy tag add development newtag ${firstHash}`);\n      const { stdout: addedTag } = yield run(\"comfy tag list development\");\n      expect(addedTag).toContain(`${firstHash}\\tlatest, newtag`);\n\n      const { stdout: success } = yield run(\n        `comfy tag move development newtag ${secondHash}`\n      );\n      expect(success).toContain(\"Tag successfully moved\");\n\n      const { stdout: movedTag } = yield run(\"comfy tag list development\");\n      expect(movedTag).toContain(`${firstHash}\\tlatest`);\n      expect(movedTag).toContain(`${secondHash}\\tnewtag`);\n    });\n\n    it(\"should not allow to move the `latest` tag\", function* () {\n      const config = { login: \"admin\", password: \"S3cret\" };\n\n      yield run(`echo '${JSON.stringify(config)}' > test.json`);\n      yield run(\"comfy setall development test.json\");\n\n      const { stdout: log } = yield run(\"comfy log development\");\n      const [firstHash, secondHash] = log.split(\"\\n\").map(parseHash);\n      expect(log).toContain(`${firstHash}\\tlatest`);\n\n      try {\n        yield run(`comfy tag move development latest ${secondHash}`);\n        fail();\n      } catch (error) {\n        /* expected to fail */\n      }\n\n      const { stdout } = yield run(\"comfy tag list development\");\n      expect(stdout).toContain(`${firstHash}\\tlatest`);\n    });\n\n    it(\"should allow to delete a tag\", function* () {\n      const { stdout: log } = yield run(\"comfy tag list development\");\n      const hash = parseHash(log);\n\n      const { stdout: success } = yield run(\n        `comfy tag add development newtag ${hash}`\n      );\n      expect(success).toContain(\"Tag successfully created\");\n\n      const { stdout: newTag } = yield run(\"comfy tag list development\");\n      expect(newTag).toContain(`${hash}\\tlatest, newtag`);\n\n      const { stdout: deletedTag } = yield run(\n        \"comfy tag delete development newtag\"\n      );\n      expect(deletedTag).toContain(\"Tag successfully deleted\");\n\n      const { stdout } = yield run(\"comfy tag list development\");\n      expect(stdout).toContain(`${hash}\\tlatest`);\n      expect(stdout).not.toContain(`${hash}\\tlatest, newtag`);\n    });\n\n    it(\"should not allow to delete the `latest` tag\", function* () {\n      const { stdout: log } = yield run(\"comfy tag list development\");\n      const hash = parseHash(log);\n      expect(log).toContain(`${hash}\\tlatest`);\n\n      try {\n        yield run(\"comfy tag delete development latest\");\n        fail();\n      } catch (error) {\n        /* expected to fail */\n      }\n\n      const { stdout } = yield run(\"comfy tag list development\");\n      expect(stdout).toContain(`${hash}\\tlatest`);\n    });\n  });\n\n  describe(\"diff\", () => {\n    it(\"should display a git-like diff between two configuration versions\", function* () {\n      yield run(\n        `echo '${JSON.stringify({\n          login: \"admin\",\n          password: \"S3cret\",\n        })}' > test.json`\n      );\n      yield run(\"comfy setall development test.json\");\n      yield run(\"comfy set development login user\");\n\n      const { stdout } = yield run(\"comfy get development login\");\n      expect(stdout).toEqual(\"user\\n\");\n\n      const { stdout: history } = yield run(\"comfy log development\");\n\n      const lines = history.trim().split(\"\\n\");\n      const [dateStr, environment, hash] = lines[1].split(\"\\t\");\n\n      const { stdout: diff } = yield run(`comfy diff development ${hash}`);\n      expect(diff).toContain('-    \"login\": \"admin\",');\n      expect(diff).toContain('+    \"login\": \"user\",');\n    });\n  });\n\n  describe(\"token\", () => {\n    it(\"should be able to create a new token\", function* () {\n      const { stdout } = yield run(\"comfy token add new-token\");\n      expect(stdout).toContain(\"Token successfully created\");\n    });\n\n    it(\"should be able to list all tokens\", function* () {\n      yield run(\"comfy token add new-token --expires-in 2\");\n      const { stdout } = yield run(\"comfy token list\");\n\n      const [firstLine, secondLine] = stdout.split(\"\\n\");\n      expect(firstLine).toContain(\"root\");\n      expect(firstLine).toContain(\"full permissions\");\n      expect(firstLine).toContain(\"active (never expire)\");\n      expect(secondLine).toContain(\"new-token\");\n      expect(secondLine).toContain(\"read only\");\n      expect(secondLine).toContain(\"active (expires in 2 days)\");\n    });\n  });\n\n  it(\"should be able to delete a token\", function* () {\n    yield run(\"comfy token add new-token --expires-in 2\");\n    const { stdout: list } = yield run(\"comfy token list\");\n\n    expect(list.split(\"\\n\").length).toBe(3); // 2 tokens + CRLF\n\n    const { stdout: success } = yield run(\"comfy token delete new-token\");\n    expect(success).toContain(\"Token successfully deleted\");\n\n    const { stdout: updatedList } = yield run(\"comfy token list\");\n    const [firstLine, secondLine] = updatedList.split(\"\\n\");\n    expect(firstLine).toContain(\"root\");\n    expect(firstLine).toContain(\"full permissions\");\n    expect(firstLine).toContain(\"active (never expire)\");\n    expect(secondLine).toBe(\"\");\n  });\n});\n"
  },
  {
    "path": "test/specs/formats.js",
    "content": "const expect = require('expect');\nconst yaml = require('js-yaml');\nconst { run, createProject } = require('../cli');\n\ndescribe('Formats', () => {\n    const config = { login: 'admin', password: 'S3cret' };\n\n    beforeEach(createProject);\n\n    beforeEach(function* () {\n        yield run(`echo '${JSON.stringify(config)}' > test.json`);\n        yield run('comfy setall development test.json');\n    });\n\n    describe('JSON', () => {\n        it('should print valid JSON format', function* () {\n            const { stdout } = yield run('comfy get development');\n\n            expect(JSON.parse(stdout)).toEqual(config);\n        });\n    });\n\n    describe('YAML', () => {\n        it('it should print valid YAML format', function* () {\n            const { stdout } = yield run('comfy get development --yml');\n\n            expect(yaml.safeLoad(stdout)).toEqual(config);\n        });\n    });\n\n    describe('Environment variables', () => {\n        it('it should print valid env. vars format', function* () {\n            const { stdout } = yield run('comfy get development --envvars');\n            const [admin, secret] = stdout.split('\\n');\n\n            expect(admin).toBe(\"export LOGIN='admin';\");\n            expect(secret).toBe(\"export PASSWORD='S3cret';\");\n        });\n    });\n});\n"
  },
  {
    "path": "test/specs/init.js",
    "content": "const expect = require('expect');\nconst run = require('../cli').run;\n\ndescribe('Project initialization', () => {\n    it('should create a new project', function*() {\n        const { stdout } = yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        expect(stdout).toContain('http://localhost:3000');\n    });\n\n    it('should create a new environment', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('comfy env ls');\n\n        expect(stdout).toContain('development');\n    });\n\n    it('should add the origin to `.comfy/config`', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('http://localhost:3000');\n    });\n\n    it('should create project ID', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('projectId=');\n    });\n\n    it('should create access key', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('accessKey=');\n    });\n\n    it('should create secret token', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('secretToken=');\n    });\n\n    it('should create private key', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('privateKey=');\n    });\n\n    it('should create a HMAC signature key', function*() {\n        yield run(\"comfy init --origin 'http://localhost:3000'\");\n\n        const { stdout } = yield run('cat .comfy/config');\n        expect(stdout).toContain('hmacKey=');\n    });\n\n    it('should be able to init even if the folder .comfy already exist', function*() {\n        yield run('mkdir .comfy');\n        const { stderr } = yield run(\"comfy init --origin 'http://localhost:3000'\");\n        expect(stderr).toBe('');\n\n        const { stdout } = yield run('ls .comfy/');\n        expect(stdout).toContain('config');\n    });\n});\n"
  },
  {
    "path": "test/specs/scenarios.js",
    "content": "const expect = require('expect');\nconst { run, createProject } = require('../cli');\n\ndescribe('Scenarios', () => {\n    beforeEach(createProject);\n\n    describe('Child Entries', () => {\n        it('should be able to delete a child entry with a new configuration', function*() {\n            const firstConfig = {\n                parent: { entry: 'entryToDelete' }\n            };\n\n            yield run(`echo '${JSON.stringify(firstConfig)}' > firstConfig.json`);\n            yield run('comfy setall development firstConfig.json');\n\n            const { stdout: firstConfigOutput } = yield run('comfy get development');\n            expect(JSON.parse(firstConfigOutput)).toEqual(firstConfig);\n\n            const secondConfig = {\n                parent: { child: { entry: 'entry' } },\n                child: { entry: 'entry' }\n            };\n\n            yield run(`echo '${JSON.stringify(secondConfig)}' > secondConfig.json`);\n            yield run('comfy setall development secondConfig.json');\n\n            const { stdout: secondConfigOutput } = yield run('comfy get development');\n            expect(JSON.parse(secondConfigOutput)).toEqual(secondConfig);\n        });\n    });\n\n    describe('Serialization', () => {\n        it('should not transform a `false` bool to a `\"false\"` string', function*() {\n            const config = { myEntry: false };\n\n            yield run(`echo '${JSON.stringify(config)}' > config.json`);\n            yield run('comfy setall development config.json');\n\n            const { stdout } = yield run('comfy get development');\n            expect(JSON.parse(stdout)).toEqual(config);\n        });\n\n        it('should not transform a `null` into something else', function*() {\n            const config = { myEntry: null };\n\n            yield run(`echo '${JSON.stringify(config)}' > config.json`);\n            yield run('comfy setall development config.json');\n\n            const { stdout } = yield run('comfy get development');\n            expect(JSON.parse(stdout)).toEqual(config);\n        });\n    });\n});\n"
  }
]