├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_TEMPLATE.md │ └── QUESTION_TEMPLATE.md ├── pull_request_template.md └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs └── LogoGreen.png ├── fixtures └── binary.txt ├── jest.config.js ├── package.json ├── scripts └── check_dependencies.js ├── src ├── core │ ├── client │ │ ├── client.internal.test.ts │ │ ├── client.internal.ts │ │ └── index.ts │ ├── config │ │ ├── config.test.ts │ │ ├── config.ts │ │ └── index.ts │ ├── errors │ │ ├── errors.helpers.ts │ │ ├── errors.test.ts │ │ ├── fetch.errors.ts │ │ ├── filesystem.errors.ts │ │ └── index.ts │ ├── events │ │ ├── events.test.ts │ │ ├── events.ts │ │ ├── failure │ │ │ ├── backoff.test.ts │ │ │ ├── backoff.ts │ │ │ ├── event-adapter.test.ts │ │ │ ├── event-adapter.ts │ │ │ ├── index.ts │ │ │ ├── map-interpreter-adapter.ts │ │ │ ├── policies.test.ts │ │ │ ├── policies.ts │ │ │ ├── policy.ts │ │ │ └── resolution.ts │ │ ├── index.ts │ │ └── reporter │ │ │ ├── index.ts │ │ │ ├── reporter.test.ts │ │ │ ├── reporter.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ ├── index.ts │ ├── interpreter │ │ ├── external-handler.ts │ │ ├── http │ │ │ ├── filters.test.ts │ │ │ ├── filters.ts │ │ │ ├── http.test.ts │ │ │ ├── http.ts │ │ │ ├── index.ts │ │ │ ├── interfaces.ts │ │ │ ├── security │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── api-key.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── digest │ │ │ │ │ ├── digest.test.ts │ │ │ │ │ ├── digest.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── http │ │ │ │ │ ├── http.test.ts │ │ │ │ │ ├── http.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── types.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── map-interpreter.errors.test.ts │ │ ├── map-interpreter.errors.ts │ │ ├── map-interpreter.test.ts │ │ ├── map-interpreter.ts │ │ ├── profile-parameter-validator.errors.ts │ │ ├── profile-parameter-validator.test.ts │ │ ├── profile-parameter-validator.ts │ │ └── stdlib │ │ │ └── index.ts │ ├── profile-provider │ │ ├── bound-profile-provider.test.ts │ │ ├── bound-profile-provider.ts │ │ ├── index.ts │ │ ├── parameters.test.ts │ │ ├── parameters.ts │ │ ├── profile-provider-configuration.ts │ │ ├── profile-provider.test.ts │ │ ├── profile-provider.ts │ │ ├── resolve-map-ast.test.ts │ │ ├── resolve-map-ast.ts │ │ ├── security.test.ts │ │ └── security.ts │ ├── profile │ │ ├── cache-profile-ast.test.ts │ │ ├── cache-profile-ast.ts │ │ ├── index.ts │ │ ├── profile-configuration.ts │ │ ├── profile.test.ts │ │ ├── profile.ts │ │ ├── profile.typed.test.ts │ │ ├── profile.typed.ts │ │ ├── resolve-profile-ast.test.ts │ │ └── resolve-profile-ast.ts │ ├── provider │ │ ├── index.ts │ │ ├── provider.test.ts │ │ ├── provider.ts │ │ ├── resolve-provider-json.test.ts │ │ ├── resolve-provider-json.ts │ │ ├── resolve-provider.test.ts │ │ └── resolve-provider.ts │ ├── registry │ │ ├── index.ts │ │ ├── registry.test.ts │ │ └── registry.ts │ ├── sandbox │ │ └── index.ts │ ├── services │ │ ├── index.ts │ │ └── services.ts │ └── usecase │ │ ├── index.ts │ │ ├── usecase.test.ts │ │ ├── usecase.ts │ │ ├── usecase.typed.ts │ │ └── utils.ts ├── index.ts ├── interfaces │ ├── binary.ts │ ├── client.ts │ ├── config.ts │ ├── crypto.ts │ ├── environment.ts │ ├── errors │ │ ├── filesystem.errors.ts │ │ ├── index.ts │ │ ├── map-interpreter.errors.ts │ │ ├── profile-parameter-validator.errors.ts │ │ └── usecase.errors.ts │ ├── events.ts │ ├── filesystem.ts │ ├── index.ts │ ├── logger.ts │ ├── profile.ts │ ├── provider.ts │ ├── sandbox.ts │ ├── timers.ts │ └── usecase.ts ├── lib │ ├── cache │ │ ├── cache.ts │ │ └── index.ts │ ├── config-hash │ │ ├── config-hash.ts │ │ └── index.ts │ ├── env │ │ ├── env.test.ts │ │ ├── env.ts │ │ └── index.ts │ ├── error │ │ ├── error.ts │ │ └── index.ts │ ├── index.ts │ ├── object │ │ ├── index.ts │ │ ├── object.test.ts │ │ └── object.ts │ ├── pipe │ │ ├── index.ts │ │ ├── pipe.test.ts │ │ └── pipe.ts │ ├── result │ │ ├── result.test.ts │ │ └── result.ts │ ├── types │ │ ├── index.ts │ │ └── types.ts │ ├── utils │ │ ├── index.ts │ │ └── utils.ts │ └── variables │ │ ├── index.ts │ │ ├── variables.test.ts │ │ └── variables.ts ├── mock │ ├── client.ts │ ├── environment.ts │ ├── filesystem.ts │ ├── index.ts │ ├── map-document-node.ts │ ├── profile-document-node.ts │ ├── provider-json.ts │ └── timers.ts ├── module.d.ts ├── node │ ├── client │ │ ├── client.integration.test.ts │ │ ├── client.test.ts │ │ ├── client.ts │ │ ├── client.typed.ts │ │ └── index.ts │ ├── crypto │ │ ├── crypto.node.ts │ │ └── index.ts │ ├── environment │ │ ├── environment.node.test.ts │ │ ├── environment.node.ts │ │ └── index.ts │ ├── fetch │ │ ├── fetch.node.test.ts │ │ ├── fetch.node.ts │ │ └── index.ts │ ├── filesystem │ │ ├── binary.node.test.ts │ │ ├── binary.node.ts │ │ ├── filesystem.node.test.ts │ │ ├── filesystem.node.ts │ │ └── index.ts │ ├── index.ts │ ├── logger │ │ ├── index.ts │ │ └── logger.node.ts │ ├── sandbox │ │ ├── index.ts │ │ ├── sandbox.node.test.ts │ │ └── sandbox.node.ts │ └── timers │ │ ├── index.ts │ │ └── timers.node.ts ├── private │ └── index.ts ├── schema-tools │ ├── index.ts │ └── superjson │ │ ├── errors.helpers.ts │ │ ├── index.ts │ │ ├── mutate.test.ts │ │ ├── mutate.ts │ │ ├── normalize.test.ts │ │ ├── normalize.ts │ │ ├── superjson.integration.test.ts │ │ ├── superjson.ts │ │ ├── utils.test.ts │ │ └── utils.ts └── user-agent.ts ├── tsconfig.json ├── tsconfig.release.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | tsconfigRootDir: __dirname, 6 | project: ['./tsconfig.json'], 7 | }, 8 | plugins: [ 9 | '@typescript-eslint', 10 | 'prettier', 11 | 'jest', 12 | 'simple-import-sort', 13 | 'jest-formatting', 14 | ], 15 | extends: [ 16 | 'eslint:recommended', 17 | 'plugin:@typescript-eslint/eslint-recommended', 18 | 'plugin:@typescript-eslint/recommended', 19 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 20 | 'plugin:import/errors', 21 | 'plugin:import/warnings', 22 | 'plugin:jest/recommended', 23 | 'plugin:jest-formatting/recommended', 24 | 'plugin:prettier/recommended' 25 | ], 26 | rules: { 27 | 'newline-before-return': 'error', 28 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 29 | 'simple-import-sort/imports': 'error', 30 | 'sort-imports': 'off', 31 | 'import/first': 'error', 32 | 'import/newline-after-import': 'error', 33 | 'import/no-duplicates': 'error', 34 | 'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.ts', 'src/mock/**']}], 35 | 'no-multiple-empty-lines': 'error', 36 | 'lines-between-class-members': 'off', 37 | '@typescript-eslint/lines-between-class-members': [ 38 | 'error', 39 | 'always', 40 | { exceptAfterSingleLine: true, exceptAfterOverload: true }, 41 | ], 42 | '@typescript-eslint/no-empty-function': 'off', 43 | '@typescript-eslint/require-await': 'off', 44 | 'spaced-comment': ['error', 'always'], 45 | quotes: [ 46 | 'error', 47 | 'single', 48 | { avoidEscape: true, allowTemplateLiterals: false }, 49 | ], 50 | 'no-implicit-coercion': 'error', 51 | '@typescript-eslint/strict-boolean-expressions': 'error', 52 | '@typescript-eslint/explicit-member-accessibility': [ 53 | 'error', 54 | { accessibility: 'explicit', overrides: { constructors: 'no-public' } }, 55 | ], 56 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], 57 | }, 58 | settings: { 59 | 'import/parsers': { 60 | '@typescript-eslint/parser': ['.ts'], 61 | }, 62 | 'import/resolver': { 63 | typescript: { 64 | alwaysTryTypes: true, 65 | }, 66 | }, 67 | }, 68 | overrides: [ 69 | { 70 | files: '*.test.ts', 71 | rules: { 72 | '@typescript-eslint/no-explicit-any': 'off', 73 | '@typescript-eslint/no-unsafe-assignment': 'off', 74 | '@typescript-eslint/no-unsafe-member-access': 'off', 75 | '@typescript-eslint/no-non-null-assertion': 'off', 76 | '@typescript-eslint/no-unsafe-argument': 'off' 77 | }, 78 | }, 79 | ], 80 | }; 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this template for reporting a bug. 4 | title: "[BUG]" 5 | labels: "Type: Bug" 6 | --- 7 | 8 | Provide a general summary of the issue in the Title above 9 | 10 | ## Expected Behavior 11 | Tell us what should happen 12 | 13 | ## Current Behavior 14 | Tell us what happens instead of the expected behavior 15 | 16 | ## Possible Solution 17 | Not obligatory, but suggest a fix/reason for the bug 18 | 19 | ## Steps to Reproduce 20 | Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant 21 | 1. 22 | 2. 23 | 3. 24 | 4. 25 | 26 | ## Your Environment 27 | Include as many relevant details about the environment you experienced the bug in. Preferably include: 28 | * Version used 29 | * Environment name and version (e.g. Node 8) 30 | * Operating System and version 31 | * Link to your project 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Use this template for asking a Question. 4 | title: "[QUESTION]" 5 | labels: "Type: Question" 6 | --- 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## Types of changes 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 15 | 16 | ## Checklist: 17 | 18 | 19 | 20 | - [ ] I have updated the documentation accordingly. If you are changing **code related to user secrets** you need to really make sure that [security documentation](https://github.com/superfaceai/one-sdk-js/blob/main/SECURITY.md) is correct. 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/superfaceai/one-sdk-js/blob/main/CONTRIBUTING.md) document. 22 | - [ ] I haven't repeated the code. (DRY) 23 | - [ ] I have added tests to cover my changes. 24 | - [ ] All new and existing tests passed. 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node: [14, 16, 18] 11 | steps: 12 | # Setup environment and checkout the project master 13 | - name: Setup Node.js environment 14 | uses: actions/setup-node@v2 15 | with: 16 | registry-url: https://registry.npmjs.org/ 17 | scope: '@superfaceai' 18 | node-version: ${{ matrix.node }} 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v2.3.4 22 | 23 | # Setup yarn cache 24 | - name: Get yarn cache directory path 25 | id: yarn-cache-dir-path 26 | run: echo "::set-output name=dir::$(yarn cache dir)" 27 | - uses: actions/cache@v2.1.3 28 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 29 | with: 30 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 31 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-yarn- 34 | 35 | # Install and run tests 36 | - name: Install dependencies 37 | run: yarn install 38 | - name: Test 39 | run: yarn test --bail 40 | 41 | lint: 42 | runs-on: ubuntu-latest 43 | steps: 44 | # Setup environment and checkout the project master 45 | - name: Setup Node.js environment 46 | uses: actions/setup-node@v2 47 | with: 48 | registry-url: https://registry.npmjs.org/ 49 | scope: '@superfaceai' 50 | node-version: '16' 51 | 52 | - name: Checkout 53 | uses: actions/checkout@v2.3.4 54 | 55 | # Setup yarn cache 56 | - name: Get yarn cache directory path 57 | id: yarn-cache-dir-path 58 | run: echo "::set-output name=dir::$(yarn cache dir)" 59 | - uses: actions/cache@v2.1.3 60 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 61 | with: 62 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 63 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 64 | restore-keys: | 65 | ${{ runner.os }}-yarn- 66 | 67 | # Install and run lint 68 | - name: Install dependencies 69 | run: yarn install 70 | - name: Lint 71 | run: yarn lint 72 | 73 | license-check: 74 | runs-on: ubuntu-latest 75 | steps: 76 | # Setup environment and checkout the project master 77 | - name: Setup Node.js environment 78 | uses: actions/setup-node@v2 79 | with: 80 | registry-url: https://registry.npmjs.org/ 81 | scope: '@superfaceai' 82 | node-version: '16' 83 | 84 | - name: Checkout 85 | uses: actions/checkout@v2.3.4 86 | 87 | # Setup yarn cache 88 | - name: Get yarn cache directory path 89 | id: yarn-cache-dir-path 90 | run: echo "::set-output name=dir::$(yarn cache dir)" 91 | - uses: actions/cache@v2.1.3 92 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 93 | with: 94 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 95 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 96 | restore-keys: | 97 | ${{ runner.os }}-yarn- 98 | 99 | # Install and run license checker 100 | - name: Install dependencies 101 | run: yarn install 102 | - name: Install License checker 103 | run: | 104 | yarn global add license-checker 105 | echo "$(yarn global bin)" >> $GITHUB_PATH 106 | - name: Check licenses 107 | run: "license-checker --onlyAllow '0BDS;MIT;Apache-2.0;ISC;BSD-3-Clause;BSD-2-Clause;CC-BY-4.0;CC-BY-3.0;BSD;CC0-1.0;Unlicense;Python-2.0' --summary" 108 | 109 | dependency-check: 110 | runs-on: ubuntu-latest 111 | steps: 112 | - name: Setup Node.js environment 113 | uses: actions/setup-node@v2 114 | with: 115 | registry-url: https://registry.npmjs.org/ 116 | scope: '@superfaceai' 117 | node-version: '16' 118 | 119 | - name: Checkout 120 | uses: actions/checkout@v2.3.4 121 | 122 | # Setup yarn cache 123 | - name: Get yarn cache directory path 124 | id: yarn-cache-dir-path 125 | run: echo "::set-output name=dir::$(yarn cache dir)" 126 | - uses: actions/cache@v2.1.3 127 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 128 | with: 129 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 130 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 131 | restore-keys: | 132 | ${{ runner.os }}-yarn- 133 | 134 | # Install and run dependency checker 135 | - name: Check dependencies 136 | run: yarn check_dependencies 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .yalc 107 | .vscode 108 | workdir/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: "as-needed", 8 | trailingComma: "es5", 9 | bracketSpacing: true, 10 | arrowParens: "avoid", 11 | endOfLine: "lf", 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Superface OneSDK 2 | 3 | We welcome contributions to [OneSDK on GitHub](https://github.com/superfaceai/one-sdk-js). 4 | 5 | **Please open an issue first if you want to make larger changes** 6 | 7 | ## Introduction 8 | 9 | We are glad that you are interested in Superface in the way of contributing. We value the pro-community developers as you are. 10 | 11 | ## Help the community 12 | 13 | 1. Report an Error or a Bug 14 | 2. Contribute to the Documentation 15 | 3. Provide Support on Issues 16 | 17 | ## Need help? 18 | 19 | If you have any question about this project (for example, how to use it) or if you just need some clarification about anything, please [open an issue](https://github.com/superfaceai/one-sdk-js/issues/new/choose) or check the [Support page](https://superface.ai/support) for other ways how to reach us. 20 | 21 | ## Contributing 22 | 23 | 24 | ### Contribute code 25 | 26 | Follow these steps: 27 | 28 | 1. **Fork & Clone** the repository 29 | 2. **Setup** the OneSDK 30 | - Install packages with `yarn install` or `npm install` 31 | - Build with `yarn build` or `npm run build` 32 | - Run tests with `yarn test` or `npm test` 33 | - Lint code with `yarn lint:fix` or `npm run lint:fix` 34 | 3. **Update** [CHANGELOG](CHANGELOG.md). See [Keep a Changelog](https://keepachangelog.com/). 35 | 4. **Commit** changes to your own branch by convention. See [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 36 | 5. **Push** your work back up to your fork. 37 | 6. Submit a **Pull Request** so that we can review your changes. 38 | 39 | **NOTE:** Be sure to merge the latest commits from "upstream" before making a pull request. 40 | 41 | **NOTE:** Please open an issue first if you want to make larger changes. 42 | 43 | ### Contribute by reporting bugs 44 | 45 | If you are experiencing bug or undocumented behavior please [open an issue](https://github.com/superfaceai/one-sdk-js/issues/new/choose) with Bug report template. 46 | 47 | ### Contribute to documentation 48 | 49 | Help us improve Superface documentation, you can fix typos, improve examples and more. 50 | 51 | The documentation inside the OneSDK repository should be kept to minimum. The [Superface documentation](https://superface.ai/docs) is hosted in the [docs repository](https://github.com/superfaceai/docs). 52 | 53 | ## Allowed Licenses 54 | 55 | Licenses of `node_modules` are checked during push CI/CD for every commit. Only the following licenses are allowed: 56 | 57 | - 0BDS 58 | - MIT 59 | - Apache-2.0 60 | - ISC 61 | - BSD-3-Clause 62 | - BSD-2-Clause 63 | - CC-BY-4.0 64 | - CC-BY-3.0;BSD 65 | - CC0-1.0 66 | - Unlicense 67 | 68 | If a new dependency requires another license, just mention it in the respective issue or pull request, we will allow new licenses on the case-by-case basis. 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Superface s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security within Superface OneSDK 2 | 3 | OneSDK interprets the map to fulfill the required usecase. This often requires making HTTP requests with authorization. Thus the user needs to provide Superface with the right secrets to access the capability exposed by the API. 4 | 5 | _Note: If any of the following links are out of date, please file an issue or open a PR and link to the relevant places again._ 6 | 7 | ## The purpose of secrets 8 | 9 | The only purpose of user secrets within superface is to authorize the user against the API. Superface does not use the secrets in any other way. 10 | 11 | ## Providing user secrets 12 | 13 | Secrets can be provided to the SDK in the following ways: 14 | * Environment variable described in super.json (recommended) 15 | * Raw secret embedded in super.json 16 | * Secrets passed directly by the calling code 17 | 18 | ## How the SDK transports secrets 19 | 20 | Secrets are transported within SDK along multiple paths, eventually being merged into security configuration that is later used. 21 | 22 | The main path of secrets into the SDK is by super.json. When the super.json file is loaded by the SDK (when a new SuperfaceClient instance is created for the first time, or manually) and [normalized](https://github.com/superfaceai/one-sdk-js/blob/master/src/internal/superjson.ts#L557), the environment variables are resolved. 23 | 24 | The secrets are read from the normalized form of super.json either by the Provider API or by a more low-level ProfileProvider API, depending on the calling code. Both of these APIs also provide ways to directly pass secrets from the calling code. 25 | 26 | The Provider API handles secrets when: 27 | * [requesting a Provider](https://github.com/superfaceai/one-sdk-js/blob/master/src/client/public/client.ts#L65) 28 | * [configuring a Provider](https://github.com/superfaceai/one-sdk-js/blob/master/src/client/public/provider.ts#L23) 29 | 30 | Even when the calling code does not explicitly request a provider it is requested implicitly when performing a usecase. 31 | 32 | The ProfileProvider API handles secrets inside the [bind](https://github.com/superfaceai/one-sdk-js/blob/master/src/client/query/profile-provider.ts#L161) method. This method [merges](https://github.com/superfaceai/one-sdk-js/blob/master/src/client/query/profile-provider.ts#L447) security configuration either from Provider API or from normalized super.json, and optionally from bind configuration with provider information. This creates SecurityConfiguration, which is passes into the MapInterpreter. 33 | 34 | The MapInterpreter only [passes](https://github.com/superfaceai/one-sdk-js/blob/master/src/internal/interpreter/map-interpreter.ts#L282) the security configuration into the HttpClient. 35 | 36 | ## How the SDK uses secrets 37 | 38 | HttpClient applies resolved secrets according to security requirements specified in the relevant map. These secrets are applied to the request body right before the request is executed. 39 | 40 | The secrets are [found](https://github.com/superfaceai/one-sdk-js/blob/master/src/internal/http/http.ts#L202) based on security requirements. They are then [applied](https://github.com/superfaceai/one-sdk-js/blob/master/src/internal/http/http.ts#L219) using the [application functions](https://github.com/superfaceai/one-sdk-js/blob/master/src/internal/http/security.ts). 41 | 42 | Once the request is executed no secrets are accessed until another request is to be prepared. 43 | 44 | ## Logging 45 | 46 | Another aspect to consider is logging. Logging is **disabled** by default. When enabled, logging may leak user secrets, so an appropriate logging level should be selected to only expose as much information as is secure in given context. 47 | 48 | To disable logging of secrets set the `DEBUG` variable to a pattern to match requested logging namespaces (as normal) and append the string `,-*:sensitive` to it (e.g. `DEBUG=*,-*:sensitive` to log everything but sensitive namespaces). More information about how logging works can be found in the description of the [debug package](https://www.npmjs.com/package/debug). 49 | -------------------------------------------------------------------------------- /docs/LogoGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaceai/one-sdk-js/a56886b60585896d31579e0066858cd8a96ffc7d/docs/LogoGreen.png -------------------------------------------------------------------------------- /fixtures/binary.txt: -------------------------------------------------------------------------------- 1 | API integration is a pain in the ass. It's hard to find good documentation, and even when you do, it's often outdated or inaccurate. It's even harder to find someone who knows what they're doing and can help you out when you get stuck. 2 | But despite all these difficulties, I remain optimistic about the future of API integration. Why? Because I believe that eventually we will reach a point where APIs will be able to integrate themselves. That is, they will be able to automatically figure out how to work together, without the need for human intervention. 3 | This may sound like a far-fetched dream, but I believe it is achievable. After all, we've already seen significant progress in the area of artificial intelligence, and it's only a matter of time before this technology is applied to the problem of API integration. 4 | So while API integration may be a pain in the ass today, I remain hopeful for the future. 5 | 6 | Generated by OpenAI GPT-3 DaVinci 2 model. 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@superfaceai/one-sdk", 3 | "version": "2.5.0", 4 | "description": "OneSDK is a universal API client which provides an unparalleled developer experience for every HTTP API", 5 | "license": "MIT", 6 | "author": "Superface Team ", 7 | "repository": "https://github.com/superfaceai/one-sdk-js.git", 8 | "homepage": "https://superface.ai", 9 | "bugs": { 10 | "url": "https://github.com/superfaceai/one-sdk-js/issues", 11 | "email": "support@superface.ai" 12 | }, 13 | "engines": { 14 | "node": ">=14" 15 | }, 16 | "keywords": [ 17 | "api", 18 | "client", 19 | "http", 20 | "sdk", 21 | "integration", 22 | "superface" 23 | ], 24 | "main": "dist/index.js", 25 | "source": "src/index.ts", 26 | "module": "dist/superface.modern.js", 27 | "unpkg": "dist/superface.umd.js", 28 | "browser": "dist/superface.umd.js", 29 | "types": "dist/index.d.ts", 30 | "private": false, 31 | "files": [ 32 | "dist/**/*" 33 | ], 34 | "scripts": { 35 | "build": "tsc -p tsconfig.release.json --outDir dist", 36 | "check_dependencies": "node scripts/check_dependencies.js", 37 | "clean": "rimraf dist/", 38 | "format:fix": "prettier --write ./src", 39 | "format": "prettier -c ./src", 40 | "lint:fix": "yarn lint --fix", 41 | "lint": "eslint src/", 42 | "prebuild": "yarn clean", 43 | "prepack": "yarn build", 44 | "prepush": "yarn build && yarn test:clean && yarn lint && yarn format", 45 | "test:base": "jest --testPathIgnorePatterns 'event-adapter.test.ts$'", 46 | "test:clean": "jest --clear-cache && yarn test", 47 | "test:long": "jest --testPathPattern 'event-adapter.test.ts$'", 48 | "test": "jest", 49 | "watch": "yarn build --watch" 50 | }, 51 | "devDependencies": { 52 | "@superfaceai/parser": "^2.1.0", 53 | "@types/debug": "^4.1.7", 54 | "@types/jest": "^27.0.1", 55 | "@types/node": "^18.11.18", 56 | "@types/node-fetch": "^2.6.2", 57 | "@typescript-eslint/eslint-plugin": "^5.45.1", 58 | "@typescript-eslint/parser": "^5.45.1", 59 | "eslint": "^8.29.0", 60 | "eslint-config-prettier": "8.5.0", 61 | "eslint-import-resolver-typescript": "^3.5.2", 62 | "eslint-plugin-import": "^2.26.0", 63 | "eslint-plugin-jest": "^27.1.6", 64 | "eslint-plugin-jest-formatting": "^3.1.0", 65 | "eslint-plugin-prettier": "^4.2.1", 66 | "eslint-plugin-simple-import-sort": "^8.0.0", 67 | "jest": "^29.0.0", 68 | "mockttp": "^3.2.3", 69 | "prettier": "2.8.0", 70 | "rimraf": "^3.0.2", 71 | "ts-jest": "^29.0.3", 72 | "typescript": "4.3.5" 73 | }, 74 | "dependencies": { 75 | "@superfaceai/ast": "1.3.0", 76 | "abort-controller": "^3.0.0", 77 | "debug": "^4.3.2", 78 | "form-data": "^4.0.0", 79 | "node-fetch": "^2", 80 | "vm2": "^3.9.7" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scripts/check_dependencies.js: -------------------------------------------------------------------------------- 1 | const package_json = require('../package.json'); 2 | 3 | const dependencies = [ 4 | ...Object.entries(package_json.dependencies), 5 | ...Object.entries(package_json.devDependencies), 6 | ]; 7 | 8 | let incorrectDeps = false; 9 | for (const [dependency, version] of dependencies) { 10 | if (dependency.match(/@superfaceai/) && version.match(/-/)) { 11 | console.log(`${dependency} is not release version (${version})`); 12 | incorrectDeps = true; 13 | } 14 | } 15 | 16 | if (incorrectDeps) { 17 | process.exit(1); 18 | } 19 | 20 | console.log("Everything's peachy!"); 21 | -------------------------------------------------------------------------------- /src/core/client/client.internal.test.ts: -------------------------------------------------------------------------------- 1 | import { invalidIdentifierIdError, invalidVersionError } from '../errors'; 2 | import { resolveProfileId } from './client.internal'; 3 | 4 | describe('InternalClient', () => { 5 | describe('resolveProfileId', () => { 6 | describe('when passing profileId as string', () => { 7 | it('returns correct id and version', async () => { 8 | expect(resolveProfileId('scope/name@1.2.3-test')).toEqual({ 9 | id: 'scope/name', 10 | version: '1.2.3-test', 11 | }); 12 | }); 13 | 14 | it('returns correct id', async () => { 15 | expect(resolveProfileId('scope/name')).toEqual({ 16 | id: 'scope/name', 17 | version: undefined, 18 | }); 19 | }); 20 | 21 | it('throws on missing minor version', async () => { 22 | expect(() => resolveProfileId('scope/name@1')).toThrow( 23 | invalidVersionError('1', 'minor') 24 | ); 25 | }); 26 | 27 | it('throws on missing patch version', async () => { 28 | expect(() => resolveProfileId('scope/name@1.2')).toThrow( 29 | invalidVersionError('1.2', 'patch') 30 | ); 31 | }); 32 | 33 | it('throws on invalid scope', async () => { 34 | expect(() => resolveProfileId('scop:7_!e/name@1.0.0')).toThrow( 35 | invalidIdentifierIdError('scop:7_!e', 'Scope') 36 | ); 37 | }); 38 | 39 | it('throws on invalid name', async () => { 40 | expect(() => resolveProfileId('scope/nam.:_-e@1.0.0')).toThrow( 41 | invalidIdentifierIdError('nam.:_-e', 'Name') 42 | ); 43 | }); 44 | }); 45 | 46 | describe('when passing profileId as object', () => { 47 | it('returns correct id and version', async () => { 48 | expect( 49 | resolveProfileId({ id: 'scope/name', version: '1.2.3-test' }) 50 | ).toEqual({ 51 | id: 'scope/name', 52 | version: '1.2.3-test', 53 | }); 54 | }); 55 | 56 | it('returns correct id', async () => { 57 | expect(resolveProfileId({ id: 'scope/name' })).toEqual({ 58 | id: 'scope/name', 59 | version: undefined, 60 | }); 61 | }); 62 | 63 | it('throws on missing minor version', async () => { 64 | expect(() => 65 | resolveProfileId({ id: 'scope/name', version: '1' }) 66 | ).toThrow(invalidVersionError('1', 'minor')); 67 | }); 68 | 69 | it('throws on missing patch version', async () => { 70 | expect(() => 71 | resolveProfileId({ id: 'scope/name', version: '1.2' }) 72 | ).toThrow(invalidVersionError('1.2', 'patch')); 73 | }); 74 | 75 | it('throws on invalid scope', async () => { 76 | expect(() => 77 | resolveProfileId({ id: 'scop:7_!e/name', version: '1.2.3' }) 78 | ).toThrow(invalidIdentifierIdError('scop:7_!e', 'Scope')); 79 | }); 80 | 81 | it('throws on invalid name', async () => { 82 | expect(() => 83 | resolveProfileId({ id: 'scope/nam.:_-e', version: '1.2.3' }) 84 | ).toThrow(invalidIdentifierIdError('nam.:_-e', 'Name')); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/core/client/client.internal.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NormalizedSuperJsonDocument, 3 | ProfileDocumentNode, 4 | } from '@superfaceai/ast'; 5 | import { extractVersion, isValidDocumentName } from '@superfaceai/ast'; 6 | 7 | import type { ICrypto, IFileSystem, ILogger, ITimers } from '../../interfaces'; 8 | import type { ISandbox } from '../../interfaces/sandbox'; 9 | import type { SuperCache } from '../../lib'; 10 | import { profileAstId, versionToString } from '../../lib'; 11 | import type { Config } from '../config'; 12 | import { 13 | invalidIdentifierIdError, 14 | invalidVersionError, 15 | profileNotInstalledError, 16 | unconfiguredProviderInPriorityError, 17 | } from '../errors'; 18 | import type { Events, Interceptable } from '../events'; 19 | import type { AuthCache, IFetch } from '../interpreter'; 20 | import { Profile, ProfileConfiguration, resolveProfileAst } from '../profile'; 21 | import type { IBoundProfileProvider } from '../profile-provider'; 22 | 23 | export class InternalClient { 24 | constructor( 25 | private readonly events: Events, 26 | private readonly superJson: NormalizedSuperJsonDocument | undefined, 27 | private readonly config: Config, 28 | private readonly sandbox: ISandbox, 29 | private readonly timers: ITimers, 30 | private readonly fileSystem: IFileSystem, 31 | private readonly boundProfileProviderCache: SuperCache<{ 32 | provider: IBoundProfileProvider; 33 | expiresAt: number; 34 | }>, 35 | private readonly crypto: ICrypto, 36 | private readonly fetchInstance: IFetch & Interceptable & AuthCache, 37 | private readonly logger?: ILogger 38 | ) {} 39 | 40 | public async getProfile( 41 | profile: string | { id: string; version?: string } 42 | ): Promise { 43 | const { id, version } = resolveProfileId(profile); 44 | 45 | const ast = await resolveProfileAst({ 46 | profileId: id, 47 | version, 48 | logger: this.logger, 49 | fetchInstance: this.fetchInstance, 50 | fileSystem: this.fileSystem, 51 | config: this.config, 52 | crypto: this.crypto, 53 | superJson: this.superJson, 54 | }); 55 | const profileConfiguration = await this.getProfileConfiguration(ast); 56 | 57 | return new Profile( 58 | profileConfiguration, 59 | ast, 60 | this.events, 61 | this.superJson, 62 | this.config, 63 | this.sandbox, 64 | this.timers, 65 | this.fileSystem, 66 | this.boundProfileProviderCache, 67 | this.crypto, 68 | this.fetchInstance, 69 | this.logger 70 | ); 71 | } 72 | 73 | public async getProfileConfiguration( 74 | ast: ProfileDocumentNode 75 | ): Promise { 76 | const profileId = profileAstId(ast); 77 | if (this.superJson !== undefined) { 78 | const profileSettings = this.superJson.profiles[profileId]; 79 | if (profileSettings === undefined) { 80 | throw profileNotInstalledError(profileId); 81 | } 82 | 83 | if ('file' in profileSettings) { 84 | // TODO: load priority and add it to ProfileConfiguration? 85 | const priority = profileSettings.priority; 86 | if (!priority.every(p => this.superJson?.providers[p])) { 87 | throw unconfiguredProviderInPriorityError( 88 | profileId, 89 | priority, 90 | Object.keys(this.superJson.providers ?? []) 91 | ); 92 | } 93 | } 94 | } 95 | 96 | return new ProfileConfiguration( 97 | profileId, 98 | versionToString(ast.header.version) 99 | ); 100 | } 101 | } 102 | 103 | export function resolveProfileId( 104 | profile: string | { id: string; version?: string } 105 | ): { id: string; version?: string } { 106 | let id: string; 107 | let version: string | undefined; 108 | 109 | if (typeof profile === 'string') { 110 | [id, version] = profile.split('@'); 111 | } else { 112 | id = profile.id; 113 | version = profile.version; 114 | } 115 | 116 | // Check if version is full 117 | if (version !== undefined) { 118 | const extracted = extractVersion(version); 119 | if (extracted.minor === undefined) { 120 | throw invalidVersionError(version, 'minor'); 121 | } 122 | if (extracted.patch === undefined) { 123 | throw invalidVersionError(version, 'patch'); 124 | } 125 | } 126 | 127 | // Check scope and name 128 | let name: string, 129 | scope: string | undefined = undefined; 130 | const [scopeOrName, possibleName] = id.split('/'); 131 | if (possibleName === undefined) { 132 | name = scopeOrName; 133 | } else { 134 | scope = scopeOrName; 135 | name = possibleName; 136 | } 137 | if (scope !== undefined && !isValidDocumentName(scope)) { 138 | throw invalidIdentifierIdError(scope, 'Scope'); 139 | } 140 | 141 | if (!isValidDocumentName(name)) { 142 | throw invalidIdentifierIdError(name, 'Name'); 143 | } 144 | 145 | return { id, version }; 146 | } 147 | -------------------------------------------------------------------------------- /src/core/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client.internal'; 2 | -------------------------------------------------------------------------------- /src/core/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | -------------------------------------------------------------------------------- /src/core/errors/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { SDKExecutionError, UnexpectedError } from '../../lib'; 2 | 3 | describe('errors', () => { 4 | describe('UnexpectedError', () => { 5 | const error = new UnexpectedError('out of nowhere'); 6 | 7 | it('throws in correct format', () => { 8 | expect(() => { 9 | throw error; 10 | }).toThrow('out of nowhere'); 11 | }); 12 | 13 | it('returns correct format', () => { 14 | expect(error.toString()).toEqual('UnexpectedError: out of nowhere'); 15 | }); 16 | }); 17 | 18 | describe('SDKExecutionError', () => { 19 | const error = new SDKExecutionError( 20 | 'short', 21 | ['long1', 'long2', 'long3'], 22 | ['hint1', 'hint2', 'hint3'] 23 | ); 24 | 25 | it('only returns the short message when short format is requested', () => { 26 | expect(error.formatShort()).toBe('short'); 27 | }); 28 | 29 | it('returns the short message, long message and hints when long format is requested', () => { 30 | expect(error.formatLong()).toBe( 31 | `short 32 | 33 | long1 34 | long2 35 | long3 36 | 37 | Hint: hint1 38 | Hint: hint2 39 | Hint: hint3 40 | ` 41 | ); 42 | }); 43 | 44 | it('returns the long format on .toString', () => { 45 | expect(error.toString()).toBe( 46 | `short 47 | 48 | long1 49 | long2 50 | long3 51 | 52 | Hint: hint1 53 | Hint: hint2 54 | Hint: hint3 55 | ` 56 | ); 57 | 58 | expect(Object.prototype.toString.call(error)).toBe( 59 | '[object SDKExecutionError]' 60 | ); 61 | }); 62 | 63 | it('returns the long format in .message', () => { 64 | expect(error.message).toBe( 65 | `short 66 | 67 | long1 68 | long2 69 | long3 70 | 71 | Hint: hint1 72 | Hint: hint2 73 | Hint: hint3 74 | ` 75 | ); 76 | }); 77 | 78 | it('formats correctly when thrown', () => { 79 | expect(() => { 80 | throw error; 81 | }).toThrow( 82 | `short 83 | 84 | long1 85 | long2 86 | long3 87 | 88 | Hint: hint1 89 | Hint: hint2 90 | Hint: hint3 91 | ` 92 | ); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/core/errors/fetch.errors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorBase } from '../../lib'; 2 | 3 | interface NetworkError { 4 | kind: 'network'; 5 | issue: 'unsigned-ssl' | 'dns' | 'timeout' | 'reject'; 6 | } 7 | 8 | interface RequestError { 9 | kind: 'request'; 10 | issue: 'abort' | 'timeout'; 11 | } 12 | 13 | export type FetchErrorIssue = NetworkError['issue'] | RequestError['issue']; 14 | 15 | export class FetchErrorBase extends ErrorBase { 16 | constructor(public override kind: string, public issue: FetchErrorIssue) { 17 | super(kind, `Fetch failed: ${issue} issue`); 18 | } 19 | } 20 | 21 | export class NetworkFetchError extends FetchErrorBase { 22 | constructor(public override issue: NetworkError['issue']) { 23 | super('NetworkError', issue); 24 | } 25 | 26 | public get normalized(): NetworkError { 27 | return { kind: 'network', issue: this.issue }; 28 | } 29 | } 30 | 31 | export class RequestFetchError extends FetchErrorBase { 32 | constructor(public override issue: RequestError['issue']) { 33 | super('RequestError', issue); 34 | } 35 | 36 | public get normalized(): RequestError { 37 | return { kind: 'request', issue: this.issue }; 38 | } 39 | } 40 | 41 | export type FetchError = NetworkFetchError | RequestFetchError; 42 | 43 | export function isFetchError(input: unknown): input is FetchError { 44 | return ( 45 | typeof input === 'object' && 46 | (input instanceof NetworkFetchError || input instanceof RequestFetchError) 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/core/errors/filesystem.errors.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IFileExistsError, 3 | INotEmptyError, 4 | INotFoundError, 5 | IPermissionDeniedError, 6 | IUnknownFileSystemError, 7 | } from '../../interfaces'; 8 | 9 | export class FileExistsError extends Error implements IFileExistsError { 10 | public override name = 'FileExistsError' as const; 11 | 12 | constructor(message: string) { 13 | super(message); 14 | Object.setPrototypeOf(this, FileExistsError.prototype); 15 | } 16 | } 17 | 18 | export class PermissionDeniedError 19 | extends Error 20 | implements IPermissionDeniedError 21 | { 22 | public override name = 'PermissionDeniedError' as const; 23 | 24 | constructor(message: string) { 25 | super(message); 26 | Object.setPrototypeOf(this, PermissionDeniedError.prototype); 27 | } 28 | } 29 | 30 | export class NotEmptyError extends Error implements INotEmptyError { 31 | public override name = 'NotEmptyError' as const; 32 | constructor(message: string) { 33 | super(message); 34 | Object.setPrototypeOf(this, NotEmptyError.prototype); 35 | } 36 | } 37 | 38 | export class NotFoundError extends Error implements INotFoundError { 39 | public override name = 'NotFoundError' as const; 40 | constructor(message: string) { 41 | super(message); 42 | Object.setPrototypeOf(this, NotFoundError.prototype); 43 | } 44 | } 45 | 46 | export class UnknownFileSystemError 47 | extends Error 48 | implements IUnknownFileSystemError 49 | { 50 | public override name = 'UnknownFileSystemError' as const; 51 | constructor(message: string) { 52 | super(message); 53 | Object.setPrototypeOf(this, UnknownFileSystemError.prototype); 54 | } 55 | } 56 | 57 | export type FileSystemError = 58 | | FileExistsError 59 | | PermissionDeniedError 60 | | NotEmptyError 61 | | NotFoundError 62 | | UnknownFileSystemError; 63 | -------------------------------------------------------------------------------- /src/core/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors.helpers'; 2 | export * from './fetch.errors'; 3 | export * from './filesystem.errors'; 4 | -------------------------------------------------------------------------------- /src/core/events/failure/backoff.test.ts: -------------------------------------------------------------------------------- 1 | import { ConstantBackoff, ExponentialBackoff, LinearBackoff } from './backoff'; 2 | 3 | describe('backoff', () => { 4 | describe('constant backoff', () => { 5 | it('stays the same', () => { 6 | const backoff = new ConstantBackoff(1234); 7 | expect(backoff.current).toBe(1234); 8 | 9 | expect(backoff.up()).toBe(1234); 10 | expect(backoff.up()).toBe(1234); 11 | 12 | expect(backoff.down()).toBe(1234); 13 | expect(backoff.down()).toBe(1234); 14 | expect(backoff.down()).toBe(1234); 15 | 16 | expect(backoff.up()).toBe(1234); 17 | 18 | expect(backoff.down()).toBe(1234); 19 | 20 | expect(backoff.current).toBe(1234); 21 | }); 22 | }); 23 | 24 | describe('linear backoff', () => { 25 | it('goes up and down linearily', () => { 26 | const backoff = new LinearBackoff(100, 10); 27 | expect(backoff.current).toBe(100); 28 | 29 | expect(backoff.up()).toBe(110); 30 | expect(backoff.up()).toBe(120); 31 | 32 | expect(backoff.down()).toBe(110); 33 | expect(backoff.down()).toBe(100); 34 | expect(backoff.down()).toBe(90); 35 | 36 | expect(backoff.up()).toBe(100); 37 | 38 | expect(backoff.down()).toBe(90); 39 | 40 | expect(backoff.current).toBe(90); 41 | }); 42 | }); 43 | 44 | describe('exponential backoff', () => { 45 | it('goes up exponentially', () => { 46 | const backoff = new ExponentialBackoff(1, 2.0); 47 | expect(backoff.current).toBe(1); 48 | expect(backoff.current).toBe(1); 49 | 50 | expect(backoff.up()).toBe(2); 51 | expect(backoff.up()).toBe(4); 52 | expect(backoff.up()).toBe(8); 53 | expect(backoff.up()).toBe(16); 54 | expect(backoff.up()).toBe(32); 55 | expect(backoff.up()).toBe(64); 56 | expect(backoff.up()).toBe(128); 57 | 58 | expect(backoff.current).toBe(128); 59 | }); 60 | 61 | it('goes down exponentially', () => { 62 | const backoff = new ExponentialBackoff(1024, 2.0); 63 | expect(backoff.current).toBe(1024); 64 | 65 | expect(backoff.down()).toBe(512); 66 | expect(backoff.down()).toBe(256); 67 | expect(backoff.down()).toBe(128); 68 | expect(backoff.down()).toBe(64); 69 | expect(backoff.down()).toBe(32); 70 | expect(backoff.down()).toBe(16); 71 | expect(backoff.down()).toBe(8); 72 | 73 | expect(backoff.current).toBe(8); 74 | }); 75 | 76 | it('goes up and down exponentionally', () => { 77 | const backoff = new ExponentialBackoff(1024, 2.0); 78 | expect(backoff.current).toBe(1024); 79 | 80 | expect(backoff.up()).toBe(2048); 81 | expect(backoff.up()).toBe(4096); 82 | 83 | expect(backoff.down()).toBe(2048); 84 | expect(backoff.down()).toBe(1024); 85 | expect(backoff.down()).toBe(512); 86 | 87 | expect(backoff.up()).toBe(1024); 88 | 89 | expect(backoff.down()).toBe(512); 90 | 91 | expect(backoff.current).toBe(512); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/core/events/failure/backoff.ts: -------------------------------------------------------------------------------- 1 | export class Backoff { 2 | public static DEFAULT_INITIAL = 500; 3 | 4 | protected _current: number; 5 | 6 | constructor( 7 | protected readonly successor: (x: number) => number, 8 | protected readonly inverseSuccessor: (x: number) => number, 9 | initial: number 10 | ) { 11 | this._current = initial; 12 | } 13 | 14 | public get current(): number { 15 | return this._current; 16 | } 17 | 18 | public up(): number { 19 | this._current = this.successor(this._current); 20 | 21 | return this._current; 22 | } 23 | 24 | public down(): number { 25 | this._current = this.inverseSuccessor(this._current); 26 | 27 | return this._current; 28 | } 29 | 30 | protected static clampValue( 31 | value: number, 32 | minimum?: number, 33 | maximum?: number 34 | ): number { 35 | if (minimum !== undefined) { 36 | value = Math.max(minimum, value); 37 | } 38 | if (maximum !== undefined) { 39 | value = Math.min(maximum, value); 40 | } 41 | 42 | return value; 43 | } 44 | } 45 | 46 | export class ConstantBackoff extends Backoff { 47 | constructor(initial: number) { 48 | super( 49 | x => x, 50 | x => x, 51 | initial 52 | ); 53 | } 54 | } 55 | 56 | export class LinearBackoff extends Backoff { 57 | constructor( 58 | initial: number, 59 | step: number, 60 | minimum?: number, 61 | maximum?: number 62 | ) { 63 | super( 64 | x => Backoff.clampValue(x + step, undefined, maximum), 65 | x => Backoff.clampValue(x - step, minimum, undefined), 66 | initial 67 | ); 68 | } 69 | } 70 | 71 | export class ExponentialBackoff extends Backoff { 72 | public static DEFAULT_BASE = 2; 73 | 74 | constructor( 75 | initial: number, 76 | base: number, 77 | minimum?: number, 78 | maximum?: number 79 | ) { 80 | super( 81 | x => Backoff.clampValue(x * base, undefined, maximum), 82 | x => Backoff.clampValue(x / base, minimum, undefined), 83 | initial 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/core/events/failure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './backoff'; 2 | export * from './event-adapter'; 3 | export * from './map-interpreter-adapter'; 4 | export * from './policies'; 5 | export * from './policy'; 6 | -------------------------------------------------------------------------------- /src/core/events/failure/map-interpreter-adapter.ts: -------------------------------------------------------------------------------- 1 | import type { MapDocumentNode } from '@superfaceai/ast'; 2 | import { HttpCallStatementNode } from '@superfaceai/ast'; 3 | 4 | import type { MapInterpreterExternalHandler } from '../../interpreter'; 5 | import { HTTPError, HttpResponse } from '../../interpreter'; 6 | import type { Events, Interceptable, InterceptableMetadata } from '../events'; 7 | import { eventInterceptor } from '../events'; 8 | 9 | export class MapInterpreterEventAdapter 10 | implements Interceptable, MapInterpreterExternalHandler 11 | { 12 | constructor( 13 | public readonly metadata?: InterceptableMetadata, 14 | public readonly events?: Events 15 | ) {} 16 | 17 | @eventInterceptor({ 18 | eventName: 'unhandled-http', 19 | placement: 'before', 20 | }) 21 | public async unhandledHttp( 22 | ast: MapDocumentNode | undefined, 23 | node: HttpCallStatementNode, 24 | response: HttpResponse 25 | ): Promise<'continue' | 'retry'> { 26 | if (response.statusCode >= 400) { 27 | throw new HTTPError( 28 | 'HTTP Error', 29 | { node, ast }, 30 | response.statusCode, 31 | response.debug.request, 32 | { body: response.body, headers: response.headers } 33 | ); 34 | } 35 | 36 | return 'continue'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/events/failure/resolution.ts: -------------------------------------------------------------------------------- 1 | import type { FailurePolicyReason } from './policy'; 2 | 3 | /** Additional common configuration for making requests. */ 4 | export type RequestConfiguration = { 5 | /** Timeout for the request after which the request is aborted and a failure is reported. */ 6 | timeout: number; 7 | }; 8 | 9 | /** 10 | * Abort the execution completely. 11 | * 12 | * This may be returned from: 13 | * * `beforeExecute`: For example circuit breaker 14 | * * `afterFailure`: Circuit breaker, max retries exhausted, etc. 15 | */ 16 | export type AbortResolution = { 17 | kind: 'abort'; 18 | /** Reason why the execution was aborted. */ 19 | reason: FailurePolicyReason; 20 | }; 21 | 22 | /** 23 | * Retry the execution, possibly with a backoff. 24 | * 25 | * This may be returned from: 26 | * * `afterFailure` 27 | */ 28 | export type RetryResolution = { 29 | kind: 'retry'; 30 | }; 31 | 32 | /** 33 | * Backoff the execution. 34 | * 35 | * This may be returned from: 36 | * * `beforeExecution` 37 | */ 38 | export type BackoffResolution = { 39 | kind: 'backoff'; 40 | /** Number of milliseconds to wait until executing */ 41 | backoff: number; 42 | }; 43 | 44 | /** 45 | * Recache before executing. 46 | * 47 | * This may be returned from: 48 | * * `beforeExecution` 49 | */ 50 | export type RecacheResolution = { 51 | kind: 'recache'; 52 | /** Optional url to a fallback registry to use for this recache request only */ 53 | newRegistry?: string; // TODO: do we want this? 54 | /** Reason that caused this recache. */ 55 | reason: FailurePolicyReason; 56 | }; 57 | 58 | /** 59 | * Switch to a different provider. 60 | * 61 | * This may be returned from: 62 | * * `beforeExecution`: When a failover happens or when going back is to be reattempted. 63 | */ 64 | export type SwitchProviderResolution = { 65 | kind: 'switch-provider'; 66 | provider: string; 67 | /** Reason that caused this switch. */ 68 | reason: FailurePolicyReason; 69 | }; 70 | 71 | /** 72 | * Continue in the current configuration, no changes should be made 73 | * 74 | * This may be returned from: 75 | * * `beforeExecution` 76 | * * `afterFailure` 77 | * * `afterSuccess` 78 | */ 79 | export type ContinueResolution = { 80 | kind: 'continue'; 81 | }; 82 | 83 | export type ExecutionResolution = 84 | | (RequestConfiguration & (ContinueResolution | BackoffResolution)) 85 | | AbortResolution 86 | | RecacheResolution 87 | | SwitchProviderResolution; 88 | 89 | export type FailureResolution = 90 | | AbortResolution 91 | | RetryResolution 92 | | ContinueResolution 93 | | RecacheResolution 94 | | SwitchProviderResolution; 95 | 96 | export type SuccessResolution = ContinueResolution; 97 | -------------------------------------------------------------------------------- /src/core/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './events'; 2 | export * from './failure'; 3 | export * from './reporter'; 4 | -------------------------------------------------------------------------------- /src/core/events/reporter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reporter'; 2 | -------------------------------------------------------------------------------- /src/core/events/reporter/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { MockEnvironment } from '../../../mock'; 2 | import { NodeCrypto } from '../../../node'; 3 | import { normalizeSuperJsonDocument } from '../../../schema-tools'; 4 | import { anonymizeSuperJson, hashSuperJson } from './utils'; 5 | 6 | const environment = new MockEnvironment(); 7 | 8 | describe('MetricReporter utils', () => { 9 | // TODO: Proper tests for config hash and anonymization 10 | describe('when computing config hash', () => { 11 | it('does debug', () => { 12 | const superJson = { 13 | profiles: { 14 | abc: { 15 | file: 'x', 16 | priority: ['first', 'second'], 17 | providers: { 18 | second: { 19 | mapRevision: '1.0', 20 | }, 21 | first: { 22 | file: 'file://some/path', 23 | }, 24 | }, 25 | }, 26 | ghe: { 27 | version: '1.2.3', 28 | }, 29 | def: 'file://hi/hello', 30 | }, 31 | providers: { 32 | foo: {}, 33 | bar: { 34 | file: 'hi', 35 | }, 36 | }, 37 | }; 38 | 39 | const normalized = normalizeSuperJsonDocument(superJson, environment); 40 | expect(anonymizeSuperJson(normalized)).toEqual({ 41 | profiles: { 42 | abc: { 43 | version: 'file', 44 | providers: [ 45 | { 46 | provider: 'second', 47 | priority: 1, 48 | version: '1.0', 49 | }, 50 | { 51 | provider: 'first', 52 | priority: 0, 53 | version: 'file', 54 | }, 55 | ], 56 | }, 57 | ghe: { 58 | version: '1.2.3', 59 | providers: [], 60 | }, 61 | def: { 62 | version: 'file', 63 | providers: [], 64 | }, 65 | }, 66 | providers: ['foo', 'bar'], 67 | }); 68 | 69 | expect(hashSuperJson(normalized, new NodeCrypto())).toBe( 70 | 'd090f0589a19634c065e903a81006f79' 71 | ); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/core/events/reporter/utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnonymizedSuperJsonDocument, 3 | NormalizedSuperJsonDocument, 4 | } from '@superfaceai/ast'; 5 | 6 | import type { ICrypto } from '../../../interfaces'; 7 | import { configHash } from '../../../lib'; 8 | 9 | export function anonymizeSuperJson( 10 | document: NormalizedSuperJsonDocument 11 | ): AnonymizedSuperJsonDocument { 12 | const profiles: AnonymizedSuperJsonDocument['profiles'] = {}; 13 | for (const [profile, profileEntry] of Object.entries(document.profiles)) { 14 | const providers: typeof profiles[string]['providers'] = []; 15 | for (const [provider, providerEntry] of Object.entries( 16 | profileEntry.providers 17 | )) { 18 | const anonymizedProvider: typeof providers[number] = { 19 | provider, 20 | version: 'unknown', 21 | }; 22 | const providerPriority = profileEntry.priority.findIndex( 23 | providerName => provider === providerName 24 | ); 25 | if (providerPriority > -1) { 26 | anonymizedProvider.priority = providerPriority; 27 | } 28 | if ('file' in providerEntry) { 29 | anonymizedProvider.version = 'file'; 30 | } else if ( 31 | 'mapRevision' in providerEntry && 32 | providerEntry.mapRevision !== undefined 33 | ) { 34 | anonymizedProvider.version = providerEntry.mapRevision; 35 | if (providerEntry.mapVariant !== undefined) { 36 | anonymizedProvider.version += `-${providerEntry.mapVariant}`; 37 | } 38 | } 39 | 40 | providers.push(anonymizedProvider); 41 | } 42 | profiles[profile] = { 43 | version: 'version' in profileEntry ? profileEntry.version : 'file', 44 | providers, 45 | }; 46 | } 47 | 48 | return { 49 | profiles, 50 | providers: Object.keys(document.providers), 51 | }; 52 | } 53 | 54 | export function hashSuperJson( 55 | document: NormalizedSuperJsonDocument, 56 | crypto: ICrypto 57 | ): string { 58 | // :,::[],: 59 | const anonymized = anonymizeSuperJson(document); 60 | const profileValues: string[] = []; 61 | for (const [profile, profileEntry] of Object.entries(anonymized.profiles)) { 62 | const providers: string[] = Object.entries(profileEntry.providers).map( 63 | ([provider, providerEntry]): string => { 64 | return [ 65 | provider, 66 | providerEntry.priority, 67 | ...(providerEntry.version !== undefined 68 | ? [providerEntry.version] 69 | : []), 70 | ].join(':'); 71 | } 72 | ); 73 | // sort by provider name to be reproducible 74 | providers.sort(); 75 | profileValues.push( 76 | [`${profile}:${profileEntry.version}`, ...providers].join(',') 77 | ); 78 | } 79 | // sort by profile name to be reproducible 80 | profileValues.sort(); 81 | 82 | // Copy and sort 83 | const providerValues = anonymized.providers.map(provider => provider).sort(); 84 | 85 | return configHash([...profileValues, ...providerValues], crypto); 86 | } 87 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './config'; 3 | export * from './errors'; 4 | export * from './events'; 5 | export * from '../interfaces'; 6 | export * from './interpreter'; 7 | export * from './profile'; 8 | export * from './profile-provider'; 9 | export * from './provider'; 10 | export * from './registry'; 11 | export * from './services'; 12 | export * from './usecase'; 13 | -------------------------------------------------------------------------------- /src/core/interpreter/external-handler.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCallStatementNode, MapDocumentNode } from '@superfaceai/ast'; 2 | 3 | import type { HttpResponse } from './http'; 4 | 5 | /** 6 | * Interface for external handler that is used in the MapInterpreter to handle certain states generically. 7 | */ 8 | export interface MapInterpreterExternalHandler { 9 | /** Invoked when the map interpreter http call finishes and no handler is defined in the map to handle such response. */ 10 | unhandledHttp?( 11 | ast: MapDocumentNode | undefined, 12 | node: HttpCallStatementNode, 13 | response: HttpResponse 14 | ): Promise<'continue' | 'retry'>; 15 | } 16 | -------------------------------------------------------------------------------- /src/core/interpreter/http/http.ts: -------------------------------------------------------------------------------- 1 | import type { HttpSecurityRequirement } from '@superfaceai/ast'; 2 | import { HttpScheme, SecurityType } from '@superfaceai/ast'; 3 | 4 | import type { ICrypto, ILogger } from '../../../interfaces'; 5 | import type { NonPrimitive, Variables } from '../../../lib'; 6 | import { UnexpectedError } from '../../../lib'; 7 | import { pipe } from '../../../lib/pipe/pipe'; 8 | import { 9 | invalidHTTPMapValueType, 10 | missingSecurityValuesError, 11 | } from '../../errors'; 12 | import { 13 | authenticateFilter, 14 | fetchFilter, 15 | handleResponseFilter, 16 | prepareRequestFilter, 17 | withRequest, 18 | withResponse, 19 | } from './filters'; 20 | import type { IFetch } from './interfaces'; 21 | import type { 22 | AuthCache, 23 | ISecurityHandler, 24 | RequestParameters, 25 | SecurityConfiguration, 26 | } from './security'; 27 | import { ApiKeyHandler, DigestHandler, HttpHandler } from './security'; 28 | import type { HttpResponse } from './types'; 29 | import { variablesToHttpMap } from './utils'; 30 | 31 | export enum NetworkErrors { 32 | TIMEOUT_ERROR = 'TIMEOUT_ERROR', 33 | } 34 | 35 | export class HttpClient { 36 | constructor( 37 | private fetchInstance: IFetch & AuthCache, 38 | private readonly crypto: ICrypto, 39 | private readonly logger?: ILogger 40 | ) {} 41 | 42 | public async request( 43 | url: string, 44 | parameters: { 45 | method: string; 46 | headers?: NonPrimitive; 47 | queryParameters?: NonPrimitive; 48 | body?: Variables; 49 | contentType?: string; 50 | accept?: string; 51 | securityRequirements?: HttpSecurityRequirement[]; 52 | securityConfiguration?: SecurityConfiguration[]; 53 | baseUrl: string; 54 | pathParameters?: NonPrimitive; 55 | integrationParameters?: Record; 56 | } 57 | ): Promise { 58 | const requestParameters: RequestParameters = { 59 | url, 60 | ...parameters, 61 | queryParameters: variablesToHttpMap( 62 | parameters.queryParameters ?? {} 63 | ).match( 64 | v => v, 65 | ([key, value]) => { 66 | throw invalidHTTPMapValueType('query parameter', key, typeof value); 67 | } 68 | ), 69 | headers: variablesToHttpMap(parameters.headers ?? {}).match( 70 | v => v, 71 | ([key, value]) => { 72 | throw invalidHTTPMapValueType('header', key, typeof value); 73 | } 74 | ), 75 | }; 76 | 77 | const handler = createSecurityHandler( 78 | this.fetchInstance, 79 | requestParameters.securityConfiguration, 80 | requestParameters.securityRequirements, 81 | this.crypto, 82 | this.logger 83 | ); 84 | 85 | const result = await pipe( 86 | { 87 | parameters: requestParameters, 88 | }, 89 | authenticateFilter(handler), 90 | prepareRequestFilter, 91 | withRequest(fetchFilter(this.fetchInstance, this.logger)), 92 | withResponse( 93 | handleResponseFilter(this.fetchInstance, this.logger, handler) 94 | ) 95 | ); 96 | 97 | if (result.response === undefined) { 98 | throw new UnexpectedError('Response is undefined'); 99 | } 100 | 101 | return result.response; 102 | } 103 | } 104 | 105 | function createSecurityHandler( 106 | fetchInstance: IFetch & AuthCache, 107 | securityConfiguration: SecurityConfiguration[] = [], 108 | securityRequirements: HttpSecurityRequirement[] = [], 109 | crypto: ICrypto, 110 | logger?: ILogger 111 | ): ISecurityHandler | undefined { 112 | let handler: ISecurityHandler | undefined = undefined; 113 | for (const requirement of securityRequirements) { 114 | const configuration = securityConfiguration.find( 115 | configuration => configuration.id === requirement.id 116 | ); 117 | if (configuration === undefined) { 118 | throw missingSecurityValuesError(requirement.id); 119 | } 120 | if (configuration.type === SecurityType.APIKEY) { 121 | handler = new ApiKeyHandler(configuration, logger); 122 | } else if (configuration.scheme === HttpScheme.DIGEST) { 123 | handler = new DigestHandler(configuration, fetchInstance, crypto, logger); 124 | } else { 125 | handler = new HttpHandler(configuration, logger); 126 | } 127 | } 128 | 129 | return handler; 130 | } 131 | -------------------------------------------------------------------------------- /src/core/interpreter/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http'; 2 | export * from './interfaces'; 3 | export * from './security'; 4 | export * from './types'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /src/core/interpreter/http/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { IBinaryData } from '../../../interfaces'; 2 | 3 | type StringBody = { _type: 'string'; data: string }; 4 | type FormDataBody = { _type: 'formdata'; data: Record }; 5 | type BinaryBody = { _type: 'binary'; data: Buffer | IBinaryData }; 6 | type URLSearchParamsBody = { 7 | _type: 'urlsearchparams'; 8 | data: Record; 9 | }; 10 | export const stringBody = (data: string): StringBody => ({ 11 | _type: 'string', 12 | data, 13 | }); 14 | export const formDataBody = (data: Record): FormDataBody => ({ 15 | _type: 'formdata', 16 | data, 17 | }); 18 | export const urlSearchParamsBody = ( 19 | data: Record 20 | ): URLSearchParamsBody => ({ _type: 'urlsearchparams', data }); 21 | export const binaryBody = (data: Buffer | IBinaryData): BinaryBody => ({ 22 | _type: 'binary', 23 | data, 24 | }); 25 | export function isStringBody(data: FetchBody): data is StringBody { 26 | return data._type === 'string'; 27 | } 28 | export function isFormDataBody(data: FetchBody): data is FormDataBody { 29 | return data._type === 'formdata'; 30 | } 31 | export function isUrlSearchParamsBody( 32 | data: FetchBody 33 | ): data is URLSearchParamsBody { 34 | return data._type === 'urlsearchparams'; 35 | } 36 | export function isBinaryBody(data: FetchBody): data is BinaryBody { 37 | return data._type === 'binary'; 38 | } 39 | export type FetchBody = 40 | | StringBody 41 | | FormDataBody 42 | | URLSearchParamsBody 43 | | BinaryBody; 44 | 45 | export type HttpMultiMap = Record; 46 | 47 | export type FetchParameters = { 48 | headers?: HttpMultiMap; 49 | method: string; 50 | body?: FetchBody; 51 | queryParameters?: HttpMultiMap; 52 | timeout?: number; 53 | }; 54 | 55 | export type FetchResponse = { 56 | status: number; 57 | statusText: string; 58 | headers: HttpMultiMap; 59 | body?: unknown; 60 | }; 61 | 62 | export type IFetch = { 63 | fetch(url: string, parameters: FetchParameters): Promise; 64 | }; 65 | 66 | export const JSON_CONTENT = 'application/json'; 67 | export const JSON_PROBLEM_CONTENT = 'application/problem+json'; 68 | export const URLENCODED_CONTENT = 'application/x-www-form-urlencoded'; 69 | export const FORMDATA_CONTENT = 'multipart/form-data'; 70 | export const BINARY_CONTENT_TYPES = [ 71 | 'application/octet-stream', 72 | 'video/*', 73 | 'audio/*', 74 | 'image/*', 75 | ]; 76 | export const BINARY_CONTENT_REGEXP = 77 | /application\/octet-stream|video\/.*|audio\/.*|image\/.*/; 78 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/api-key/api-key.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiKeySecurityScheme, 3 | ApiKeySecurityValues, 4 | } from '@superfaceai/ast'; 5 | import { ApiKeyPlacement } from '@superfaceai/ast'; 6 | 7 | import type { ILogger, LogFunction } from '../../../../../interfaces'; 8 | import type { Variables } from '../../../../../lib'; 9 | import { isPrimitive } from '../../../../../lib'; 10 | import { apiKeyInBodyError } from '../../../../errors'; 11 | import type { HttpMultiMap } from '../../interfaces'; 12 | import type { 13 | AuthenticateRequestAsync, 14 | ISecurityHandler, 15 | RequestParameters, 16 | } from '../interfaces'; 17 | import { DEFAULT_AUTHORIZATION_HEADER_NAME } from '../interfaces'; 18 | 19 | const DEBUG_NAMESPACE = 'http:api-key-handler'; 20 | 21 | export class ApiKeyHandler implements ISecurityHandler { 22 | private log?: LogFunction; 23 | 24 | constructor( 25 | public readonly configuration: ApiKeySecurityScheme & ApiKeySecurityValues, 26 | logger?: ILogger 27 | ) { 28 | this.log = logger?.log(DEBUG_NAMESPACE); 29 | this.log?.('Initialized api key authentization handler'); 30 | } 31 | 32 | public authenticate: AuthenticateRequestAsync = async ( 33 | parameters: RequestParameters 34 | ) => { 35 | let body: Variables | undefined = parameters.body; 36 | const headers: HttpMultiMap = parameters.headers ?? {}; 37 | const pathParameters = parameters.pathParameters ?? {}; 38 | const queryParameters = parameters.queryParameters ?? {}; 39 | 40 | const name = this.configuration.name ?? DEFAULT_AUTHORIZATION_HEADER_NAME; 41 | 42 | switch (this.configuration.in) { 43 | case ApiKeyPlacement.HEADER: 44 | this.log?.('Setting api key to header'); 45 | headers[name] = this.configuration.apikey; 46 | break; 47 | 48 | case ApiKeyPlacement.BODY: 49 | this.log?.('Setting api key to body'); 50 | body = applyApiKeyAuthInBody( 51 | body ?? {}, 52 | name.startsWith('/') ? name.slice(1).split('/') : [name], 53 | this.configuration.apikey 54 | ); 55 | break; 56 | 57 | case ApiKeyPlacement.PATH: 58 | this.log?.('Setting api key to path'); 59 | pathParameters[name] = this.configuration.apikey; 60 | break; 61 | 62 | case ApiKeyPlacement.QUERY: 63 | this.log?.('Setting api key to query'); 64 | queryParameters[name] = this.configuration.apikey; 65 | break; 66 | } 67 | 68 | return { 69 | ...parameters, 70 | headers, 71 | pathParameters, 72 | queryParameters, 73 | body, 74 | }; 75 | }; 76 | } 77 | 78 | function applyApiKeyAuthInBody( 79 | requestBody: Variables, 80 | referenceTokens: string[], 81 | apikey: string, 82 | visitedReferenceTokens: string[] = [] 83 | ): Variables { 84 | if (isPrimitive(requestBody)) { 85 | const valueLocation = visitedReferenceTokens.length 86 | ? `value at /${visitedReferenceTokens.join('/')}` 87 | : 'body'; 88 | const bodyType = Array.isArray(requestBody) ? 'Array' : typeof requestBody; 89 | 90 | throw apiKeyInBodyError(valueLocation, bodyType); 91 | } 92 | 93 | const token = referenceTokens.shift(); 94 | if (token === undefined) { 95 | return apikey; 96 | } 97 | 98 | const segVal = requestBody[token] ?? {}; 99 | requestBody[token] = applyApiKeyAuthInBody(segVal, referenceTokens, apikey, [ 100 | ...visitedReferenceTokens, 101 | token, 102 | ]); 103 | 104 | return requestBody; 105 | } 106 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/api-key/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiKeyHandler } from './api-key'; 2 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/digest/index.ts: -------------------------------------------------------------------------------- 1 | export { DigestHandler } from './digest'; 2 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/http/http.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpScheme, SecurityType } from '@superfaceai/ast'; 2 | 3 | import { URLENCODED_CONTENT } from '../../interfaces'; 4 | import type { RequestParameters, SecurityConfiguration } from '../../security'; 5 | import { HttpHandler } from './http'; 6 | 7 | describe('HttpHandler', () => { 8 | let httpHandler: HttpHandler; 9 | let parameters: RequestParameters; 10 | let configuration: SecurityConfiguration & { type: SecurityType.HTTP }; 11 | 12 | describe('prepare', () => { 13 | it('sets header to correct value', async () => { 14 | parameters = { 15 | url: '/api/', 16 | baseUrl: 'https://test.com/', 17 | method: 'get', 18 | headers: {}, 19 | pathParameters: {}, 20 | queryParameters: {}, 21 | body: undefined, 22 | contentType: URLENCODED_CONTENT, 23 | }; 24 | configuration = { 25 | id: 'test', 26 | type: SecurityType.HTTP, 27 | scheme: HttpScheme.BASIC, 28 | username: 'user', 29 | password: 'secret', 30 | }; 31 | httpHandler = new HttpHandler(configuration); 32 | expect( 33 | (await httpHandler.authenticate(parameters)).headers?.['Authorization'] 34 | ).toEqual('Basic dXNlcjpzZWNyZXQ='); 35 | }); 36 | }); 37 | 38 | describe('bearer', () => { 39 | it('sets header to correct value', async () => { 40 | parameters = { 41 | url: '/api/', 42 | baseUrl: 'https://test.com/', 43 | method: 'get', 44 | headers: {}, 45 | pathParameters: {}, 46 | queryParameters: {}, 47 | body: undefined, 48 | contentType: URLENCODED_CONTENT, 49 | }; 50 | configuration = { 51 | id: 'test', 52 | type: SecurityType.HTTP, 53 | scheme: HttpScheme.BEARER, 54 | token: 'secret', 55 | }; 56 | httpHandler = new HttpHandler(configuration); 57 | 58 | expect( 59 | (await httpHandler.authenticate(parameters)).headers?.['Authorization'] 60 | ).toEqual('Bearer secret'); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/http/http.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityType } from '@superfaceai/ast'; 2 | import { HttpScheme } from '@superfaceai/ast'; 3 | 4 | import type { ILogger, LogFunction } from '../../../../../interfaces'; 5 | import type { HttpMultiMap } from '../../interfaces'; 6 | import type { 7 | AuthenticateRequestAsync, 8 | ISecurityHandler, 9 | RequestParameters, 10 | SecurityConfiguration, 11 | } from '../interfaces'; 12 | import { DEFAULT_AUTHORIZATION_HEADER_NAME } from '../interfaces'; 13 | 14 | const DEBUG_NAMESPACE = 'http:security:http-handler'; 15 | 16 | export class HttpHandler implements ISecurityHandler { 17 | private log?: LogFunction | undefined; 18 | constructor( 19 | public readonly configuration: SecurityConfiguration & { 20 | type: SecurityType.HTTP; 21 | }, 22 | logger?: ILogger 23 | ) { 24 | this.log = logger?.log(DEBUG_NAMESPACE); 25 | this.log?.('Initialized http authentization handler'); 26 | } 27 | 28 | public authenticate: AuthenticateRequestAsync = async ( 29 | parameters: RequestParameters 30 | ) => { 31 | const headers: HttpMultiMap = parameters.headers ?? {}; 32 | 33 | switch (this.configuration.scheme) { 34 | case HttpScheme.BASIC: 35 | this.log?.('Setting basic http auhentization'); 36 | 37 | headers[DEFAULT_AUTHORIZATION_HEADER_NAME] = applyBasicAuth( 38 | this.configuration 39 | ); 40 | break; 41 | case HttpScheme.BEARER: 42 | this.log?.('Setting bearer http auhentization'); 43 | 44 | headers[DEFAULT_AUTHORIZATION_HEADER_NAME] = applyBearerToken( 45 | this.configuration 46 | ); 47 | break; 48 | } 49 | 50 | return { 51 | ...parameters, 52 | headers, 53 | }; 54 | }; 55 | } 56 | 57 | function applyBasicAuth( 58 | configuration: SecurityConfiguration & { 59 | type: SecurityType.HTTP; 60 | scheme: HttpScheme.BASIC; 61 | } 62 | ): string { 63 | return ( 64 | 'Basic ' + 65 | Buffer.from(`${configuration.username}:${configuration.password}`).toString( 66 | 'base64' 67 | ) 68 | ); 69 | } 70 | 71 | function applyBearerToken( 72 | configuration: SecurityConfiguration & { 73 | type: SecurityType.HTTP; 74 | scheme: HttpScheme.BEARER; 75 | } 76 | ): string { 77 | return `Bearer ${configuration.token}`; 78 | } 79 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/http/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpHandler } from './http'; 2 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-key'; 2 | export * from './digest'; 3 | export * from './http'; 4 | export * from './interfaces'; 5 | -------------------------------------------------------------------------------- /src/core/interpreter/http/security/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiKeySecurityScheme, 3 | ApiKeySecurityValues, 4 | BasicAuthSecurityScheme, 5 | BasicAuthSecurityValues, 6 | BearerTokenSecurityScheme, 7 | BearerTokenSecurityValues, 8 | DigestSecurityScheme, 9 | DigestSecurityValues, 10 | HttpSecurityRequirement, 11 | } from '@superfaceai/ast'; 12 | 13 | import type { NonPrimitive, SuperCache, Variables } from '../../../../lib'; 14 | import type { FetchParameters, HttpMultiMap } from '../interfaces'; 15 | import type { HttpResponse } from '../types'; 16 | 17 | export const DEFAULT_AUTHORIZATION_HEADER_NAME = 'Authorization'; 18 | 19 | export type AuthCache = { 20 | digest: SuperCache; 21 | }; 22 | 23 | /** 24 | * This type defines function used for authentization with more complex auth. methods (oauth, diges), we useFetchInstance to fetch auth. response and to set credentials to cache 25 | */ 26 | export type AuthenticateRequestAsync = ( 27 | parameters: RequestParameters 28 | ) => Promise; 29 | 30 | /** 31 | * This type defines function used for handling response with complex auth methods. 32 | * It returns undefined (when there is no need to retry request) or úarameters used in new request 33 | */ 34 | export type HandleResponseAsync = ( 35 | response: HttpResponse, 36 | resourceRequestParameters: RequestParameters 37 | ) => Promise | undefined; 38 | 39 | /** 40 | * Represents class that is able to prepare (set headers, path etc.) and handle (challange responses for eg. digest) authentication 41 | */ 42 | export interface ISecurityHandler { 43 | /** 44 | * Hold SecurityConfiguration context for handling more complex authentizations 45 | */ 46 | readonly configuration: SecurityConfiguration; 47 | 48 | authenticate: AuthenticateRequestAsync; 49 | 50 | handleResponse?: HandleResponseAsync; 51 | } 52 | 53 | export type SecurityConfiguration = 54 | | (ApiKeySecurityScheme & ApiKeySecurityValues) 55 | | (BasicAuthSecurityScheme & BasicAuthSecurityValues) 56 | | (BearerTokenSecurityScheme & BearerTokenSecurityValues) 57 | | (DigestSecurityScheme & DigestSecurityValues); 58 | 59 | export type RequestParameters = { 60 | url: string; 61 | method: string; 62 | headers?: HttpMultiMap; 63 | queryParameters?: HttpMultiMap; 64 | body?: Variables; 65 | contentType?: string; 66 | accept?: string; 67 | securityRequirements?: HttpSecurityRequirement[]; 68 | securityConfiguration?: SecurityConfiguration[]; 69 | baseUrl: string; 70 | pathParameters?: NonPrimitive; 71 | integrationParameters?: Record; 72 | }; 73 | 74 | export type HttpRequest = FetchParameters & { url: string }; 75 | -------------------------------------------------------------------------------- /src/core/interpreter/http/types.ts: -------------------------------------------------------------------------------- 1 | import type { HttpMultiMap } from './interfaces'; 2 | 3 | export interface HttpResponse { 4 | statusCode: number; 5 | body: unknown; 6 | headers: HttpMultiMap; 7 | debug: { 8 | request: { 9 | headers: HttpMultiMap; 10 | url: string; 11 | body: unknown; 12 | }; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/core/interpreter/http/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { err, ok } from '../../../lib'; 2 | import { getHeaderMulti, variablesToHttpMap } from '..'; 3 | import type { HttpMultiMap } from './interfaces'; 4 | import { 5 | createUrl, 6 | deleteHeader, 7 | getHeader, 8 | hasHeader, 9 | setHeader, 10 | } from './utils'; 11 | 12 | describe('interpreter · http · utils', () => { 13 | describe('getHeader', () => { 14 | it('returns all values', () => { 15 | expect( 16 | getHeader({ foo: 'bar', Foo: 'baz', FOO: ['qux', 'quz'] }, 'foo') 17 | ).toEqual('bar, baz, qux, quz'); 18 | }); 19 | 20 | it('cleans up undefined values', () => { 21 | expect( 22 | getHeader({ foo: 'bar', Foo: undefined, FOO: ['qux', 'quz'] }, 'foo') 23 | ).toEqual('bar, qux, quz'); 24 | }); 25 | }); 26 | 27 | describe('hasHeader', () => { 28 | it('should return true if header is present', () => { 29 | expect(hasHeader({ Foo: 'bar' }, 'foo')).toBe(true); 30 | }); 31 | 32 | it('should return false if header is not present', () => { 33 | expect(hasHeader({ Foo: 'bar' }, 'baz')).toBe(false); 34 | }); 35 | }); 36 | 37 | describe('setHeader', () => { 38 | it('mutates passed data', () => { 39 | const headers = {}; 40 | setHeader(headers, 'foo', 'bar'); 41 | expect(headers).toEqual({ foo: 'bar' }); 42 | }); 43 | 44 | it('does not mutate passed data if header already exists', () => { 45 | const headers = { foo: 'bar' }; 46 | setHeader(headers, 'foo', 'baz'); 47 | expect(headers).toEqual({ foo: 'bar' }); 48 | }); 49 | }); 50 | 51 | describe('deleteHeader', () => { 52 | it('deletes both foo and Foo headers', () => { 53 | const headers = { foo: 'bar', Foo: 'baz' }; 54 | deleteHeader(headers, 'Foo'); 55 | expect(headers).toEqual({}); 56 | }); 57 | }); 58 | 59 | describe('createUrl', () => { 60 | it('correctly creates url for empty string', () => { 61 | const mapUrl = ''; 62 | expect(createUrl(mapUrl, { baseUrl: 'http://example.com' })).toBe( 63 | 'http://example.com' 64 | ); 65 | }); 66 | 67 | it('correctly creates url for single slash', () => { 68 | const mapUrl = '/'; 69 | expect(createUrl(mapUrl, { baseUrl: 'http://example.com' })).toBe( 70 | 'http://example.com/' 71 | ); 72 | }); 73 | 74 | it('returns an error for absolute url', () => { 75 | const mapUrl = 'something'; 76 | expect(() => 77 | createUrl(mapUrl, { baseUrl: 'http://example.com' }) 78 | ).toThrow('Expected relative url'); 79 | }); 80 | 81 | it('replaces integration parameters in baseURL', () => { 82 | expect( 83 | createUrl('/baz', { 84 | baseUrl: 'http://example.com/{FOO}/{BAR}', 85 | integrationParameters: { 86 | FOO: 'foo', 87 | BAR: 'bar', 88 | }, 89 | }) 90 | ).toBe('http://example.com/foo/bar/baz'); 91 | }); 92 | 93 | it('replaces path parameters in inputUrl', () => { 94 | expect( 95 | createUrl('/{FOO}/{BAR.BAZ}', { 96 | baseUrl: 'http://example.com', 97 | pathParameters: { 98 | FOO: 'foo', 99 | BAR: { BAZ: 'baz' }, 100 | }, 101 | }) 102 | ).toBe('http://example.com/foo/baz'); 103 | }); 104 | 105 | it('throws missing key error if path parameter for inputUrl is missing', () => { 106 | expect(() => 107 | createUrl('/{FOO}/{BAR}', { 108 | baseUrl: 'http://example.com', 109 | pathParameters: { FOO: 'foo' }, 110 | }) 111 | ).toThrow('Missing or mistyped values for URL path replacement: BAR'); 112 | }); 113 | 114 | it('throws mistyped key error if path parameter for inputUrl is not string', () => { 115 | expect(() => 116 | createUrl('/{FOO}/{BAR}', { 117 | baseUrl: 'http://example.com', 118 | pathParameters: { FOO: 'foo', BAR: [1, 2, 3] }, 119 | }) 120 | ).toThrow('Missing or mistyped values for URL path replacement: BAR'); 121 | }); 122 | }); 123 | 124 | describe('variablesToHttpMap', () => { 125 | it.each([ 126 | [{}, ok({})], 127 | [ 128 | { foo: 'string', bar: ['s1', 's2'] }, 129 | ok({ foo: 'string', bar: ['s1', 's2'] }), 130 | ], 131 | [ 132 | { foo: undefined, bar: ['s1', null, undefined, 's2'], baz: null }, 133 | ok({ bar: ['s1', 's2'] }), 134 | ], 135 | [ 136 | { foo: 1, bar: [true, false] }, 137 | ok({ foo: '1', bar: ['true', 'false'] }), 138 | ], 139 | [{ foo: Buffer.from([]) }, err(['foo', Buffer.from([])])], 140 | [{ foo: ['s1', {}] }, err(['foo', {}])], 141 | [{ foo: ['s1', []] }, err(['foo', []])], 142 | ])('correctly handles "%s"', (value, expected) => { 143 | const result = variablesToHttpMap(value); 144 | expect(result).toStrictEqual(expected); 145 | }); 146 | }); 147 | 148 | describe('getHeaderMulti', () => { 149 | const map: HttpMultiMap = { 150 | foo: 'string', 151 | bar: ['a', 'b'], 152 | }; 153 | 154 | it.each([ 155 | ['none', undefined], 156 | ['foo', ['string']], 157 | ['bar', ['a', 'b']], 158 | ])('correctly handles "%s"', (key, expected) => { 159 | expect(getHeaderMulti(map, key)).toStrictEqual(expected); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/core/interpreter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './external-handler'; 2 | export * from './http'; 3 | export * from './interfaces'; 4 | export * from './map-interpreter'; 5 | export * from './map-interpreter.errors'; 6 | export * from './profile-parameter-validator'; 7 | export * from './profile-parameter-validator.errors'; 8 | -------------------------------------------------------------------------------- /src/core/interpreter/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ComlinkAssignmentNode, 3 | ComlinkListLiteralNode, 4 | ComlinkObjectLiteralNode, 5 | ComlinkPrimitiveLiteralNode, 6 | EnumDefinitionNode, 7 | EnumValueNode, 8 | FieldDefinitionNode, 9 | ListDefinitionNode, 10 | ModelTypeNameNode, 11 | NamedFieldDefinitionNode, 12 | NamedModelDefinitionNode, 13 | NonNullDefinitionNode, 14 | ObjectDefinitionNode, 15 | PrimitiveTypeNameNode, 16 | ProfileASTNode, 17 | ProfileDocumentNode, 18 | ProfileHeaderNode, 19 | UnionDefinitionNode, 20 | UseCaseDefinitionNode, 21 | UseCaseExampleNode, 22 | UseCaseSlotDefinitionNode, 23 | } from '@superfaceai/ast'; 24 | 25 | export interface ProfileVisitor { 26 | visit(node: ProfileASTNode, ...parameters: unknown[]): unknown; 27 | visitComlinkListLiteralNode( 28 | node: ComlinkListLiteralNode, 29 | ...parameters: unknown[] 30 | ): unknown; 31 | visitComlinkObjectLiteralNode( 32 | node: ComlinkObjectLiteralNode, 33 | ...parameters: unknown[] 34 | ): unknown; 35 | visitComlinkPrimitiveLiteralNode( 36 | node: ComlinkPrimitiveLiteralNode, 37 | ...parameters: unknown[] 38 | ): unknown; 39 | visitComlinkPrimitiveLiteralNode( 40 | node: ComlinkPrimitiveLiteralNode, 41 | ...parameters: unknown[] 42 | ): unknown; 43 | visitComlinkAssignmentNode( 44 | node: ComlinkAssignmentNode, 45 | ...parameters: unknown[] 46 | ): unknown; 47 | visitEnumDefinitionNode( 48 | node: EnumDefinitionNode, 49 | ...parameters: unknown[] 50 | ): unknown; 51 | visitEnumValueNode(node: EnumValueNode, ...parameters: unknown[]): unknown; 52 | visitFieldDefinitionNode( 53 | node: FieldDefinitionNode, 54 | ...parameters: unknown[] 55 | ): unknown; 56 | visitListDefinitionNode( 57 | node: ListDefinitionNode, 58 | ...parameters: unknown[] 59 | ): unknown; 60 | visitModelTypeNameNode( 61 | node: ModelTypeNameNode, 62 | ...parameters: unknown[] 63 | ): unknown; 64 | visitNamedFieldDefinitionNode( 65 | node: NamedFieldDefinitionNode, 66 | ...parameters: unknown[] 67 | ): unknown; 68 | visitNamedModelDefinitionNode( 69 | node: NamedModelDefinitionNode, 70 | ...parameters: unknown[] 71 | ): unknown; 72 | visitNonNullDefinitionNode( 73 | node: NonNullDefinitionNode, 74 | ...parameters: unknown[] 75 | ): unknown; 76 | visitObjectDefinitionNode( 77 | node: ObjectDefinitionNode, 78 | ...parameters: unknown[] 79 | ): unknown; 80 | visitPrimitiveTypeNameNode( 81 | node: PrimitiveTypeNameNode, 82 | ...parameters: unknown[] 83 | ): unknown; 84 | visitProfileDocumentNode( 85 | node: ProfileDocumentNode, 86 | ...parameters: unknown[] 87 | ): unknown; 88 | visitProfileHeaderNode( 89 | node: ProfileHeaderNode, 90 | ...parameters: unknown[] 91 | ): unknown; 92 | visitUnionDefinitionNode( 93 | node: UnionDefinitionNode, 94 | ...parameters: unknown[] 95 | ): unknown; 96 | visitUseCaseDefinitionNode( 97 | node: UseCaseDefinitionNode, 98 | ...parameters: unknown[] 99 | ): unknown; 100 | visitUseCaseExampleNode( 101 | node: UseCaseExampleNode, 102 | ...parameters: unknown[] 103 | ): unknown; 104 | visitUseCaseSlotDefinitionNode( 105 | node: UseCaseSlotDefinitionNode, 106 | ...parameters: unknown[] 107 | ): unknown; 108 | } 109 | -------------------------------------------------------------------------------- /src/core/interpreter/profile-parameter-validator.errors.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IInputValidationError, 3 | IResultValidationError, 4 | } from '../../interfaces'; 5 | import { ErrorBase, UnexpectedError } from '../../lib'; 6 | 7 | export type ErrorContext = { path?: string[] }; 8 | export type ValidationError = 9 | | { 10 | kind: 'wrongInput'; 11 | context?: ErrorContext; 12 | } 13 | | { 14 | kind: 'enumValue'; 15 | context?: ErrorContext & { actual: string }; 16 | } 17 | | { 18 | kind: 'wrongType'; 19 | context: ErrorContext & { expected: string; actual: string }; 20 | } 21 | | { kind: 'notArray'; context: ErrorContext & { input: unknown } } 22 | | { kind: 'wrongUnion'; context: ErrorContext & { expected: string[] } } 23 | | { 24 | kind: 'elementsInArrayWrong'; 25 | context: ErrorContext & { suberrors: ValidationError[] }; 26 | } 27 | | { 28 | kind: 'missingRequired'; 29 | context?: ErrorContext & { field: string }; 30 | } 31 | | { 32 | kind: 'nullInNonNullable'; 33 | context?: ErrorContext & { field: string }; 34 | }; 35 | 36 | export function isWrongTypeError(err: ValidationError): err is { 37 | kind: 'wrongType'; 38 | context: { expected: string; actual: string }; 39 | } { 40 | return err.kind === 'wrongType'; 41 | } 42 | 43 | export function addFieldToErrors( 44 | errors: ValidationError[], 45 | field: string 46 | ): ValidationError[] { 47 | return errors.map(err => 48 | err.kind === 'missingRequired' 49 | ? { ...err, context: { ...err.context, field } } 50 | : err 51 | ); 52 | } 53 | 54 | export function formatErrors(errors?: ValidationError[]): string { 55 | if (!errors) { 56 | return 'Unknown error'; 57 | } 58 | 59 | return errors 60 | .map(err => { 61 | const prefix = err.context?.path 62 | ? `Path: ${err.context.path.join('.')}\nError: ` 63 | : 'Error: '; 64 | switch (err.kind) { 65 | case 'wrongType': 66 | return `${prefix}Wrong type: expected ${err.context.expected}, but got ${err.context.actual}`; 67 | 68 | case 'notArray': 69 | return `${prefix}${JSON.stringify( 70 | err.context.input 71 | )} is not an array`; 72 | 73 | case 'missingRequired': 74 | return `${prefix}Missing required field`; 75 | 76 | case 'wrongUnion': 77 | return `${prefix}Result does not satisfy union: expected one of: ${err.context.expected.join( 78 | ', ' 79 | )}`; 80 | 81 | case 'elementsInArrayWrong': 82 | return `${prefix}Some elements in array do not match criteria:\n${formatErrors( 83 | err.context.suberrors 84 | )}`; 85 | 86 | case 'enumValue': 87 | return ( 88 | `${prefix}Invalid enum value` + 89 | (err.context !== undefined ? `: ${err.context?.actual}` : '') 90 | ); 91 | 92 | case 'wrongInput': 93 | return 'Wrong input'; 94 | 95 | case 'nullInNonNullable': 96 | return `${prefix}Null in non-nullable field`; 97 | 98 | default: 99 | throw new UnexpectedError('Invalid error!'); 100 | } 101 | }) 102 | .join('\n'); 103 | } 104 | 105 | export class InputValidationError 106 | extends ErrorBase 107 | implements IInputValidationError 108 | { 109 | public name = 'InputValidationError' as const; 110 | 111 | constructor(public errors?: ValidationError[]) { 112 | super( 113 | 'InputValidationError', 114 | 'Input validation failed:' + '\n' + formatErrors(errors) 115 | ); 116 | } 117 | } 118 | 119 | export class ResultValidationError 120 | extends ErrorBase 121 | implements IResultValidationError 122 | { 123 | public name = 'ResultValidationError' as const; 124 | 125 | constructor(public errors?: ValidationError[]) { 126 | super( 127 | 'ResultValidationError', 128 | 'Result validation failed:' + '\n' + formatErrors(errors) 129 | ); 130 | } 131 | } 132 | 133 | export const isInputValidationError = ( 134 | err: unknown 135 | ): err is InputValidationError => { 136 | return err instanceof InputValidationError; 137 | }; 138 | 139 | export const isResultValidationError = ( 140 | err: unknown 141 | ): err is ResultValidationError => { 142 | return err instanceof ResultValidationError; 143 | }; 144 | -------------------------------------------------------------------------------- /src/core/interpreter/stdlib/index.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger, LogFunction } from '../../../interfaces'; 2 | import { deepFreeze } from '../../../lib'; 3 | 4 | const DEBUG_NAMESPACE = 'debug-log'; 5 | 6 | const STDLIB_UNSTABLE = (debugLog?: LogFunction) => ({ 7 | time: { 8 | isoDateToUnixTimestamp(iso: string): number { 9 | return new Date(iso).getTime(); 10 | }, 11 | unixTimestampToIsoDate(unix: number): string { 12 | return new Date(unix).toISOString(); 13 | }, 14 | }, 15 | debug: { 16 | log(formatter: string, ...args: unknown[]): void { 17 | return debugLog?.(formatter, ...args); 18 | }, 19 | }, 20 | }); 21 | 22 | const STDLIB = (debugLog?: LogFunction) => 23 | deepFreeze({ 24 | unstable: STDLIB_UNSTABLE(debugLog), 25 | }); 26 | 27 | export type Stdlib = ReturnType; 28 | 29 | export function getStdlib(logger?: ILogger): Stdlib { 30 | const debugLog = logger?.log(DEBUG_NAMESPACE); 31 | 32 | // TODO: This should later decide whether to return debug functions or just their stubs 33 | return STDLIB(debugLog); 34 | } 35 | -------------------------------------------------------------------------------- /src/core/profile-provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bound-profile-provider'; 2 | export * from './profile-provider'; 3 | export * from './security'; 4 | export * from './profile-provider-configuration'; 5 | export * from './resolve-map-ast'; 6 | -------------------------------------------------------------------------------- /src/core/profile-provider/parameters.test.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderJson } from '@superfaceai/ast'; 2 | 3 | import { resolveIntegrationParameters } from './parameters'; 4 | 5 | describe('resolveIntegrationParameters', () => { 6 | let mockProviderJson: ProviderJson; 7 | 8 | beforeEach(() => { 9 | mockProviderJson = { 10 | name: 'test', 11 | services: [{ id: 'test-service', baseUrl: 'service/base/url' }], 12 | securitySchemes: [], 13 | defaultService: 'test-service', 14 | parameters: [ 15 | { 16 | name: 'first', 17 | description: 'first test value', 18 | }, 19 | { 20 | name: 'second', 21 | }, 22 | { 23 | name: 'third', 24 | default: 'third-default', 25 | }, 26 | { 27 | name: 'fourth', 28 | default: 'fourth-default', 29 | }, 30 | ], 31 | }; 32 | }); 33 | 34 | afterEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | 38 | it('returns undefined when parameters are undefined', () => { 39 | expect(resolveIntegrationParameters(mockProviderJson)).toBeUndefined(); 40 | }); 41 | 42 | it('prints warning when unknown parameter is used', () => { 43 | const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); 44 | 45 | mockProviderJson.parameters = undefined; 46 | expect( 47 | resolveIntegrationParameters(mockProviderJson, { test: 'test' }) 48 | ).toEqual({ test: 'test' }); 49 | 50 | expect(consoleSpy).toHaveBeenCalledWith( 51 | 'Warning: Super.json defines integration parameters but provider.json does not' 52 | ); 53 | }); 54 | 55 | it('returns resolved parameters', () => { 56 | expect( 57 | resolveIntegrationParameters(mockProviderJson, { 58 | first: 'plain value', 59 | second: '$TEST_SECOND', // unset env value without default 60 | third: '$TEST_THIRD', // unset env value with default 61 | // fourth is missing - should be resolved to its default 62 | }) 63 | ).toEqual({ 64 | first: 'plain value', 65 | second: '$TEST_SECOND', 66 | third: 'third-default', 67 | fourth: 'fourth-default', 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/core/profile-provider/parameters.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderJson } from '@superfaceai/ast'; 2 | import { prepareProviderParameters } from '@superfaceai/ast'; 3 | 4 | export function resolveIntegrationParameters( 5 | providerJson: ProviderJson, 6 | parameters?: Record 7 | ): Record | undefined { 8 | if (parameters === undefined) { 9 | return undefined; 10 | } 11 | 12 | const providerJsonParameters = providerJson.parameters || []; 13 | if ( 14 | Object.keys(parameters).length !== 0 && 15 | providerJsonParameters.length === 0 16 | ) { 17 | console.warn( 18 | 'Warning: Super.json defines integration parameters but provider.json does not' 19 | ); 20 | } 21 | const result: Record = {}; 22 | 23 | const preparedParameters = prepareProviderParameters( 24 | providerJson.name, 25 | providerJsonParameters 26 | ); 27 | 28 | // Resolve parameters defined in super.json 29 | for (const [key, value] of Object.entries(parameters)) { 30 | const providerJsonParameter = providerJsonParameters.find( 31 | parameter => parameter.name === key 32 | ); 33 | // If value name and prepared value equals we are dealing with unset env 34 | if ( 35 | providerJsonParameter && 36 | preparedParameters[providerJsonParameter.name] === value 37 | ) { 38 | if (providerJsonParameter.default !== undefined) { 39 | result[key] = providerJsonParameter.default; 40 | } 41 | } 42 | 43 | // Use original value 44 | if (!result[key]) { 45 | result[key] = value; 46 | } 47 | } 48 | 49 | // Resolve parameters which are missing in super.json and have default value 50 | for (const parameter of providerJsonParameters) { 51 | if ( 52 | result[parameter.name] === undefined && 53 | parameter.default !== undefined 54 | ) { 55 | result[parameter.name] = parameter.default; 56 | } 57 | } 58 | 59 | return result; 60 | } 61 | -------------------------------------------------------------------------------- /src/core/profile-provider/profile-provider-configuration.ts: -------------------------------------------------------------------------------- 1 | export class ProfileProviderConfiguration { 2 | constructor( 3 | public readonly revision?: string, 4 | public readonly variant?: string 5 | ) {} 6 | 7 | public get cacheKey(): string { 8 | // TODO: Research a better way? 9 | return JSON.stringify(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/core/profile-provider/resolve-map-ast.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MapDocumentNode, 3 | NormalizedSuperJsonDocument, 4 | } from '@superfaceai/ast'; 5 | import { assertMapDocumentNode, EXTENSIONS } from '@superfaceai/ast'; 6 | 7 | import type { IConfig, IFileSystem, ILogger } from '../../interfaces'; 8 | import { isSettingsWithAst, UnexpectedError } from '../../lib'; 9 | import { 10 | profileIdsDoNotMatchError, 11 | profileNotFoundError, 12 | profileProviderNotFoundError, 13 | providersDoNotMatchError, 14 | referencedFileNotFoundError, 15 | sourceFileExtensionFoundError, 16 | unsupportedFileExtensionError, 17 | variantMismatchError, 18 | } from '../errors'; 19 | 20 | const DEBUG_NAMESPACE = 'map-file-resolution'; 21 | 22 | export async function resolveMapAst({ 23 | profileId, 24 | providerName, 25 | variant, 26 | superJson, 27 | fileSystem, 28 | logger, 29 | config, 30 | }: { 31 | profileId: string; 32 | providerName: string; 33 | variant?: string; 34 | logger?: ILogger; 35 | superJson: NormalizedSuperJsonDocument | undefined; 36 | fileSystem: IFileSystem; 37 | config: IConfig; 38 | }): Promise { 39 | if (superJson === undefined) { 40 | return undefined; 41 | } 42 | const profileSettings = superJson.profiles[profileId]; 43 | 44 | if (profileSettings === undefined) { 45 | throw profileNotFoundError(profileId); 46 | } 47 | 48 | const profileProviderSettings = profileSettings.providers[providerName]; 49 | 50 | if (profileProviderSettings === undefined) { 51 | throw profileProviderNotFoundError(profileId, providerName); 52 | } 53 | 54 | const log = logger?.log(DEBUG_NAMESPACE); 55 | if (isSettingsWithAst(profileProviderSettings)) { 56 | switch (typeof profileProviderSettings.ast) { 57 | case 'string': 58 | return assertMapDocumentNode( 59 | JSON.parse(String(profileProviderSettings.ast)) 60 | ); 61 | case 'object': 62 | return assertMapDocumentNode(profileProviderSettings.ast); 63 | default: 64 | throw new UnexpectedError( 65 | `Unsupported ast format ${typeof profileProviderSettings.ast}` 66 | ); 67 | } 68 | } else if ('file' in profileProviderSettings) { 69 | let path: string; 70 | if (profileProviderSettings.file.endsWith(EXTENSIONS.map.source)) { 71 | path = fileSystem.path.resolve( 72 | fileSystem.path.dirname(config.superfacePath), 73 | profileProviderSettings.file.replace( 74 | EXTENSIONS.map.source, 75 | EXTENSIONS.map.build 76 | ) 77 | ); 78 | // check if ast exists to print usefull error (needs to be compiled) 79 | if (!(await fileSystem.exists(path))) { 80 | throw sourceFileExtensionFoundError(EXTENSIONS.map.source); 81 | } 82 | } else if (profileProviderSettings.file.endsWith(EXTENSIONS.map.build)) { 83 | path = fileSystem.path.resolve( 84 | fileSystem.path.dirname(config.superfacePath), 85 | profileProviderSettings.file 86 | ); 87 | } else { 88 | throw unsupportedFileExtensionError( 89 | profileProviderSettings.file, 90 | EXTENSIONS.map.source 91 | ); 92 | } 93 | 94 | log?.(`Reading compiled map from path: "${path}"`); 95 | 96 | const contents = await fileSystem.readFile( 97 | fileSystem.path.resolve( 98 | fileSystem.path.dirname(config.superfacePath), 99 | path 100 | ) 101 | ); 102 | 103 | if (contents.isErr()) { 104 | throw referencedFileNotFoundError(path, []); 105 | } 106 | 107 | const ast = assertMapDocumentNode(JSON.parse(contents.value)); 108 | 109 | // check if variant match 110 | if (variant !== ast.header.variant) { 111 | throw variantMismatchError(ast.header.variant, variant); 112 | } 113 | // check if provider name match 114 | if (providerName !== ast.header.provider) { 115 | throw providersDoNotMatchError(ast.header.provider, providerName, 'map'); 116 | } 117 | // check if profile id match 118 | const astProfileId = 119 | ast.header.profile.scope !== undefined 120 | ? `${ast.header.profile.scope}/${ast.header.profile.name}` 121 | : ast.header.profile.name; 122 | if (astProfileId !== profileId) { 123 | throw profileIdsDoNotMatchError(astProfileId, profileId); 124 | } 125 | 126 | return ast; 127 | } 128 | 129 | return undefined; 130 | } 131 | -------------------------------------------------------------------------------- /src/core/profile-provider/security.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityScheme, SecurityValues } from '@superfaceai/ast'; 2 | import { 3 | HttpScheme, 4 | isApiKeySecurityValues, 5 | isBasicAuthSecurityValues, 6 | isBearerTokenSecurityValues, 7 | isDigestSecurityValues, 8 | SecurityType, 9 | } from '@superfaceai/ast'; 10 | 11 | import { invalidSecurityValuesError, securityNotFoundError } from '../errors'; 12 | import type { SecurityConfiguration } from '../interpreter'; 13 | 14 | export function resolveSecurityConfiguration( 15 | schemes: SecurityScheme[], 16 | values: SecurityValues[], 17 | providerName: string 18 | ): SecurityConfiguration[] { 19 | const result: SecurityConfiguration[] = []; 20 | 21 | for (const vals of values) { 22 | const scheme = schemes.find(scheme => scheme.id === vals.id); 23 | if (scheme === undefined) { 24 | const definedSchemes = schemes.map(s => s.id); 25 | throw securityNotFoundError(providerName, definedSchemes, vals); 26 | } 27 | 28 | const invalidSchemeValuesErrorBuilder = ( 29 | scheme: SecurityScheme, 30 | values: SecurityValues, 31 | requiredKeys: [string, ...string[]] 32 | ) => { 33 | const valueKeys = Object.keys(values).filter(k => k !== 'id'); 34 | 35 | return invalidSecurityValuesError( 36 | providerName, 37 | scheme.type, 38 | scheme.id, 39 | valueKeys, 40 | requiredKeys 41 | ); 42 | }; 43 | 44 | if (scheme.type === SecurityType.APIKEY) { 45 | if (!isApiKeySecurityValues(vals)) { 46 | throw invalidSchemeValuesErrorBuilder(scheme, vals, ['apikey']); 47 | } 48 | 49 | result.push({ 50 | ...scheme, 51 | ...vals, 52 | }); 53 | } else { 54 | switch (scheme.scheme) { 55 | case HttpScheme.BASIC: 56 | if (!isBasicAuthSecurityValues(vals)) { 57 | throw invalidSchemeValuesErrorBuilder(scheme, vals, [ 58 | 'username', 59 | 'password', 60 | ]); 61 | } 62 | 63 | result.push({ 64 | ...scheme, 65 | ...vals, 66 | }); 67 | break; 68 | 69 | case HttpScheme.BEARER: 70 | if (!isBearerTokenSecurityValues(vals)) { 71 | throw invalidSchemeValuesErrorBuilder(scheme, vals, ['token']); 72 | } 73 | 74 | result.push({ 75 | ...scheme, 76 | ...vals, 77 | }); 78 | break; 79 | 80 | case HttpScheme.DIGEST: 81 | if (!isDigestSecurityValues(vals)) { 82 | throw invalidSchemeValuesErrorBuilder(scheme, vals, ['digest']); 83 | } 84 | 85 | result.push({ 86 | ...scheme, 87 | ...vals, 88 | }); 89 | break; 90 | } 91 | } 92 | } 93 | 94 | return result; 95 | } 96 | -------------------------------------------------------------------------------- /src/core/profile/cache-profile-ast.ts: -------------------------------------------------------------------------------- 1 | import type { ProfileDocumentNode } from '@superfaceai/ast'; 2 | import { EXTENSIONS, isProfileDocumentNode } from '@superfaceai/ast'; 3 | 4 | import type { IConfig, IFileSystem, LogFunction } from '../../interfaces'; 5 | 6 | export async function tryToLoadCachedAst({ 7 | profileId, 8 | version, 9 | fileSystem, 10 | config, 11 | log, 12 | }: { 13 | profileId: string; 14 | version: string; 15 | fileSystem: IFileSystem; 16 | config: IConfig; 17 | log?: LogFunction; 18 | }): Promise { 19 | if (config.cache === false) { 20 | return undefined; 21 | } 22 | 23 | const profileCachePath = fileSystem.path.join( 24 | config.cachePath, 25 | 'profiles', 26 | `${profileId}@${version}${EXTENSIONS.profile.build}` 27 | ); 28 | 29 | const contents = await fileSystem.readFile(profileCachePath); 30 | // Try to load 31 | if (contents.isErr()) { 32 | log?.( 33 | 'Reading of cached profile file failed with error %O', 34 | contents.error 35 | ); 36 | 37 | return undefined; 38 | } 39 | // Try to parse 40 | let possibleAst: unknown; 41 | try { 42 | possibleAst = JSON.parse(contents.value); 43 | } catch (error) { 44 | log?.('Parsing of cached profile file failed with error %O', error); 45 | 46 | return undefined; 47 | } 48 | 49 | // Check if valid ProfileDocumentNode 50 | if (!isProfileDocumentNode(possibleAst)) { 51 | log?.('Cached profile file is not valid ProfileDocumentNode'); 52 | 53 | return undefined; 54 | } 55 | 56 | // Check id 57 | const cachedId: string = 58 | possibleAst.header.scope !== undefined 59 | ? `${possibleAst.header.scope}/${possibleAst.header.name}` 60 | : possibleAst.header.name; 61 | if (profileId !== cachedId) { 62 | log?.( 63 | 'Cached profile id (%S) does not matched to used id (%S)', 64 | cachedId, 65 | profileId 66 | ); 67 | 68 | return undefined; 69 | } 70 | 71 | // Check version 72 | let cachedVersion = `${possibleAst.header.version.major}.${possibleAst.header.version.minor}.${possibleAst.header.version.patch}`; 73 | if (possibleAst.header.version.label !== undefined) { 74 | cachedVersion += `-${possibleAst.header.version.label}`; 75 | } 76 | 77 | if (version !== cachedVersion) { 78 | log?.( 79 | 'Cached profile version (%S) does not matched to used version (%S)', 80 | cachedVersion, 81 | version 82 | ); 83 | 84 | return undefined; 85 | } 86 | 87 | return possibleAst; 88 | } 89 | 90 | export async function cacheProfileAst({ 91 | ast, 92 | version, 93 | fileSystem, 94 | config, 95 | log, 96 | }: { 97 | ast: ProfileDocumentNode; 98 | version: string; 99 | fileSystem: IFileSystem; 100 | config: IConfig; 101 | log?: LogFunction; 102 | }): Promise { 103 | const profileCachePath = 104 | ast.header.scope !== undefined 105 | ? fileSystem.path.join(config.cachePath, 'profiles', ast.header.scope) 106 | : fileSystem.path.join(config.cachePath, 'profiles'); 107 | 108 | if (config.cache === true) { 109 | try { 110 | await fileSystem.mkdir(profileCachePath, { 111 | recursive: true, 112 | }); 113 | const path = fileSystem.path.join( 114 | profileCachePath, 115 | `${ast.header.name}@${version}${EXTENSIONS.profile.build}` 116 | ); 117 | await fileSystem.writeFile(path, JSON.stringify(ast, undefined, 2)); 118 | } catch (error) { 119 | log?.(`Failed to cache profile AST for ${ast.header.name}: %O`, error); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/core/profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile'; 2 | export * from './profile.typed'; 3 | export * from './profile-configuration'; 4 | export * from './resolve-profile-ast'; 5 | -------------------------------------------------------------------------------- /src/core/profile/profile-configuration.ts: -------------------------------------------------------------------------------- 1 | export class ProfileConfiguration { 2 | constructor(public readonly id: string, public readonly version: string) {} 3 | 4 | public get cacheKey(): string { 5 | // TODO: Research a better way? 6 | return JSON.stringify(this); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/core/profile/profile.test.ts: -------------------------------------------------------------------------------- 1 | import type { SuperJsonDocument } from '@superfaceai/ast'; 2 | 3 | import { SuperCache } from '../../lib'; 4 | import { mockProfileDocumentNode, MockTimers } from '../../mock'; 5 | import { 6 | NodeCrypto, 7 | NodeEnvironment, 8 | NodeFetch, 9 | NodeFileSystem, 10 | } from '../../node'; 11 | import { normalizeSuperJsonDocument } from '../../schema-tools/superjson/normalize'; 12 | import { Config } from '../config'; 13 | import { usecaseNotFoundError } from '../errors'; 14 | import { Events } from '../events'; 15 | import type { IBoundProfileProvider } from '../profile-provider'; 16 | import { PureJSSandbox } from '../sandbox'; 17 | import { Profile } from './profile'; 18 | import { ProfileConfiguration } from './profile-configuration'; 19 | 20 | const crypto = new NodeCrypto(); 21 | 22 | function createProfile(superJson: SuperJsonDocument): Profile { 23 | const timers = new MockTimers(); 24 | const events = new Events(timers); 25 | const cache = new SuperCache<{ 26 | provider: IBoundProfileProvider; 27 | expiresAt: number; 28 | }>(); 29 | const config = new Config(NodeFileSystem); 30 | const sandbox = new PureJSSandbox(); 31 | const ast = mockProfileDocumentNode({ usecaseName: 'sayHello' }); 32 | const configuration = new ProfileConfiguration('test', '1.0.0'); 33 | 34 | return new Profile( 35 | configuration, 36 | ast, 37 | events, 38 | normalizeSuperJsonDocument(superJson, new NodeEnvironment()), 39 | config, 40 | sandbox, 41 | timers, 42 | NodeFileSystem, 43 | cache, 44 | crypto, 45 | new NodeFetch(timers) 46 | ); 47 | } 48 | 49 | describe('Profile', () => { 50 | describe('when calling getUseCases', () => { 51 | it('should return new UseCase', async () => { 52 | const superJson = { 53 | profiles: { 54 | test: { 55 | version: '1.0.0', 56 | }, 57 | }, 58 | providers: {}, 59 | }; 60 | const profile = createProfile(superJson); 61 | 62 | expect(profile.getUseCase('sayHello')).toMatchObject({ 63 | name: 'sayHello', 64 | }); 65 | }); 66 | 67 | it('should throw on non existent use case name', async () => { 68 | const superJson = { 69 | profiles: { 70 | test: { 71 | version: '1.0.0', 72 | }, 73 | }, 74 | providers: {}, 75 | }; 76 | const profile = createProfile(superJson); 77 | 78 | expect(() => profile.getUseCase('made-up')).toThrow( 79 | usecaseNotFoundError('made-up', ['sayHello']) 80 | ); 81 | }); 82 | }); 83 | 84 | it('should call getConfiguredProviders correctly', async () => { 85 | const superJson = { 86 | profiles: { 87 | test: { 88 | version: '1.0.0', 89 | providers: { 90 | first: { 91 | file: '../some.suma', 92 | }, 93 | second: { 94 | file: '../some.suma', 95 | }, 96 | }, 97 | }, 98 | }, 99 | providers: { 100 | first: { 101 | file: '../provider.json', 102 | }, 103 | }, 104 | }; 105 | const profile = createProfile(superJson); 106 | 107 | expect(profile.getConfiguredProviders()).toEqual(['first', 'second']); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/core/profile/profile.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NormalizedSuperJsonDocument, 3 | ProfileDocumentNode, 4 | } from '@superfaceai/ast'; 5 | import { isUseCaseDefinitionNode } from '@superfaceai/ast'; 6 | 7 | import type { 8 | IConfig, 9 | ICrypto, 10 | IFileSystem, 11 | ILogger, 12 | IProfile, 13 | ITimers, 14 | } from '../../interfaces'; 15 | import type { ISandbox } from '../../interfaces/sandbox'; 16 | import type { SuperCache } from '../../lib'; 17 | import { usecaseNotFoundError } from '../errors'; 18 | import type { Events, Interceptable } from '../events'; 19 | import type { AuthCache, IFetch } from '../interpreter'; 20 | import type { IBoundProfileProvider } from '../profile-provider'; 21 | import { UseCase } from '../usecase'; 22 | import type { ProfileConfiguration } from './profile-configuration'; 23 | 24 | export abstract class ProfileBase { 25 | constructor( 26 | public readonly configuration: ProfileConfiguration, 27 | public readonly ast: ProfileDocumentNode, 28 | protected readonly events: Events, 29 | protected readonly superJson: NormalizedSuperJsonDocument | undefined, 30 | protected readonly config: IConfig, 31 | protected readonly sandbox: ISandbox, 32 | protected readonly timers: ITimers, 33 | protected readonly fileSystem: IFileSystem, 34 | protected readonly boundProfileProviderCache: SuperCache<{ 35 | provider: IBoundProfileProvider; 36 | expiresAt: number; 37 | }>, 38 | protected readonly crypto: ICrypto, 39 | protected readonly fetchInstance: IFetch & Interceptable & AuthCache, 40 | protected readonly logger?: ILogger 41 | ) {} 42 | 43 | public getConfiguredProviders(): string[] { 44 | return Object.keys( 45 | this.superJson?.profiles[this.configuration.id]?.providers ?? {} 46 | ); 47 | } 48 | } 49 | 50 | export class Profile extends ProfileBase implements IProfile { 51 | public getUseCase(name: string): UseCase { 52 | const supportedUsecaseNames = this.ast.definitions 53 | .filter(isUseCaseDefinitionNode) 54 | .map(u => u.useCaseName); 55 | if (!supportedUsecaseNames.includes(name)) { 56 | throw usecaseNotFoundError(name, supportedUsecaseNames); 57 | } 58 | 59 | return new UseCase( 60 | this, 61 | name, 62 | this.events, 63 | this.config, 64 | this.sandbox, 65 | this.superJson, 66 | this.timers, 67 | this.fileSystem, 68 | this.crypto, 69 | this.boundProfileProviderCache, 70 | this.fetchInstance, 71 | this.logger 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/core/profile/profile.typed.test.ts: -------------------------------------------------------------------------------- 1 | import { SuperCache } from '../../lib'; 2 | import { 3 | MockEnvironment, 4 | MockFileSystem, 5 | mockProfileDocumentNode, 6 | MockTimers, 7 | } from '../../mock'; 8 | import { NodeCrypto, NodeFetch, NodeFileSystem } from '../../node'; 9 | import { normalizeSuperJsonDocument } from '../../schema-tools/superjson/normalize'; 10 | import { Config } from '../config'; 11 | import { Events } from '../events'; 12 | import type { IBoundProfileProvider } from '../profile-provider'; 13 | import { PureJSSandbox } from '../sandbox'; 14 | import { UseCase } from '../usecase'; 15 | import { TypedProfile } from './profile.typed'; 16 | import { ProfileConfiguration } from './profile-configuration'; 17 | 18 | const crypto = new NodeCrypto(); 19 | 20 | describe('TypedProfile', () => { 21 | const mockSuperJson = normalizeSuperJsonDocument( 22 | { 23 | profiles: { 24 | test: { 25 | version: '1.0.0', 26 | }, 27 | }, 28 | providers: {}, 29 | }, 30 | new MockEnvironment() 31 | ); 32 | const mockProfileConfiguration = new ProfileConfiguration('test', '1.0.0'); 33 | const ast = mockProfileDocumentNode(); 34 | 35 | const timers = new MockTimers(); 36 | const events = new Events(timers); 37 | const cache = new SuperCache<{ 38 | provider: IBoundProfileProvider; 39 | expiresAt: number; 40 | }>(); 41 | const config = new Config(NodeFileSystem); 42 | const sandbox = new PureJSSandbox(); 43 | const fileSystem = MockFileSystem(); 44 | 45 | describe('getUseCases', () => { 46 | it('should get usecase correctly', async () => { 47 | const typedProfile = new TypedProfile( 48 | mockProfileConfiguration, 49 | ast, 50 | events, 51 | mockSuperJson, 52 | cache, 53 | config, 54 | sandbox, 55 | timers, 56 | fileSystem, 57 | crypto, 58 | new NodeFetch(timers), 59 | ['sayHello'] 60 | ); 61 | 62 | expect(typedProfile.getUseCase('sayHello')).toEqual( 63 | new UseCase( 64 | typedProfile, 65 | 'sayHello', 66 | events, 67 | config, 68 | sandbox, 69 | mockSuperJson, 70 | timers, 71 | fileSystem, 72 | crypto, 73 | cache, 74 | new NodeFetch(timers) 75 | ) 76 | ); 77 | }); 78 | 79 | it('should throw when usecase is not found', async () => { 80 | const typedProfile = new TypedProfile( 81 | mockProfileConfiguration, 82 | mockProfileDocumentNode(), 83 | events, 84 | mockSuperJson, 85 | cache, 86 | config, 87 | sandbox, 88 | timers, 89 | fileSystem, 90 | crypto, 91 | new NodeFetch(timers), 92 | ['sayHello'] 93 | ); 94 | 95 | expect(() => typedProfile.getUseCase('nope')).toThrow( 96 | new RegExp('Usecase not found: "nope"') 97 | ); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/core/profile/profile.typed.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NormalizedSuperJsonDocument, 3 | ProfileDocumentNode, 4 | } from '@superfaceai/ast'; 5 | 6 | import type { 7 | IConfig, 8 | ICrypto, 9 | IFileSystem, 10 | ILogger, 11 | ITimers, 12 | } from '../../interfaces'; 13 | import type { ISandbox } from '../../interfaces/sandbox'; 14 | import type { NonPrimitive, SuperCache } from '../../lib'; 15 | import { UnexpectedError } from '../../lib'; 16 | import { usecaseNotFoundError } from '../errors'; 17 | import type { Events, Interceptable } from '../events'; 18 | import type { AuthCache, IFetch } from '../interpreter'; 19 | import type { IBoundProfileProvider } from '../profile-provider'; 20 | import { TypedUseCase } from '../usecase'; 21 | import { ProfileBase } from './profile'; 22 | import type { ProfileConfiguration } from './profile-configuration'; 23 | 24 | export type UsecaseType< 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | TInput extends NonPrimitive | undefined = any, 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | TOutput = any 29 | > = { 30 | [name: string]: [TInput, TOutput]; 31 | }; 32 | 33 | export type KnownUsecase = { 34 | [name in keyof TUsecase]: TypedUseCase; 35 | }; 36 | 37 | export class TypedProfile< 38 | // TKnownUsecases extends KnownUsecase 39 | TUsecaseTypes extends UsecaseType 40 | > extends ProfileBase { 41 | private readonly knownUsecases: KnownUsecase; 42 | 43 | constructor( 44 | public override readonly configuration: ProfileConfiguration, 45 | public override readonly ast: ProfileDocumentNode, 46 | protected override readonly events: Events, 47 | protected override readonly superJson: 48 | | NormalizedSuperJsonDocument 49 | | undefined, 50 | protected override readonly boundProfileProviderCache: SuperCache<{ 51 | provider: IBoundProfileProvider; 52 | expiresAt: number; 53 | }>, 54 | protected override readonly config: IConfig, 55 | protected override readonly sandbox: ISandbox, 56 | protected override readonly timers: ITimers, 57 | protected override readonly fileSystem: IFileSystem, 58 | protected override readonly crypto: ICrypto, 59 | protected override readonly fetchInstance: IFetch & 60 | Interceptable & 61 | AuthCache, 62 | usecases: (keyof TUsecaseTypes)[], 63 | protected override readonly logger?: ILogger 64 | ) { 65 | super( 66 | configuration, 67 | ast, 68 | events, 69 | superJson, 70 | config, 71 | sandbox, 72 | timers, 73 | fileSystem, 74 | boundProfileProviderCache, 75 | crypto, 76 | fetchInstance, 77 | logger 78 | ); 79 | this.knownUsecases = usecases.reduce( 80 | (acc, usecase) => ({ 81 | ...acc, 82 | [usecase]: new TypedUseCase< 83 | TUsecaseTypes[typeof usecase][0], 84 | TUsecaseTypes[typeof usecase][1] 85 | >( 86 | this, 87 | usecase as string, 88 | events, 89 | config, 90 | sandbox, 91 | superJson, 92 | timers, 93 | fileSystem, 94 | crypto, 95 | boundProfileProviderCache, 96 | fetchInstance, 97 | logger 98 | ), 99 | }), 100 | {} as KnownUsecase 101 | ); 102 | } 103 | 104 | public get useCases(): KnownUsecase { 105 | if (this.knownUsecases === undefined) { 106 | throw new UnexpectedError( 107 | 'Thou shall not access the typed interface from untyped Profile' 108 | ); 109 | } else { 110 | return this.knownUsecases; 111 | } 112 | } 113 | 114 | public getUseCase>( 115 | name: TName 116 | ): KnownUsecase[TName] { 117 | const usecase = this.knownUsecases?.[name]; 118 | if (!usecase) { 119 | throw usecaseNotFoundError( 120 | name.toString(), 121 | Object.keys(this.knownUsecases) 122 | ); 123 | } 124 | 125 | return usecase; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/core/provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './resolve-provider'; 3 | export * from './resolve-provider-json'; 4 | -------------------------------------------------------------------------------- /src/core/provider/provider.test.ts: -------------------------------------------------------------------------------- 1 | import { Provider, ProviderConfiguration } from './provider'; 2 | 3 | describe('Provider Configuration', () => { 4 | it('should cache key correctly', async () => { 5 | const mockProviderConfiguration = new ProviderConfiguration('test', []); 6 | expect(mockProviderConfiguration.cacheKey).toEqual('{"provider":"test"}'); 7 | }); 8 | }); 9 | 10 | describe('Provider', () => { 11 | it('configures provider correctly', async () => { 12 | const mockProviderConfiguration = new ProviderConfiguration('test', []); 13 | const mockProvider = new Provider(mockProviderConfiguration); 14 | 15 | await expect( 16 | mockProvider.configure({ 17 | security: [ 18 | { 19 | username: 'second', 20 | password: 'seconds', 21 | id: 'second-id', 22 | }, 23 | ], 24 | }) 25 | ).resolves.toEqual( 26 | new Provider( 27 | new ProviderConfiguration('test', [ 28 | { 29 | username: 'second', 30 | password: 'seconds', 31 | id: 'second-id', 32 | }, 33 | ]) 34 | ) 35 | ); 36 | }); 37 | 38 | it('configures provider correctly and merges configuration', async () => { 39 | const mockSecurity = [ 40 | { id: 'first-id', username: 'digest-user', password: 'digest-password' }, 41 | ]; 42 | const mockProviderConfiguration = new ProviderConfiguration( 43 | 'test', 44 | mockSecurity 45 | ); 46 | const mockProvider = new Provider(mockProviderConfiguration); 47 | 48 | await expect(mockProvider.configure()).resolves.toEqual( 49 | new Provider(new ProviderConfiguration('test', mockSecurity)) 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/core/provider/provider.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityValues } from '@superfaceai/ast'; 2 | 3 | import type { IProvider } from '../../interfaces'; 4 | import { mergeSecurity } from '../../schema-tools'; 5 | 6 | export class ProviderConfiguration { 7 | // TODO: where should we store security and parameters when they are passed to getProvider? Maybe Provider instance? 8 | /** @deprecated only for use in testing library */ 9 | public readonly security: SecurityValues[]; 10 | public readonly parameters?: Record; 11 | 12 | constructor( 13 | public readonly name: string, 14 | security: SecurityValues[], 15 | parameters?: Record 16 | ) { 17 | this.security = security; 18 | // Sanitize parameters 19 | if (parameters === undefined || Object.keys(parameters).length === 0) { 20 | this.parameters = undefined; 21 | } else { 22 | this.parameters = parameters; 23 | } 24 | } 25 | 26 | public get cacheKey(): string { 27 | // TODO: Research a better way? 28 | return JSON.stringify({ provider: this.name }); 29 | } 30 | } 31 | 32 | export class Provider implements IProvider { 33 | constructor(public readonly configuration: ProviderConfiguration) {} 34 | 35 | /** @deprecated */ 36 | public async configure(configuration?: { 37 | security?: SecurityValues[]; 38 | }): Promise { 39 | const newConfiguration = new ProviderConfiguration( 40 | this.configuration.name, 41 | mergeSecurity(this.configuration.security, configuration?.security ?? []) 42 | ); 43 | 44 | return new Provider(newConfiguration); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/provider/resolve-provider-json.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NormalizedSuperJsonDocument, 3 | ProviderJson, 4 | } from '@superfaceai/ast'; 5 | import { assertProviderJson } from '@superfaceai/ast'; 6 | 7 | import type { IConfig, IFileSystem, ILogger } from '../../interfaces'; 8 | import { isSettingsWithAst, UnexpectedError } from '../../lib'; 9 | import { 10 | providersDoNotMatchError, 11 | referencedFileNotFoundError, 12 | unconfiguredProviderError, 13 | } from '../errors'; 14 | 15 | const DEBUG_NAMESPACE = 'provider-file-resolution'; 16 | 17 | export async function resolveProviderJson({ 18 | providerName, 19 | superJson, 20 | fileSystem, 21 | logger, 22 | config, 23 | }: { 24 | providerName: string; 25 | logger?: ILogger; 26 | superJson: NormalizedSuperJsonDocument | undefined; 27 | fileSystem: IFileSystem; 28 | config: IConfig; 29 | }): Promise { 30 | if (superJson === undefined) { 31 | return undefined; 32 | } 33 | const providerSettings = superJson.providers[providerName]; 34 | 35 | if (providerSettings === undefined) { 36 | throw unconfiguredProviderError(providerName); 37 | } 38 | 39 | if (isSettingsWithAst(providerSettings)) { 40 | switch (typeof providerSettings.ast) { 41 | case 'string': 42 | return assertProviderJson(JSON.parse(String(providerSettings.ast))); 43 | case 'object': 44 | return assertProviderJson(providerSettings.ast); 45 | default: 46 | throw new UnexpectedError( 47 | `Unsupported ast format ${typeof providerSettings.ast}` 48 | ); 49 | } 50 | } 51 | 52 | if (providerSettings.file === undefined) { 53 | return undefined; 54 | } 55 | 56 | const log = logger?.log(DEBUG_NAMESPACE); 57 | 58 | const path = fileSystem.path.resolve( 59 | fileSystem.path.dirname(config.superfacePath), 60 | providerSettings.file 61 | ); 62 | 63 | log?.(`Reading provider json from path: "${path}"`); 64 | const contents = await fileSystem.readFile(path); 65 | 66 | if (contents.isErr()) { 67 | throw referencedFileNotFoundError(path, []); 68 | } 69 | 70 | const providerJson = assertProviderJson(JSON.parse(contents.value)); 71 | 72 | // check if provider name match 73 | if (providerName !== providerJson.name) { 74 | throw providersDoNotMatchError( 75 | providerJson.name, 76 | providerName, 77 | 'provider.json' 78 | ); 79 | } 80 | 81 | return providerJson; 82 | } 83 | -------------------------------------------------------------------------------- /src/core/provider/resolve-provider.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NormalizedSuperJsonDocument, 3 | SecurityValues, 4 | } from '@superfaceai/ast'; 5 | 6 | import { 7 | noConfiguredProviderError, 8 | profileNotFoundError, 9 | unableToResolveProviderError, 10 | } from '../errors'; 11 | import { Provider, ProviderConfiguration } from './provider'; 12 | 13 | /** 14 | * Resolves ProviderConfiguration from parameters. 15 | * Fallbacks to SuperJson information if provider not specified 16 | */ 17 | export function resolveProvider({ 18 | provider, 19 | security, 20 | parameters, 21 | superJson, 22 | profileId, 23 | }: { 24 | security?: SecurityValues[]; 25 | parameters?: Record; 26 | superJson?: NormalizedSuperJsonDocument; 27 | provider?: string | Provider; 28 | profileId?: string; 29 | }): Provider { 30 | if (provider !== undefined) { 31 | return createProvider({ 32 | provider, 33 | security, 34 | superJson, 35 | parameters, 36 | }); 37 | } 38 | 39 | if (profileId !== undefined) { 40 | if (superJson !== undefined) { 41 | const profileSettings = superJson.profiles[profileId]; 42 | 43 | if (profileSettings === undefined) { 44 | throw profileNotFoundError(profileId); 45 | } 46 | 47 | const priorityProviders = profileSettings.priority; 48 | 49 | if (priorityProviders.length > 0) { 50 | return createProvider({ 51 | provider: priorityProviders[0], 52 | security, 53 | superJson, 54 | parameters, 55 | }); 56 | } 57 | } 58 | 59 | throw noConfiguredProviderError(profileId); 60 | } 61 | // This should be unreachable in common use. We always have defined provider or profile id and super.json 62 | throw unableToResolveProviderError(); 63 | } 64 | 65 | function createProvider({ 66 | provider, 67 | security, 68 | parameters, 69 | superJson, 70 | }: { 71 | security?: SecurityValues[]; 72 | parameters?: Record; 73 | superJson?: NormalizedSuperJsonDocument; 74 | provider: string | Provider; 75 | }): Provider { 76 | if (typeof provider === 'string') { 77 | // Fallback to super json values if possible 78 | const providerSettings = superJson?.providers[provider]; 79 | 80 | return new Provider( 81 | new ProviderConfiguration( 82 | provider, 83 | security ?? providerSettings?.security ?? [], 84 | parameters ?? providerSettings?.parameters 85 | ) 86 | ); 87 | } 88 | 89 | // Pass possibly new security and parameters 90 | return new Provider( 91 | new ProviderConfiguration( 92 | provider.configuration.name, 93 | security ?? provider.configuration.security ?? [], 94 | parameters ?? provider.configuration.parameters 95 | ) 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/core/registry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './registry'; 2 | -------------------------------------------------------------------------------- /src/core/sandbox/index.ts: -------------------------------------------------------------------------------- 1 | import type { IConfig, ILogger } from '../../interfaces'; 2 | import type { ISandbox } from '../../interfaces/sandbox'; 3 | import type { NonPrimitive } from '../../lib'; 4 | import type { Stdlib } from '../interpreter/stdlib'; 5 | 6 | /** 7 | * WARNING: 8 | * 9 | * This isn't sandbox at all, this simply evaluates user provided JavaScript code. 10 | * So smart bad bad user, can do harmful things. 11 | */ 12 | export class PureJSSandbox implements ISandbox { 13 | public evalScript( 14 | _config: IConfig, 15 | js: string, 16 | stdlib?: Stdlib, 17 | _logger?: ILogger, 18 | variableDefinitions?: NonPrimitive 19 | ): unknown { 20 | const scope = { 21 | std: stdlib, 22 | ...variableDefinitions, 23 | }; 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-implied-eval 26 | const func = new Function( 27 | ...Object.keys(scope), 28 | ` 29 | 'use strict'; 30 | const vmResult = ${js} 31 | ; return vmResult; 32 | ` 33 | ); 34 | 35 | return func(...Object.values(scope)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | -------------------------------------------------------------------------------- /src/core/services/services.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderService } from '@superfaceai/ast'; 2 | 3 | export interface IServiceSelector { 4 | getUrl(serviceId?: string): string | undefined; 5 | } 6 | 7 | export class ServiceSelector implements IServiceSelector { 8 | private readonly serviceUrls: Record; 9 | private readonly defaultService?: string; 10 | 11 | constructor(services: ProviderService[], defaultService?: string) { 12 | this.serviceUrls = Object.fromEntries(services.map(s => [s.id, s.baseUrl])); 13 | this.defaultService = defaultService; 14 | } 15 | 16 | public static empty(): ServiceSelector { 17 | return new ServiceSelector([]); 18 | } 19 | 20 | public static withDefaultUrl(baseUrl: string): ServiceSelector { 21 | return new ServiceSelector([{ id: 'default', baseUrl }], 'default'); 22 | } 23 | 24 | /** 25 | * Gets the url of `serviceId`. If `serviceId` is undefined returns url of the default service, or undefined if default service is also undefined. 26 | */ 27 | public getUrl(serviceId?: string): string | undefined { 28 | const service = serviceId ?? this.defaultService; 29 | if (service === undefined) { 30 | return undefined; 31 | } 32 | 33 | return this.serviceUrls[service]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/usecase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './usecase'; 2 | export * from './usecase.typed'; 3 | // export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/core/usecase/usecase.typed.ts: -------------------------------------------------------------------------------- 1 | import type { PerformError, PerformOptions } from '../../interfaces'; 2 | import type { NonPrimitive, Result, UnexpectedError } from '../../lib'; 3 | import { UseCaseBase } from './usecase'; 4 | 5 | export class TypedUseCase< 6 | TInput extends NonPrimitive | undefined, 7 | TOutput 8 | > extends UseCaseBase { 9 | public async perform( 10 | input: TInput, 11 | options?: PerformOptions 12 | ): Promise> { 13 | // Disable failover when user specified provider 14 | // needs to happen here because bindAndPerform is subject to retry from event hooks 15 | // including provider failover 16 | this.toggleFailover(options?.provider === undefined); 17 | 18 | return this.bindAndPerform(input, options); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core/usecase/utils.ts: -------------------------------------------------------------------------------- 1 | // import type { IBinaryData, IChunked } from '../../interfaces'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './lib'; 3 | export * from './node'; 4 | export * from './schema-tools'; 5 | export * from './user-agent'; 6 | -------------------------------------------------------------------------------- /src/interfaces/binary.ts: -------------------------------------------------------------------------------- 1 | export interface IDataContainer { 2 | read(size?: number): Promise; 3 | toStream(): NodeJS.ReadableStream; // FIXME: This should be ECMAScript ReadableStream 4 | } 5 | 6 | export interface IBinaryData { 7 | peek(size?: number): Promise; 8 | read(size?: number): Promise; 9 | getAllData(): Promise; 10 | chunkBy(chunkSize: number): AsyncIterable; 11 | toStream(): NodeJS.ReadableStream; 12 | } 13 | 14 | export interface IInitializable { 15 | initialize(): Promise; 16 | } 17 | 18 | export interface IDestructible { 19 | destroy(): Promise; 20 | } 21 | 22 | export interface IBinaryDataMeta { 23 | readonly name: string | undefined; 24 | readonly mimetype: string | undefined; 25 | readonly size: number | undefined; 26 | } 27 | 28 | export function isBinaryData(input: unknown): input is IBinaryData { 29 | return ( 30 | typeof input === 'object' && 31 | input !== null && 32 | 'peek' in input && 33 | 'getAllData' in input && 34 | 'chunkBy' in input && 35 | 'toStream' in input 36 | ); 37 | } 38 | 39 | export function isInitializable(input: unknown): input is IInitializable { 40 | return typeof input === 'object' && input !== null && 'initialize' in input; 41 | } 42 | 43 | export function isDestructible(input: unknown): input is IDestructible { 44 | return typeof input === 'object' && input !== null && 'destroy' in input; 45 | } 46 | 47 | export function isBinaryDataMeta(input: unknown): input is IBinaryDataMeta { 48 | return ( 49 | typeof input === 'object' && 50 | input !== null && 51 | 'name' in input && 52 | 'mimetype' in input 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/interfaces/client.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityValues } from '@superfaceai/ast'; 2 | 3 | import type { IEvents } from './events'; 4 | import type { IProfile } from './profile'; 5 | import type { IProvider } from './provider'; 6 | 7 | export interface ISuperfaceClient { 8 | getProvider( 9 | providerName: string, 10 | options?: { 11 | parameters?: Record; 12 | security?: 13 | | SecurityValues[] 14 | | { [id: string]: Omit }; 15 | } 16 | ): Promise; 17 | getProfile( 18 | profile: string | { id: string; version?: string } 19 | ): Promise; 20 | getProviderForProfile(profileId: string): Promise; 21 | on(...args: Parameters): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/config.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | cachePath: string; 3 | disableReporting: boolean; 4 | metricDebounceTimeMax: number; 5 | metricDebounceTimeMin: number; 6 | sandboxTimeout: number; 7 | sdkAuthToken?: string; 8 | superfaceApiUrl: string; 9 | superfaceCacheTimeout: number; 10 | superfacePath: string; 11 | debug: boolean; 12 | cache: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/crypto.ts: -------------------------------------------------------------------------------- 1 | export interface ICrypto { 2 | hashString(input: string, algorithm: 'MD5' | 'sha256'): string; 3 | randomInt(max: number): number; 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/environment.ts: -------------------------------------------------------------------------------- 1 | export interface IEnvironment { 2 | getString(key: string): string | undefined; 3 | getNumber(key: string): number | undefined; 4 | getBoolean(key: string): boolean | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/errors/filesystem.errors.ts: -------------------------------------------------------------------------------- 1 | export interface IFileExistsError extends Error { 2 | name: 'FileExistsError'; 3 | } 4 | 5 | export interface IPermissionDeniedError extends Error { 6 | name: 'PermissionDeniedError'; 7 | } 8 | 9 | export interface INotEmptyError extends Error { 10 | name: 'NotEmptyError'; 11 | } 12 | 13 | export interface INotFoundError extends Error { 14 | name: 'NotFoundError'; 15 | } 16 | 17 | export interface IUnknownFileSystemError extends Error { 18 | name: 'UnknownFileSystemError'; 19 | } 20 | 21 | export type IFileSystemError = 22 | | IFileExistsError 23 | | IPermissionDeniedError 24 | | INotEmptyError 25 | | INotFoundError 26 | | IUnknownFileSystemError; 27 | -------------------------------------------------------------------------------- /src/interfaces/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filesystem.errors'; 2 | export * from './map-interpreter.errors'; 3 | export * from './profile-parameter-validator.errors'; 4 | export * from './usecase.errors'; 5 | -------------------------------------------------------------------------------- /src/interfaces/errors/map-interpreter.errors.ts: -------------------------------------------------------------------------------- 1 | import type { MapASTNode, MapDocumentNode } from '@superfaceai/ast'; 2 | 3 | import type { HttpMultiMap } from '../../core/interpreter/http'; 4 | 5 | export interface ErrorMetadata { 6 | node?: MapASTNode; 7 | ast?: MapDocumentNode; 8 | } 9 | 10 | export interface IMapASTError extends Error { 11 | name: 'MapASTError'; 12 | metadata?: ErrorMetadata; 13 | } 14 | 15 | export interface IMappedError extends Error { 16 | name: 'MappedError'; 17 | metadata?: ErrorMetadata; 18 | properties?: T; 19 | } 20 | 21 | export interface IJessieError extends Error { 22 | name: 'JessieError'; 23 | metadata?: ErrorMetadata; 24 | } 25 | 26 | export interface IHTTPError extends Error { 27 | name: 'HTTPError'; 28 | metadata?: ErrorMetadata; 29 | statusCode?: number; 30 | request?: { 31 | body?: unknown; 32 | headers?: HttpMultiMap; 33 | url?: string; 34 | }; 35 | response?: { 36 | body?: unknown; 37 | headers?: HttpMultiMap; 38 | }; 39 | } 40 | 41 | export interface IMappedHTTPError extends Error { 42 | name: 'MappedHTTPError'; 43 | metadata?: ErrorMetadata; 44 | statusCode?: number; 45 | properties?: T; 46 | } 47 | 48 | export type MapInterpreterError = 49 | | IMapASTError 50 | | IMappedHTTPError 51 | | IMappedError 52 | | IHTTPError 53 | | IJessieError; 54 | 55 | export const isMapInterpreterError = (e: unknown): e is MapInterpreterError => { 56 | return ( 57 | typeof e === 'object' && 58 | e !== null && 59 | 'name' in e && 60 | [ 61 | 'MapASTError', 62 | 'MappedHTTPError', 63 | 'MappedError', 64 | 'HTTPError', 65 | 'JessieError', 66 | ].includes((e as { name: string }).name) 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/interfaces/errors/profile-parameter-validator.errors.ts: -------------------------------------------------------------------------------- 1 | export interface IInputValidationError extends Error { 2 | name: 'InputValidationError'; 3 | } 4 | 5 | export interface IResultValidationError extends Error { 6 | name: 'ResultValidationError'; 7 | } 8 | 9 | export type ProfileParameterError = 10 | | IInputValidationError 11 | | IResultValidationError; 12 | -------------------------------------------------------------------------------- /src/interfaces/errors/usecase.errors.ts: -------------------------------------------------------------------------------- 1 | import type { SDKExecutionError } from '../../lib'; 2 | import type { MapInterpreterError } from './map-interpreter.errors'; 3 | import type { ProfileParameterError } from './profile-parameter-validator.errors'; 4 | 5 | // TODO 6 | export type PerformError = 7 | | ProfileParameterError 8 | | MapInterpreterError 9 | | SDKExecutionError; 10 | -------------------------------------------------------------------------------- /src/interfaces/events.ts: -------------------------------------------------------------------------------- 1 | export type EventFilter = { usecase?: string; profile?: string }; 2 | 3 | export interface IEvents { 4 | on( 5 | event: E, 6 | options: { 7 | priority: number; 8 | filter?: EventFilter; 9 | }, 10 | callback: Params[E] 11 | ): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/filesystem.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from '../lib'; 2 | import type { IFileSystemError } from './errors/filesystem.errors'; 3 | 4 | export interface IFileSystem { 5 | /** 6 | * Collection of utilities for working with paths in a OS-specific way 7 | */ 8 | path: { 9 | /** 10 | * Returns the path to the current working directory 11 | */ 12 | cwd(): string; 13 | 14 | /** 15 | * Returns the directory name of the given path. 16 | */ 17 | dirname: (path: string) => string; 18 | 19 | /** 20 | * Joins path with platform specific separator. 21 | */ 22 | join: (...path: string[]) => string; 23 | 24 | /** 25 | * Normalizes the given path. 26 | */ 27 | normalize: (path: string) => string; 28 | 29 | /** 30 | * Resolves path from left to the rightmost argument as an absolute path. 31 | */ 32 | resolve: (...pathSegments: string[]) => string; 33 | 34 | /** 35 | * Return the relative path from directory `from` to directory `to`. 36 | */ 37 | relative: (from: string, to: string) => string; 38 | }; 39 | 40 | /** 41 | * Synchronous variants of filesystem functions 42 | */ 43 | sync: { 44 | /** 45 | * Returns `true` if directory or file exists. 46 | */ 47 | exists: (path: string) => boolean; 48 | /** 49 | * Returns `true` if directory or file 50 | * exists, is readable and writable for the current user. 51 | */ 52 | isAccessible: (path: string) => boolean; 53 | 54 | /** 55 | * Returns `true` if `path` is a directory. 56 | * Returns `false` if `path` is not a directory or doesn't exist. 57 | */ 58 | isDirectory: (path: string) => boolean; 59 | 60 | /** 61 | * Returns `true` if `path` is a file. 62 | * Returns `false` if `path` is not a file or doesn't exist. 63 | */ 64 | isFile: (path: string) => boolean; 65 | 66 | /** 67 | * Creates a directory if it does not exist. 68 | */ 69 | mkdir: ( 70 | path: string, 71 | options?: { recursive?: boolean } 72 | ) => Result; 73 | 74 | /** 75 | * Reads file content as string. 76 | */ 77 | readFile: (path: string) => Result; 78 | 79 | /** 80 | * Returns list of files at `path`. 81 | */ 82 | readdir: (path: string) => Result; 83 | 84 | /** 85 | * Removes file or director if it exists. 86 | * Fails silently if the directory does not exist or is not possible to remove it 87 | */ 88 | rm: ( 89 | path: string, 90 | options?: { recursive?: boolean } 91 | ) => Result; 92 | 93 | /** 94 | * Writes string to file. 95 | */ 96 | writeFile: (path: string, data: string) => Result; 97 | }; 98 | 99 | /** 100 | * Returns `true` if directory or file exists. 101 | */ 102 | exists: (path: string) => Promise; 103 | 104 | /** 105 | * Returns `true` if directory or file 106 | * exists, is readable and writable for the current user. 107 | */ 108 | isAccessible: (path: string) => Promise; 109 | 110 | /** 111 | * Returns `true` if `path` is a directory. 112 | * Returns `false` if `path` is not a directory or doesn't exist. 113 | */ 114 | isDirectory: (path: string) => Promise; 115 | 116 | /** 117 | * Returns `true` if `path` is a file. 118 | * Returns `false` if `path` is not a file or doesn't exist. 119 | */ 120 | isFile: (path: string) => Promise; 121 | 122 | /** 123 | * Creates a directory if it does not exist. 124 | */ 125 | mkdir: ( 126 | path: string, 127 | options?: { recursive?: boolean } 128 | ) => Promise>; 129 | 130 | /** 131 | * Reads file content as string. 132 | */ 133 | readFile: (path: string) => Promise>; 134 | 135 | /** 136 | * Returns list of files at `path`. 137 | */ 138 | readdir: (path: string) => Promise>; 139 | 140 | /** 141 | * Removes file or directory if it exists. 142 | * Fails silently if the directory does not exist or is not possible to remove it 143 | */ 144 | rm: ( 145 | path: string, 146 | options?: { recursive?: boolean } 147 | ) => Promise>; 148 | 149 | /** 150 | * Writes string to file. 151 | */ 152 | writeFile: ( 153 | path: string, 154 | data: string 155 | ) => Promise>; 156 | } 157 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binary'; 2 | export * from './client'; 3 | export * from './config'; 4 | export * from './crypto'; 5 | export * from './errors'; 6 | export * from './environment'; 7 | export * from './events'; 8 | export * from './filesystem'; 9 | export * from './logger'; 10 | export * from './profile'; 11 | export * from './provider'; 12 | export * from './timers'; 13 | export * from './usecase'; 14 | -------------------------------------------------------------------------------- /src/interfaces/logger.ts: -------------------------------------------------------------------------------- 1 | export type LogFunction = { 2 | (format: string, ...args: unknown[]): void; 3 | enabled: boolean; 4 | }; 5 | 6 | export interface ILogger { 7 | log(name: string): LogFunction; 8 | log(name: string, format: string, ...args: unknown[]): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/profile.ts: -------------------------------------------------------------------------------- 1 | import type { IUseCase } from './usecase'; 2 | 3 | export interface IProfile { 4 | getConfiguredProviders(): string[]; 5 | getUseCase(name: string): IUseCase; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/provider.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityValues } from '@superfaceai/ast'; 2 | 3 | export interface IProviderConfiguration { 4 | name: string; 5 | /** @deprecated only for use in testing library */ 6 | security: SecurityValues[]; 7 | cacheKey: string; 8 | } 9 | 10 | export interface IProvider { 11 | configuration: IProviderConfiguration; 12 | configure(configuration?: { 13 | security?: SecurityValues[]; 14 | }): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/sandbox.ts: -------------------------------------------------------------------------------- 1 | import type { Stdlib } from '../core/interpreter/stdlib'; 2 | import type { NonPrimitive } from '../lib'; 3 | import type { IConfig } from './config'; 4 | import type { ILogger } from './logger'; 5 | 6 | export interface ISandbox { 7 | evalScript( 8 | config: IConfig, 9 | js: string, 10 | stdlib?: Stdlib, 11 | logger?: ILogger, 12 | variableDefinitions?: NonPrimitive 13 | ): unknown; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/timers.ts: -------------------------------------------------------------------------------- 1 | export interface ITimeout { 2 | value: unknown; 3 | } 4 | 5 | export interface ITimers { 6 | setTimeout( 7 | callback: (...args: unknown[]) => unknown, 8 | timeout: number 9 | ): ITimeout; 10 | clearTimeout(timeout: ITimeout): void; 11 | sleep(ms: number): Promise; 12 | now(): number; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/usecase.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityValues } from '@superfaceai/ast'; 2 | 3 | import type { NonPrimitive, Result, UnexpectedError, Variables } from '../lib'; 4 | import type { PerformError } from './errors'; 5 | import type { IProvider } from './provider'; 6 | 7 | export type PerformOptions = { 8 | provider?: IProvider | string; 9 | parameters?: Record; 10 | security?: SecurityValues[] | { [id: string]: Omit }; 11 | mapVariant?: string; 12 | mapRevision?: string; 13 | }; 14 | 15 | export interface IUseCase { 16 | perform< 17 | TInput extends NonPrimitive | undefined = Record< 18 | string, 19 | Variables | undefined 20 | >, 21 | TOutput = unknown 22 | >( 23 | input?: TInput, 24 | options?: PerformOptions 25 | ): Promise>; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/cache/cache.ts: -------------------------------------------------------------------------------- 1 | export class SuperCache { 2 | private cache: Record = {}; 3 | 4 | public getCached(cacheKey: string, initializer: () => T): T; 5 | public getCached(cacheKey: string, initializer: () => Promise): Promise; 6 | public getCached( 7 | cacheKey: string, 8 | initializer: () => T | Promise 9 | ): T | Promise { 10 | const cached = this.cache[cacheKey]; 11 | if (cached !== undefined) { 12 | return cached; 13 | } 14 | 15 | const initialized = initializer(); 16 | if (initialized instanceof Promise) { 17 | return initialized.then(value => { 18 | this.cache[cacheKey] = value; 19 | 20 | return value; 21 | }); 22 | } else { 23 | this.cache[cacheKey] = initialized; 24 | 25 | return initialized; 26 | } 27 | } 28 | 29 | public invalidate(cacheKey: string): void { 30 | if (this.cache[cacheKey] !== undefined) { 31 | delete this.cache[cacheKey]; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache'; 2 | -------------------------------------------------------------------------------- /src/lib/config-hash/config-hash.ts: -------------------------------------------------------------------------------- 1 | import type { ICrypto } from '../../interfaces'; 2 | 3 | export function configHash(values: unknown[], crypto: ICrypto): string { 4 | const data = values 5 | .map(value => { 6 | if (typeof value === 'string') { 7 | return value; 8 | } else { 9 | return JSON.stringify(value); 10 | } 11 | }) 12 | .join(';'); 13 | 14 | return crypto.hashString(data, 'MD5'); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/config-hash/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config-hash'; 2 | -------------------------------------------------------------------------------- /src/lib/env/env.test.ts: -------------------------------------------------------------------------------- 1 | import { MockEnvironment } from '../../mock'; 2 | import { resolveEnv, resolveEnvRecord } from './env'; 3 | 4 | const mockEnvVariable = 'superJsonTest'; 5 | const environment = new MockEnvironment(); 6 | 7 | describe('lib/env', () => { 8 | beforeEach(() => { 9 | environment.clear(); 10 | }); 11 | 12 | it('resolves env correctly when it is found', () => { 13 | environment.addValue(mockEnvVariable, 'test'); 14 | expect(resolveEnv(`$${mockEnvVariable}`, environment)).toEqual('test'); 15 | }); 16 | 17 | it('resolves env correctly when it is not found', () => { 18 | expect(resolveEnv(`$${mockEnvVariable}`, environment)).toEqual( 19 | `$${mockEnvVariable}` 20 | ); 21 | }); 22 | }); 23 | 24 | describe('when resolving env record', () => { 25 | beforeEach(() => { 26 | environment.clear(); 27 | }); 28 | 29 | it('resolves env correctly when value is string', () => { 30 | environment.addValue(mockEnvVariable, 'test'); 31 | const mockRecord = { testKey: `$${mockEnvVariable}` }; 32 | 33 | expect(resolveEnvRecord(mockRecord, environment)).toEqual({ 34 | testKey: 'test', 35 | }); 36 | }); 37 | 38 | it('resolves env correctly when value is object', () => { 39 | environment.addValue(mockEnvVariable, 'test'); 40 | const mockRecord = { 41 | testWrapperKey: { testKey: `$${mockEnvVariable}` }, 42 | nullKey: null, 43 | }; 44 | 45 | expect(resolveEnvRecord(mockRecord, environment)).toEqual({ 46 | testWrapperKey: { testKey: 'test' }, 47 | nullKey: null, 48 | }); 49 | }); 50 | 51 | it('resolves env correctly when value is undefined', () => { 52 | environment.addValue(mockEnvVariable, 'test'); 53 | const mockRecord = { 54 | testKey: `$${mockEnvVariable}`, 55 | undefinedKey: undefined, 56 | }; 57 | 58 | expect(resolveEnvRecord(mockRecord, environment)).toEqual({ 59 | testKey: 'test', 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/lib/env/env.ts: -------------------------------------------------------------------------------- 1 | import type { IEnvironment, ILogger } from '../../interfaces'; 2 | import { clone } from '../object'; 3 | 4 | const DEBUG_NAMESPACE = 'lib/env'; 5 | 6 | /** 7 | * Attempts to resolve environment value. 8 | * 9 | * If the value starts with `$` character, it attempts to look it up in the environment variables. 10 | * If the value is not in environment or doesn't start with `$` it is returned as is. 11 | */ 12 | export function resolveEnv( 13 | str: string, 14 | environment: IEnvironment, 15 | logger?: ILogger 16 | ): string { 17 | let value = str; 18 | 19 | if (str.startsWith('$')) { 20 | const variable = str.slice(1); 21 | const env = environment.getString(variable); 22 | if (env !== undefined) { 23 | value = env; 24 | } else { 25 | logger?.log(DEBUG_NAMESPACE, `Enviroment variable ${variable} not found`); 26 | } 27 | } 28 | 29 | return value; 30 | } 31 | 32 | /** 33 | * Resolve environment values in a record recursively. 34 | * 35 | * Returns a clone of the of the original record with every string field replaced by the result of `resolveEnd(field)`. 36 | */ 37 | export function resolveEnvRecord>( 38 | record: T, 39 | environment: IEnvironment, 40 | logger?: ILogger 41 | ): T { 42 | // If typed as `Partial` typescript complains with "Type 'string' cannot be used to index type 'Partial'. ts(2536)" 43 | const result: Partial> = {}; 44 | 45 | for (const [key, value] of Object.entries(record)) { 46 | if (typeof value === 'string') { 47 | // replace strings 48 | result[key] = resolveEnv(value, environment, logger); 49 | } else if (typeof value === 'object' && value !== null) { 50 | // recurse objects 51 | result[key] = resolveEnvRecord( 52 | value as Record, 53 | environment, 54 | logger 55 | ); 56 | } else { 57 | if (value !== undefined) { 58 | // clone everything else 59 | result[key] = clone(value); 60 | } 61 | } 62 | } 63 | 64 | return result as T; 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | -------------------------------------------------------------------------------- /src/lib/error/error.ts: -------------------------------------------------------------------------------- 1 | export function ensureErrorSubclass(error: unknown): Error { 2 | if (typeof error === 'string') { 3 | return new Error(error); 4 | } else if (error instanceof Error) { 5 | return error; 6 | } 7 | 8 | return new Error(JSON.stringify(error)); 9 | } 10 | 11 | export class ErrorBase { 12 | constructor(public kind: string, public message: string) {} 13 | 14 | public get [Symbol.toStringTag](): string { 15 | return this.kind; 16 | } 17 | 18 | public toString(): string { 19 | return `${this.kind}: ${this.message}`; 20 | } 21 | } 22 | 23 | export class UnexpectedError extends ErrorBase { 24 | constructor( 25 | public override message: string, 26 | public additionalContext?: unknown 27 | ) { 28 | super('UnexpectedError', message); 29 | } 30 | } 31 | 32 | /** 33 | * This is a base class for errors that the SDK may throw during normal execution. 34 | * 35 | * These errors should be as descriptive as possible to explain the problem to the user. 36 | */ 37 | export class SDKExecutionError extends Error { 38 | constructor( 39 | private shortMessage: string, 40 | private longLines: string[], 41 | private hints: string[] 42 | ) { 43 | super(shortMessage); 44 | 45 | // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work 46 | Object.setPrototypeOf(this, SDKBindError.prototype); 47 | 48 | this.message = this.formatLong(); 49 | this.name = 'SDKExecutionError'; 50 | } 51 | 52 | /** 53 | * Formats this error into a one-line string 54 | */ 55 | public formatShort(): string { 56 | return this.shortMessage; 57 | } 58 | 59 | /** 60 | * Formats this error into a possible multi-line string with more context, details and hints 61 | */ 62 | public formatLong(): string { 63 | let result = this.shortMessage; 64 | 65 | if (this.longLines.length > 0) { 66 | result += '\n'; 67 | for (const line of this.longLines) { 68 | result += '\n' + line; 69 | } 70 | } 71 | 72 | if (this.hints.length > 0) { 73 | result += '\n'; 74 | for (const hint of this.hints) { 75 | result += '\nHint: ' + hint; 76 | } 77 | } 78 | 79 | return result + '\n'; 80 | } 81 | 82 | public get [Symbol.toStringTag](): string { 83 | return this.name; 84 | } 85 | 86 | public override toString(): string { 87 | return this.formatLong(); 88 | } 89 | } 90 | 91 | export class SDKBindError extends SDKExecutionError { 92 | constructor(shortMessage: string, longLines: string[], hints: string[]) { 93 | super(shortMessage, longLines, hints); 94 | 95 | // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work 96 | Object.setPrototypeOf(this, SDKBindError.prototype); 97 | 98 | this.name = 'SDKBindError'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache'; 2 | export * from './config-hash'; 3 | export * from './env'; 4 | export * from './error'; 5 | export * from './object'; 6 | export * from './pipe'; 7 | export * from './result/result'; 8 | export * from './types'; 9 | export * from './utils'; 10 | export * from './variables'; 11 | -------------------------------------------------------------------------------- /src/lib/object/index.ts: -------------------------------------------------------------------------------- 1 | export * from './object'; 2 | -------------------------------------------------------------------------------- /src/lib/object/object.test.ts: -------------------------------------------------------------------------------- 1 | import { clone, recursiveKeyList } from './object'; 2 | 3 | describe('recursiveKeyList', () => { 4 | it('should return all objects keys from a flat object', () => { 5 | const object = { 6 | a: 1, 7 | 2: true, 8 | nope: undefined, 9 | }; 10 | 11 | expect(recursiveKeyList(object).sort()).toMatchObject(['2', 'a', 'nope']); 12 | }); 13 | 14 | it('should return all objects keys from a nested object', () => { 15 | const object = { 16 | a: 1, 17 | b: { 18 | c: { 19 | d: { 20 | e: 2, 21 | }, 22 | f: null, 23 | g: undefined, 24 | }, 25 | }, 26 | }; 27 | 28 | expect(recursiveKeyList(object, v => v !== undefined).sort()).toMatchObject( 29 | ['a', 'b', 'b.c', 'b.c.d', 'b.c.d.e', 'b.c.f'] 30 | ); 31 | }); 32 | }); 33 | 34 | describe('clone', () => { 35 | it('should clone any object', () => { 36 | const object = { 37 | a: 1, 38 | b: { 39 | c: { 40 | d: { 41 | e: 2, 42 | }, 43 | f: null, 44 | g: undefined, 45 | }, 46 | }, 47 | }; 48 | const cloned = clone(object); 49 | expect(cloned).toStrictEqual(cloned); 50 | }); 51 | 52 | it('should clone undefined', () => { 53 | const object = undefined; 54 | const cloned = clone(object); 55 | expect(cloned).toStrictEqual(undefined); 56 | }); 57 | 58 | it('should clone null', () => { 59 | const object = null; 60 | const cloned = clone(object); 61 | expect(cloned).toStrictEqual(null); 62 | }); 63 | 64 | it('should clone empty object', () => { 65 | const object = {}; 66 | const cloned = clone(object); 67 | expect(cloned).toStrictEqual({}); 68 | }); 69 | 70 | describe('when cloning buffer', () => { 71 | let object: { buffer: Buffer }; 72 | 73 | beforeEach(() => { 74 | object = { 75 | buffer: Buffer.from('data'), 76 | }; 77 | }); 78 | 79 | it('should clone buffer', () => { 80 | const cloned = clone(object); 81 | expect(Buffer.isBuffer(cloned.buffer)).toBe(true); 82 | expect(cloned.buffer.toString()).toEqual(object.buffer.toString()); 83 | }); 84 | 85 | it('should create new instance of buffer', () => { 86 | const cloned = clone(object); 87 | expect(cloned.buffer).not.toBe(object.buffer); 88 | }); 89 | }); 90 | 91 | describe('when cloning array', () => { 92 | let object: { array: Array }; 93 | 94 | beforeEach(() => { 95 | object = { 96 | array: [1, { a: 1, b: 2, c: 'string' }], 97 | }; 98 | }); 99 | 100 | it('should clone array', () => { 101 | const cloned = clone(object); 102 | expect(Array.isArray(cloned.array)).toBe(true); 103 | expect(cloned).toStrictEqual(object); 104 | }); 105 | 106 | it('should create new instance of array', () => { 107 | const cloned = clone(object); 108 | expect(cloned.array).not.toBe(object.array); 109 | }); 110 | }); 111 | 112 | describe('when cloning date', () => { 113 | let object: { date: Date }; 114 | 115 | beforeEach(() => { 116 | object = { 117 | date: new Date(), 118 | }; 119 | }); 120 | 121 | it('should clone date', () => { 122 | const cloned = clone(object); 123 | expect(cloned.date).toBeInstanceOf(Date); 124 | expect(cloned.date).toStrictEqual(object.date); 125 | }); 126 | 127 | it('should create new instance of date', () => { 128 | const cloned = clone(object); 129 | expect(cloned.date).not.toBe(object.date); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/lib/object/object.ts: -------------------------------------------------------------------------------- 1 | import { UnexpectedError } from '../error'; 2 | import type { None } from '../variables'; 3 | import { isClassInstance, isNone } from '../variables'; 4 | 5 | /** 6 | * Creates a deep clone of the value. 7 | */ 8 | export function clone(value: T): T { 9 | if (value === null) { 10 | return value; 11 | } 12 | 13 | if (value instanceof Date) { 14 | return new Date(value.getTime()) as unknown as T; 15 | } 16 | 17 | if (Array.isArray(value)) { 18 | const arrayCopy = [] as unknown[]; 19 | for (const item of value) { 20 | arrayCopy.push(clone(item)); 21 | } 22 | 23 | return arrayCopy as unknown as T; 24 | } 25 | 26 | if (Buffer.isBuffer(value)) { 27 | return Buffer.from(value) as unknown as T; 28 | } 29 | 30 | if (isClassInstance(value)) { 31 | return value; 32 | } 33 | 34 | if (typeof value === 'object') { 35 | const objectCopy = Object.entries(value).map(([key, value]) => [ 36 | key, 37 | clone(value) as unknown, 38 | ]); 39 | 40 | return Object.fromEntries(objectCopy) as unknown as T; 41 | } 42 | 43 | return value; 44 | } 45 | 46 | export function isRecord(input: unknown): input is Record { 47 | if (typeof input !== 'object' || input === null) { 48 | return false; 49 | } 50 | 51 | return true; 52 | } 53 | 54 | export function fromEntriesOptional>( 55 | ...entries: [key: string, value: T | None][] 56 | ): Record { 57 | const base: Record = {}; 58 | 59 | for (const [key, value] of entries) { 60 | if (!isNone(value)) { 61 | base[key] = value; 62 | } 63 | } 64 | 65 | return base; 66 | } 67 | 68 | /** 69 | * Recursively descends the record and returns a list of enumerable keys 70 | */ 71 | export function recursiveKeyList( 72 | record: Record, 73 | filter?: (value: unknown) => boolean, 74 | base?: string 75 | ): string[] { 76 | const keys: string[] = []; 77 | 78 | for (const [key, value] of Object.entries(record)) { 79 | if (filter !== undefined && !filter(value)) { 80 | continue; 81 | } 82 | 83 | let basedKey = key; 84 | if (base !== undefined) { 85 | basedKey = base + '.' + key; 86 | } 87 | keys.push(basedKey); 88 | 89 | if (typeof value === 'object' && value !== null) { 90 | keys.push( 91 | ...recursiveKeyList(value as Record, filter, basedKey) 92 | ); 93 | } 94 | } 95 | 96 | return keys; 97 | } 98 | 99 | /** 100 | * Recursively index into a record. 101 | * 102 | * Throws if a child cannot be indexed into. 103 | */ 104 | export function indexRecord>( 105 | input: Record, 106 | key: string[] 107 | ): T | undefined { 108 | // check for input being undefined is for sanity only 109 | if (key.length === 0 || input === null || input === undefined) { 110 | return undefined; 111 | } 112 | 113 | if (key.length === 1) { 114 | return input[key[0]]; 115 | } 116 | 117 | const currentKey = key.shift(); 118 | if (currentKey === undefined) { 119 | throw new UnexpectedError('unreachable'); 120 | } 121 | 122 | const next = input[currentKey]; 123 | if (!isRecord(next)) { 124 | throw new UnexpectedError('Cannot index into non-object'); 125 | } 126 | 127 | return indexRecord(next as Record, key); 128 | } 129 | 130 | // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze 131 | export type RecursiveReadonly = { 132 | readonly [P in keyof T]: RecursiveReadonly; 133 | }; 134 | export function deepFreeze(o: T): RecursiveReadonly { 135 | for (const name of Object.getOwnPropertyNames(o)) { 136 | const value = (o as Record)[name]; 137 | if (value !== undefined && typeof value === 'object') { 138 | deepFreeze(value); 139 | } 140 | } 141 | 142 | return Object.freeze(o); 143 | } 144 | -------------------------------------------------------------------------------- /src/lib/pipe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pipe'; 2 | -------------------------------------------------------------------------------- /src/lib/pipe/pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from './pipe'; 2 | 3 | describe('pipe', () => { 4 | it('should pipe the value through filters correctly', async () => { 5 | const result = await pipe( 6 | 2, 7 | input => input * 2, 8 | input => input + 1 9 | ); 10 | expect(result).toBe(5); 11 | }); 12 | 13 | it('should clone initial object', async () => { 14 | const original = { value: 7 }; 15 | const result = await pipe(original, input => { 16 | input.value = 5; 17 | 18 | return input; 19 | }); 20 | 21 | expect(original.value).toBe(7); 22 | expect(result.value).toBe(5); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/lib/pipe/pipe.ts: -------------------------------------------------------------------------------- 1 | import { clone } from '../object'; 2 | import type { MaybePromise } from '../types'; 3 | 4 | export async function pipe( 5 | initial: T, 6 | ...filters: ((input: T) => MaybePromise)[] 7 | ): Promise { 8 | let accumulator = clone(initial); 9 | 10 | for (const filter of filters) { 11 | accumulator = await filter(accumulator); 12 | } 13 | 14 | return accumulator; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/result/result.test.ts: -------------------------------------------------------------------------------- 1 | import { Err, err, Ok, ok } from './result'; 2 | 3 | describe('Result wrappers', () => { 4 | describe('when using Ok', () => { 5 | type MockValueType = { test: string }; 6 | const mockValue: MockValueType = { test: 'test' }; 7 | const mockOk = new Ok(mockValue); 8 | 9 | it('checks is ok correctly', () => { 10 | expect(mockOk.isOk()).toEqual(true); 11 | }); 12 | 13 | it('checks is err correctly', () => { 14 | expect(mockOk.isErr()).toEqual(false); 15 | }); 16 | 17 | it('maps value correctly', () => { 18 | expect(mockOk.map((value: MockValueType) => value.test)).toEqual({ 19 | value: 'test', 20 | }); 21 | }); 22 | 23 | it('maps err correctly', () => { 24 | expect(mockOk.mapErr((e: unknown) => e)).toEqual({ 25 | value: { test: 'test' }, 26 | }); 27 | }); 28 | 29 | it('matches value correctly', () => { 30 | expect( 31 | mockOk.match( 32 | (t: MockValueType) => t.test, 33 | (e: unknown) => e 34 | ) 35 | ).toEqual('test'); 36 | }); 37 | 38 | it('uses andThen correctly', () => { 39 | expect(mockOk.andThen((t: MockValueType) => ok(t.test))).toEqual({ 40 | value: 'test', 41 | }); 42 | }); 43 | 44 | it('unwraps correctly', () => { 45 | expect(mockOk.unwrap()).toEqual({ test: 'test' }); 46 | }); 47 | 48 | it('maps async correctly', async () => { 49 | await expect( 50 | mockOk.mapAsync((value: MockValueType) => Promise.resolve(value.test)) 51 | ).resolves.toEqual({ value: 'test' }); 52 | }); 53 | 54 | it('maps err async correctly', async () => { 55 | await expect( 56 | mockOk.mapErrAsync(() => Promise.resolve('test')) 57 | ).resolves.toEqual(ok(mockValue)); 58 | }); 59 | 60 | it('maps then async correctly', async () => { 61 | await expect( 62 | mockOk.andThenAsync((t: MockValueType) => Promise.resolve(ok(t))) 63 | ).resolves.toEqual(ok(mockValue)); 64 | }); 65 | }); 66 | 67 | describe('when using Err', () => { 68 | type MockValueType = { name: string; message: string }; 69 | const mockError: MockValueType = { name: 'test', message: 'test' }; 70 | const mockErr = new Err(mockError); 71 | 72 | it('checks is ok correctly', () => { 73 | expect(mockErr.isOk()).toEqual(false); 74 | }); 75 | 76 | it('checks is err correctly', () => { 77 | expect(mockErr.isErr()).toEqual(true); 78 | }); 79 | 80 | it('maps value correctly', () => { 81 | expect(mockErr.map((t: unknown) => t)).toEqual( 82 | err({ name: 'test', message: 'test' }) 83 | ); 84 | }); 85 | 86 | it('maps err correctly', () => { 87 | expect(mockErr.mapErr((e: unknown) => e)).toEqual({ 88 | error: { name: 'test', message: 'test' }, 89 | }); 90 | }); 91 | 92 | it('matches value correctly', () => { 93 | expect( 94 | mockErr.match( 95 | (t: unknown) => t, 96 | (e: unknown) => e 97 | ) 98 | ).toEqual({ name: 'test', message: 'test' }); 99 | }); 100 | 101 | it('uses andThen correctly', () => { 102 | expect( 103 | mockErr.andThen(() => err({ name: 'test', message: 'inner' })) 104 | ).toEqual({ error: { name: 'test', message: 'test' } }); 105 | }); 106 | 107 | it('unwraps correctly', () => { 108 | expect(() => mockErr.unwrap()).toThrow(mockError); 109 | }); 110 | 111 | it('maps async correctly', async () => { 112 | await expect( 113 | mockErr.mapAsync(() => Promise.reject('this should not map')) 114 | ).resolves.toEqual(err(mockError)); 115 | }); 116 | 117 | it('maps err async correctly', async () => { 118 | await expect( 119 | mockErr.mapErrAsync((t: MockValueType) => Promise.resolve(t)) 120 | ).resolves.toEqual(err(mockError)); 121 | }); 122 | 123 | it('maps then async correctly', async () => { 124 | const mockFn = jest.fn(); 125 | await expect(mockErr.andThenAsync(mockFn)).resolves.toEqual( 126 | err(mockError) 127 | ); 128 | 129 | expect(mockFn).not.toHaveBeenCalled(); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/lib/result/result.ts: -------------------------------------------------------------------------------- 1 | // This interface exists as a guideline of what to implement on both result variants and as a place where documentation can be attached. 2 | interface IResult { 3 | /** Returns `true` if this result represents an `Ok variant. */ 4 | isOk(): this is Ok; 5 | 6 | /** Returns `true` if this result represents an `Err` variant. */ 7 | isErr(): this is Err; 8 | 9 | /** Maps `Ok` variant and propagates `Err` variant. */ 10 | map(f: (t: T) => U): Result; 11 | 12 | /** Maps `Err` variant and propagates `Ok` variant. */ 13 | mapErr(f: (e: E) => U): Result; 14 | 15 | /** Fallibly maps `Ok` variant and propagates `Err` variant. */ 16 | andThen(f: (t: T) => Result): Result; 17 | 18 | /** Calls `ok` if `this` is `Ok` variant and `err` if `this` is `Err` variant. */ 19 | match(ok: (t: T) => U, err: (e: E) => U): U; 20 | 21 | /** Unwraps `Ok` variant and throws on `Err` variant. */ 22 | unwrap(): T; 23 | } 24 | 25 | interface IAsyncResult { 26 | /** Maps `Ok` variant asynchronously and propagates `Err` variant. */ 27 | mapAsync(f: (t: T) => Promise): Promise>; 28 | 29 | /** Maps `Err` variant asynchronously and propagates `Ok` variant. */ 30 | mapErrAsync(f: (t: E) => Promise): Promise>; 31 | 32 | /** Fallibly maps `Ok` variant asynchronously and propagates `Err` variant. */ 33 | andThenAsync(f: (t: T) => Promise>): Promise>; 34 | } 35 | 36 | export class Ok implements IResult, IAsyncResult { 37 | constructor(public readonly value: T) {} 38 | 39 | public isOk(): this is Ok { 40 | return true; 41 | } 42 | 43 | public isErr(): this is Err { 44 | return !this.isOk(); 45 | } 46 | 47 | public map(f: (t: T) => U): Result { 48 | return ok(f(this.value)); 49 | } 50 | 51 | public mapErr(_: (e: E) => U): Result { 52 | return ok(this.value); 53 | } 54 | 55 | public andThen(f: (t: T) => Result): Result { 56 | return f(this.value); 57 | } 58 | 59 | public match(ok: (t: T) => U, _: (e: E) => U): U { 60 | return ok(this.value); 61 | } 62 | 63 | public unwrap(): T { 64 | return this.value; 65 | } 66 | 67 | public async mapAsync(f: (t: T) => Promise): Promise> { 68 | const inner = await f(this.value); 69 | 70 | return ok(inner); 71 | } 72 | 73 | public async mapErrAsync(_: (t: E) => Promise): Promise> { 74 | return ok(this.value); 75 | } 76 | 77 | public async andThenAsync( 78 | f: (t: T) => Promise> 79 | ): Promise> { 80 | return f(this.value); 81 | } 82 | } 83 | 84 | export class Err implements IResult, IAsyncResult { 85 | constructor(public readonly error: E) {} 86 | 87 | public isOk(): this is Ok { 88 | return false; 89 | } 90 | 91 | public isErr(): this is Err { 92 | return !this.isOk(); 93 | } 94 | 95 | public map(_: (t: T) => U): Result { 96 | return err(this.error); 97 | } 98 | 99 | public mapErr(f: (e: E) => U): Result { 100 | return err(f(this.error)); 101 | } 102 | 103 | public andThen(_: (t: T) => Result): Result { 104 | return err(this.error); 105 | } 106 | 107 | public match(_: (t: T) => U, err: (e: E) => U): U { 108 | return err(this.error); 109 | } 110 | 111 | public unwrap(): T { 112 | throw this.error; 113 | } 114 | 115 | public async mapAsync(_: (t: T) => Promise): Promise> { 116 | return err(this.error); 117 | } 118 | 119 | public async mapErrAsync(f: (t: E) => Promise): Promise> { 120 | const inner = await f(this.error); 121 | 122 | return err(inner); 123 | } 124 | 125 | public async andThenAsync( 126 | _: (t: T) => Promise> 127 | ): Promise> { 128 | return err(this.error); 129 | } 130 | } 131 | 132 | export type Result = Ok | Err; 133 | export const ok = (value: T): Ok => new Ok(value); 134 | export const err = (err: E): Err => new Err(err); 135 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /src/lib/types/types.ts: -------------------------------------------------------------------------------- 1 | export type MaybePromise = T | Promise; 2 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ProfileDocumentNode } from '@superfaceai/ast'; 2 | 3 | export function profileAstId(ast: ProfileDocumentNode): string { 4 | return ast.header.scope !== undefined 5 | ? ast.header.scope + '/' + ast.header.name 6 | : ast.header.name; 7 | } 8 | 9 | export function versionToString(version: { 10 | major: number; 11 | minor: number; 12 | patch: number; 13 | label?: string; 14 | }): string { 15 | let versionString = `${version.major}.${version.minor}.${version.patch}`; 16 | 17 | if (version.label !== undefined) { 18 | versionString += `-${version.label}`; 19 | } 20 | 21 | return versionString; 22 | } 23 | 24 | export function forceCast(_: unknown): asserts _ is T {} 25 | 26 | export function isSettingsWithAst(input: T): input is T & { ast: unknown } { 27 | return typeof input === 'object' && input !== null && 'ast' in input; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/variables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './variables'; 2 | -------------------------------------------------------------------------------- /src/lib/variables/variables.ts: -------------------------------------------------------------------------------- 1 | import type { IBinaryData } from '../../interfaces'; 2 | import { isBinaryData } from '../../interfaces'; 3 | import { UnexpectedError } from '../error'; 4 | 5 | export type None = undefined | null; 6 | export type Primitive = 7 | | string 8 | | boolean 9 | | number 10 | | unknown[] // Arrays should be considered opaque value and therefore act as a primitive, same with 11 | | None 12 | | IBinaryData 13 | | Buffer; 14 | export type NonPrimitive = { 15 | [key: string]: Primitive | NonPrimitive; 16 | }; 17 | export type Variables = Primitive | NonPrimitive; 18 | 19 | // FIXME: This is temporary solution; find a better way to handle this 20 | export function isClassInstance(input: unknown): boolean { 21 | if (input === null || input === undefined) { 22 | return false; 23 | } 24 | 25 | if (typeof input !== 'object') { 26 | return false; 27 | } 28 | 29 | if (Array.isArray(input)) { 30 | return false; 31 | } 32 | 33 | const proto = Object.getPrototypeOf(input) as object; 34 | 35 | if (proto === null || proto === Object.prototype) { 36 | return false; 37 | } 38 | 39 | return typeof proto.constructor === 'function'; 40 | } 41 | 42 | export function isNone(input: unknown): input is None { 43 | return input === undefined || input === null; 44 | } 45 | 46 | export function isPrimitive(input: unknown): input is Primitive { 47 | return ( 48 | ['string', 'number', 'boolean'].includes(typeof input) || 49 | Array.isArray(input) || 50 | isNone(input) || 51 | isBinaryData(input) || 52 | Buffer.isBuffer(input) || 53 | isClassInstance(input) 54 | ); 55 | } 56 | 57 | export function isNonPrimitive(input: unknown): input is NonPrimitive { 58 | return ( 59 | typeof input === 'object' && 60 | input !== null && 61 | !Array.isArray(input) && 62 | !isBinaryData(input) && 63 | !Buffer.isBuffer(input) && 64 | !isClassInstance(input) 65 | ); 66 | } 67 | 68 | export function isVariables(input: unknown): input is Variables { 69 | return isPrimitive(input) || isNonPrimitive(input); 70 | } 71 | 72 | export function isEmptyRecord( 73 | input: Record 74 | ): input is Record { 75 | return isNonPrimitive(input) && Object.keys(input).length === 0; 76 | } 77 | 78 | export function assertIsVariables(input: unknown): asserts input is Variables { 79 | if (!isVariables(input)) { 80 | throw new UnexpectedError(`Invalid result type: ${typeof input}`); 81 | } 82 | } 83 | 84 | export function castToVariables(input: unknown): Variables { 85 | assertIsVariables(input); 86 | 87 | return input; 88 | } 89 | 90 | export function castToNonPrimitive(input: unknown): NonPrimitive { 91 | if (!isNonPrimitive(input)) { 92 | throw new UnexpectedError('Input is not NonPrimitive'); 93 | } 94 | 95 | return input; 96 | } 97 | 98 | /** 99 | * Recursively merges variables from `left` and then from `right` into a new object. 100 | */ 101 | export function mergeVariables( 102 | left: NonPrimitive, 103 | right: NonPrimitive 104 | ): NonPrimitive { 105 | const result: NonPrimitive = {}; 106 | 107 | for (const key of Object.keys(left)) { 108 | result[key] = left[key]; 109 | } 110 | for (const key of Object.keys(right)) { 111 | const l = left[key]; 112 | const r = right[key]; 113 | if ( 114 | r !== undefined && 115 | l !== undefined && 116 | isNonPrimitive(r) && 117 | isNonPrimitive(l) 118 | ) { 119 | result[key] = mergeVariables(l, r); 120 | } else { 121 | result[key] = right[key]; 122 | } 123 | } 124 | 125 | return result; 126 | } 127 | 128 | /** 129 | * Turns a variable (both primitive and non-primitive) into a string. 130 | */ 131 | export function variableToString(variable: Variables): string { 132 | if (typeof variable === 'string') { 133 | return variable; 134 | } 135 | 136 | if (variable === undefined) { 137 | return 'undefined'; 138 | } 139 | 140 | if (Buffer.isBuffer(variable)) { 141 | return variable.toString(); 142 | } 143 | 144 | return JSON.stringify(variable); 145 | } 146 | 147 | /** 148 | * Stringifies a Record of variables. `None` values are removed. 149 | */ 150 | export function variablesToStrings( 151 | variables: NonPrimitive 152 | ): Record { 153 | const result: Record = {}; 154 | 155 | for (const [key, value] of Object.entries(variables)) { 156 | if (!isNone(value)) { 157 | result[key] = variableToString(value); 158 | } 159 | } 160 | 161 | return result; 162 | } 163 | -------------------------------------------------------------------------------- /src/mock/environment.ts: -------------------------------------------------------------------------------- 1 | import type { IEnvironment } from '../core'; 2 | 3 | export class MockEnvironment implements IEnvironment { 4 | private values: Record = {}; 5 | 6 | public getString(key: string): string | undefined { 7 | return this.values[key]; 8 | } 9 | 10 | public getNumber(key: string): number | undefined { 11 | const value = this.values[key]; 12 | 13 | if (value === undefined) { 14 | return undefined; 15 | } 16 | 17 | return Number(value); 18 | } 19 | 20 | public getBoolean(key: string): boolean | undefined { 21 | const value = this.values[key]; 22 | 23 | if (value === undefined) { 24 | return undefined; 25 | } 26 | 27 | return value.toLowerCase() === 'true'; 28 | } 29 | 30 | public addValue(key: string, value: string | number | boolean): void { 31 | this.values[key] = value.toString(); 32 | } 33 | 34 | public clear(): void { 35 | this.values = {}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/mock/filesystem.ts: -------------------------------------------------------------------------------- 1 | import type { FileSystemError, IFileSystem } from '../core'; 2 | import type { Result } from '../lib/result/result'; 3 | import { ok } from '../lib/result/result'; 4 | import { NodeFileSystem } from '../node'; 5 | 6 | export type IPartialFileSystem = Partial> & { 7 | path?: Partial; 8 | sync?: Partial; 9 | }; 10 | export const MockFileSystem = ( 11 | fileSystem?: IPartialFileSystem 12 | ): IFileSystem => ({ 13 | sync: { 14 | exists: fileSystem?.sync?.exists ?? jest.fn(() => true), 15 | isAccessible: fileSystem?.sync?.isAccessible ?? jest.fn(() => true), 16 | isDirectory: fileSystem?.sync?.isDirectory ?? jest.fn(() => false), 17 | isFile: fileSystem?.sync?.isFile ?? jest.fn(() => true), 18 | mkdir: fileSystem?.sync?.mkdir ?? jest.fn(() => ok(undefined)), 19 | readFile: fileSystem?.sync?.readFile ?? jest.fn(() => ok('')), 20 | readdir: fileSystem?.sync?.readdir ?? jest.fn(() => ok([])), 21 | rm: fileSystem?.sync?.rm ?? jest.fn(() => ok(undefined)), 22 | writeFile: fileSystem?.sync?.writeFile ?? jest.fn(() => ok(undefined)), 23 | }, 24 | path: { 25 | cwd: fileSystem?.path?.cwd ?? jest.fn(() => '.'), 26 | dirname: fileSystem?.path?.dirname ?? jest.fn(() => ''), 27 | join: 28 | fileSystem?.path?.join ?? 29 | jest.fn((...strings: string[]) => strings.join('/')), 30 | normalize: fileSystem?.path?.normalize ?? jest.fn((path: string) => path), 31 | resolve: fileSystem?.path?.resolve ?? jest.fn(NodeFileSystem.path.resolve), 32 | relative: fileSystem?.path?.relative ?? jest.fn(() => ''), 33 | }, 34 | exists: fileSystem?.exists ?? jest.fn(async () => true), 35 | isAccessible: fileSystem?.isAccessible ?? jest.fn(async () => true), 36 | isDirectory: fileSystem?.isDirectory ?? jest.fn(async () => true), 37 | isFile: fileSystem?.isFile ?? jest.fn(async () => true), 38 | mkdir: 39 | fileSystem?.mkdir ?? 40 | jest.fn(async (): Promise> => ok(undefined)), 41 | readFile: 42 | fileSystem?.readFile ?? 43 | jest.fn(async (): Promise> => ok('')), 44 | readdir: 45 | fileSystem?.readdir ?? 46 | jest.fn(async (): Promise> => ok([])), 47 | rm: 48 | fileSystem?.rm ?? 49 | jest.fn(async (): Promise> => ok(undefined)), 50 | writeFile: 51 | fileSystem?.writeFile ?? 52 | jest.fn(async (): Promise> => ok(undefined)), 53 | }); 54 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './environment'; 3 | export * from './filesystem'; 4 | export * from './timers'; 5 | export * from './profile-document-node'; 6 | export * from './map-document-node'; 7 | export * from './provider-json'; 8 | -------------------------------------------------------------------------------- /src/mock/map-document-node.ts: -------------------------------------------------------------------------------- 1 | import type { MapDocumentNode } from '@superfaceai/ast'; 2 | 3 | export const mockMapDocumentNode = (options?: { 4 | name?: string; 5 | scope?: string; 6 | provider?: string; 7 | variant?: string; 8 | version?: { 9 | major: number; 10 | minor: number; 11 | patch: number; 12 | label?: string; 13 | }; 14 | usecaseName?: string; 15 | }): MapDocumentNode => { 16 | const ast: MapDocumentNode = { 17 | astMetadata: { 18 | sourceChecksum: 'checksum', 19 | astVersion: { 20 | major: 1, 21 | minor: 0, 22 | patch: 0, 23 | }, 24 | parserVersion: { 25 | major: 1, 26 | minor: 0, 27 | patch: 0, 28 | }, 29 | }, 30 | kind: 'MapDocument', 31 | header: { 32 | kind: 'MapHeader', 33 | profile: { 34 | name: options?.name ?? 'test', 35 | scope: options?.scope ?? undefined, 36 | version: { 37 | major: options?.version?.major ?? 1, 38 | minor: options?.version?.minor ?? 0, 39 | patch: options?.version?.patch ?? 0, 40 | label: options?.version?.label ?? undefined, 41 | }, 42 | }, 43 | provider: options?.provider ?? 'test', 44 | variant: options?.variant ?? undefined, 45 | }, 46 | definitions: [ 47 | { 48 | kind: 'MapDefinition', 49 | name: options?.usecaseName ?? 'Test', 50 | usecaseName: options?.usecaseName ?? 'Test', 51 | statements: [], 52 | }, 53 | ], 54 | }; 55 | // Remove undefined properties 56 | if (ast.header.profile.scope === undefined) { 57 | delete ast.header.profile.scope; 58 | } 59 | 60 | if (ast.header.variant === undefined) { 61 | delete ast.header.variant; 62 | } 63 | 64 | if (ast.header.profile.version.label === undefined) { 65 | delete ast.header.profile.version.label; 66 | } 67 | 68 | return ast; 69 | }; 70 | -------------------------------------------------------------------------------- /src/mock/profile-document-node.ts: -------------------------------------------------------------------------------- 1 | import type { ProfileDocumentNode } from '@superfaceai/ast'; 2 | 3 | export const mockProfileDocumentNode = (options?: { 4 | name?: string; 5 | scope?: string; 6 | version?: { 7 | major: number; 8 | minor: number; 9 | patch: number; 10 | label?: string; 11 | }; 12 | usecaseName?: string; 13 | }): ProfileDocumentNode => ({ 14 | kind: 'ProfileDocument', 15 | astMetadata: { 16 | sourceChecksum: 'checksum', 17 | astVersion: { 18 | major: 1, 19 | minor: 0, 20 | patch: 0, 21 | }, 22 | parserVersion: { 23 | major: 1, 24 | minor: 0, 25 | patch: 0, 26 | }, 27 | }, 28 | header: { 29 | kind: 'ProfileHeader', 30 | scope: options?.scope, 31 | name: options?.name ?? 'test', 32 | version: { 33 | major: options?.version?.major ?? 1, 34 | minor: options?.version?.minor ?? 0, 35 | patch: options?.version?.patch ?? 0, 36 | label: options?.version?.label, 37 | }, 38 | }, 39 | definitions: [ 40 | { 41 | kind: 'UseCaseDefinition', 42 | useCaseName: options?.usecaseName ?? 'Test', 43 | safety: 'safe', 44 | result: { 45 | kind: 'UseCaseSlotDefinition', 46 | value: { 47 | kind: 'ObjectDefinition', 48 | fields: [ 49 | { 50 | kind: 'FieldDefinition', 51 | fieldName: 'message', 52 | required: true, 53 | type: { 54 | kind: 'NonNullDefinition', 55 | type: { 56 | kind: 'PrimitiveTypeName', 57 | name: 'string', 58 | }, 59 | }, 60 | }, 61 | ], 62 | }, 63 | }, 64 | }, 65 | ], 66 | }); 67 | -------------------------------------------------------------------------------- /src/mock/provider-json.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IntegrationParameter, 3 | ProviderJson, 4 | ProviderService, 5 | SecurityScheme, 6 | } from '@superfaceai/ast'; 7 | import { ApiKeyPlacement, HttpScheme, SecurityType } from '@superfaceai/ast'; 8 | 9 | export const mockProviderJson = (options?: { 10 | name?: string; 11 | services?: ProviderService[]; 12 | security?: SecurityScheme[]; 13 | parameters?: IntegrationParameter[]; 14 | }): ProviderJson => ({ 15 | name: options?.name ?? 'test', 16 | services: options?.services ?? [ 17 | { id: 'test-service', baseUrl: 'service/base/url' }, 18 | ], 19 | securitySchemes: options?.security ?? [ 20 | { 21 | type: SecurityType.HTTP, 22 | id: 'basic', 23 | scheme: HttpScheme.BASIC, 24 | }, 25 | { 26 | id: 'api', 27 | type: SecurityType.APIKEY, 28 | in: ApiKeyPlacement.HEADER, 29 | name: 'Authorization', 30 | }, 31 | { 32 | id: 'bearer', 33 | type: SecurityType.HTTP, 34 | scheme: HttpScheme.BEARER, 35 | bearerFormat: 'some', 36 | }, 37 | { 38 | id: 'digest', 39 | type: SecurityType.HTTP, 40 | scheme: HttpScheme.DIGEST, 41 | }, 42 | ], 43 | defaultService: 44 | options?.services !== undefined ? options.services[0].id : 'test-service', 45 | parameters: options?.parameters ?? [ 46 | { 47 | name: 'first', 48 | description: 'first test value', 49 | }, 50 | { 51 | name: 'second', 52 | }, 53 | { 54 | name: 'third', 55 | default: 'third-default', 56 | }, 57 | { 58 | name: 'fourth', 59 | default: 'fourth-default', 60 | }, 61 | ], 62 | }); 63 | -------------------------------------------------------------------------------- /src/mock/timers.ts: -------------------------------------------------------------------------------- 1 | import type { ITimeout, ITimers } from '../core'; 2 | 3 | export class MockTimeout implements ITimeout { 4 | constructor(public value: number) {} 5 | } 6 | 7 | export class MockTimers implements ITimers { 8 | public current = Date.now(); 9 | public timers: { 10 | callback: (...args: unknown[]) => unknown; 11 | timeout: number; 12 | enabled: boolean; 13 | }[] = []; 14 | 15 | public setTimeout( 16 | callback: (...args: unknown[]) => unknown, 17 | timeout: number 18 | ): ITimeout { 19 | this.timers.push({ callback, timeout, enabled: true }); 20 | 21 | return new MockTimeout(this.timers.length - 1); 22 | } 23 | 24 | public clearTimeout(timeout: MockTimeout): void { 25 | this.timers[timeout.value].enabled = false; 26 | } 27 | 28 | public async sleep(ms: number): Promise { 29 | this.current += ms; 30 | } 31 | 32 | public now(): number { 33 | return this.current; 34 | } 35 | 36 | public tick(ms: number): void { 37 | this.current += ms; 38 | for (const timer of this.timers) { 39 | if (timer.enabled) { 40 | timer.timeout -= ms; 41 | if (timer.timeout <= 0) { 42 | timer.callback(); 43 | timer.enabled = false; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/module.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'isomorphic-form-data'; 2 | -------------------------------------------------------------------------------- /src/node/client/client.typed.ts: -------------------------------------------------------------------------------- 1 | import type { UsecaseType } from '../../core'; 2 | import { resolveProfileAst, resolveProfileId, TypedProfile } from '../../core'; 3 | import type { NonPrimitive } from '../../lib'; 4 | import { NodeFileSystem } from '../filesystem'; 5 | import { SuperfaceClientBase } from './client'; 6 | 7 | type ProfileUseCases = { 8 | [profile: string]: UsecaseType; 9 | }; 10 | 11 | export type TypedSuperfaceClient< 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | TProfiles extends ProfileUseCases 14 | > = SuperfaceClientBase & { 15 | getProfile( 16 | profile: TProfile | { id: TProfile; version?: string } 17 | ): Promise>; 18 | }; 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | export function createTypedClient>( 22 | profileDefinitions: TProfiles 23 | ): { new (): TypedSuperfaceClient } { 24 | return class TypedSuperfaceClientClass 25 | extends SuperfaceClientBase 26 | implements TypedSuperfaceClient 27 | { 28 | public async getProfile( 29 | profile: TProfile | { id: TProfile; version?: string } 30 | ): Promise> { 31 | const { id, version } = resolveProfileId( 32 | profile as string | { id: string; version?: string } 33 | ); 34 | 35 | const ast = await resolveProfileAst({ 36 | profileId: id, 37 | version, 38 | logger: this.logger, 39 | fetchInstance: this.fetchInstance, 40 | fileSystem: NodeFileSystem, 41 | config: this.config, 42 | crypto: this.crypto, 43 | superJson: this.superJson, 44 | }); 45 | const profileConfiguration = await this.internal.getProfileConfiguration( 46 | ast 47 | ); 48 | 49 | return new TypedProfile( 50 | profileConfiguration, 51 | ast, 52 | this.events, 53 | this.superJson, 54 | this.boundProfileProviderCache, 55 | this.config, 56 | this.sandbox, 57 | this.timers, 58 | NodeFileSystem, 59 | this.crypto, 60 | this.fetchInstance, 61 | Object.keys(profileDefinitions[id]), 62 | this.logger 63 | ); 64 | } 65 | }; 66 | } 67 | 68 | export const typeHelper = (): [TInput, TOutput] => { 69 | return [undefined as unknown, undefined as unknown] as [TInput, TOutput]; 70 | }; 71 | -------------------------------------------------------------------------------- /src/node/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './client.typed'; 3 | -------------------------------------------------------------------------------- /src/node/crypto/crypto.node.ts: -------------------------------------------------------------------------------- 1 | import { createHash, randomInt } from 'crypto'; 2 | 3 | import type { ICrypto } from '../../core'; 4 | 5 | export class NodeCrypto implements ICrypto { 6 | public hashString(input: string, algorithm: 'MD5' | 'sha256'): string { 7 | const hash = createHash(algorithm); 8 | hash.update(input); 9 | 10 | return hash.digest('hex'); 11 | } 12 | 13 | public randomInt(max: number): number { 14 | return randomInt(max); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/node/crypto/index.ts: -------------------------------------------------------------------------------- 1 | export { NodeCrypto } from './crypto.node'; 2 | -------------------------------------------------------------------------------- /src/node/environment/environment.node.test.ts: -------------------------------------------------------------------------------- 1 | import { NodeEnvironment } from './environment.node'; 2 | 3 | describe('NodeEnvironment', () => { 4 | const environment = new NodeEnvironment(); 5 | const variableName = 'TEST_VARIABLE'; 6 | const originalValue = process.env[variableName]; 7 | 8 | beforeEach(() => { 9 | delete process.env[variableName]; 10 | }); 11 | 12 | afterAll(() => { 13 | if (originalValue !== undefined) { 14 | process.env[variableName] = originalValue; 15 | } 16 | }); 17 | 18 | it('successfuly gets a string from environment', () => { 19 | process.env[variableName] = 'test'; 20 | 21 | expect(environment.getString(variableName)).toBe('test'); 22 | }); 23 | 24 | it('successfuly gets a number from environment', () => { 25 | process.env[variableName] = '13'; 26 | 27 | expect(environment.getNumber(variableName)).toBe(13); 28 | }); 29 | 30 | it('successfuly gets a boolean from environment', () => { 31 | process.env[variableName] = 'true'; 32 | expect(environment.getBoolean(variableName)).toBe(true); 33 | 34 | process.env[variableName] = '1'; 35 | expect(environment.getBoolean(variableName)).toBe(true); 36 | 37 | process.env[variableName] = 'false'; 38 | expect(environment.getBoolean(variableName)).toBe(false); 39 | }); 40 | 41 | it('returns NaN when the variable is not a number', () => { 42 | process.env[variableName] = 'not a number'; 43 | 44 | expect(environment.getNumber(variableName)).toBeNaN(); 45 | }); 46 | 47 | it('trims a string variable', () => { 48 | process.env[variableName] = ' test '; 49 | 50 | expect(environment.getString(variableName)).toBe('test'); 51 | }); 52 | 53 | it('returns undefined when no value is found', () => { 54 | expect(environment.getString(variableName)).toBeUndefined(); 55 | expect(environment.getNumber(variableName)).toBeUndefined(); 56 | expect(environment.getBoolean(variableName)).toBeUndefined(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/node/environment/environment.node.ts: -------------------------------------------------------------------------------- 1 | import type { IEnvironment } from '../../core'; 2 | 3 | export class NodeEnvironment implements IEnvironment { 4 | public getString(key: string): string | undefined { 5 | return process.env[key]?.trim(); 6 | } 7 | 8 | public getNumber(key: string): number | undefined { 9 | const value = process.env[key]?.trim(); 10 | 11 | if (value === undefined) { 12 | return undefined; 13 | } 14 | 15 | return Number(value); 16 | } 17 | 18 | public getBoolean(key: string): boolean | undefined { 19 | const value = process.env[key]?.trim(); 20 | 21 | if (value === undefined) { 22 | return undefined; 23 | } 24 | 25 | return value.toLowerCase() === 'true' || value === '1'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/node/environment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './environment.node'; 2 | -------------------------------------------------------------------------------- /src/node/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetch.node'; 2 | -------------------------------------------------------------------------------- /src/node/filesystem/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filesystem.node'; 2 | export { BinaryData } from './binary.node'; 3 | export type { BinaryDataOptions } from './binary.node'; 4 | -------------------------------------------------------------------------------- /src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './crypto'; 3 | export * from './environment'; 4 | export * from './fetch'; 5 | export * from './filesystem'; 6 | export * from './logger'; 7 | export * from './sandbox'; 8 | export * from './timers'; 9 | -------------------------------------------------------------------------------- /src/node/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger.node'; 2 | -------------------------------------------------------------------------------- /src/node/logger/logger.node.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug'; 2 | 3 | import type { ILogger, LogFunction } from '../../core'; 4 | import { SuperCache } from '../../lib'; 5 | 6 | export class NodeLogger implements ILogger { 7 | private cache: SuperCache = new SuperCache(); 8 | 9 | public log(name: string): LogFunction; 10 | public log(name: string, format: string, ...args: unknown[]): void; 11 | public log( 12 | name: string, 13 | format?: string, 14 | ...args: unknown[] 15 | ): void | LogFunction { 16 | const instance = this.cache.getCached(name, () => { 17 | const debugLog = createDebug('superface:' + name); 18 | if (name.endsWith(':sensitive')) { 19 | debugLog( 20 | ` 21 | WARNING: YOU HAVE ALLOWED LOGGING SENSITIVE INFORMATION. 22 | THIS LOGGING LEVEL DOES NOT PREVENT LEAKING SECRETS AND SHOULD NOT BE USED IF THE LOGS ARE GOING TO BE SHARED. 23 | CONSIDER DISABLING SENSITIVE INFORMATION LOGGING BY APPENDING THE DEBUG ENVIRONMENT VARIABLE WITH ",-*:sensitive". 24 | ` 25 | ); 26 | } 27 | 28 | return debugLog; 29 | }); 30 | 31 | if (format === undefined) { 32 | return instance; 33 | } 34 | 35 | instance(format, ...args); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/node/sandbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sandbox.node'; 2 | -------------------------------------------------------------------------------- /src/node/sandbox/sandbox.node.ts: -------------------------------------------------------------------------------- 1 | import { VM } from 'vm2'; 2 | 3 | import type { Stdlib } from '../../core/interpreter/stdlib'; 4 | import type { IConfig, ILogger } from '../../interfaces'; 5 | import type { ISandbox } from '../../interfaces/sandbox'; 6 | import type { NonPrimitive } from '../../lib'; 7 | import { isClassInstance, isNone } from '../../lib'; 8 | 9 | const DEBUG_NAMESPACE = 'sandbox'; 10 | 11 | export class NodeSandbox implements ISandbox { 12 | public evalScript( 13 | config: IConfig, 14 | js: string, 15 | stdlib?: Stdlib, 16 | logger?: ILogger, 17 | variableDefinitions?: NonPrimitive 18 | ): unknown { 19 | const vm = new VM({ 20 | sandbox: { 21 | std: stdlib, 22 | ...variableDefinitions, 23 | }, 24 | compiler: 'javascript', 25 | wasm: false, 26 | eval: false, 27 | timeout: config.sandboxTimeout, 28 | fixAsync: true, 29 | }); 30 | 31 | const log = logger?.log(DEBUG_NAMESPACE); 32 | 33 | // Defensively delete global objects 34 | // These deletions mostly don't protect, but produce "nicer" errors for the user 35 | vm.run( 36 | ` 37 | 'use strict' 38 | 39 | delete global.require // Forbidden 40 | delete global.process // Forbidden 41 | delete global.console // Forbidden/useless 42 | 43 | delete global.setTimeout 44 | delete global.setInterval 45 | delete global.setImmediate 46 | delete global.clearTimeout 47 | delete global.clearInterval 48 | delete global.clearImmediate 49 | // delete global.String 50 | // delete global.Number 51 | // delete global.Buffer 52 | // delete global.Boolean 53 | // delete global.Array 54 | // delete global.Date 55 | // delete global.RegExp // Forbidden - needed for object literals to work, weirdly 56 | delete global.Function // Can be restored by taking .constructor of any function, but the VM protection kicks in 57 | // delete global.Object 58 | delete global.VMError // Useless 59 | delete global.Proxy // Forbidden 60 | delete global.Reflect // Forbidden 61 | // delete global.Promise // Forbidden, also VM protection - BUT needed for object literals to work, weirdly 62 | delete global.Symbol // Forbidden 63 | 64 | delete global.eval // Forbidden, also VM protects 65 | delete global.WebAssembly // Forbidden, also VM protects 66 | delete global.AsyncFunction // Forbidden, also VM protects 67 | delete global.SharedArrayBuffer // Just in case 68 | ` 69 | ); 70 | 71 | log?.('Evaluating:', js); 72 | const result = vm.run( 73 | ` 74 | 'use strict'; 75 | const vmResult = ${js} 76 | ;vmResult` 77 | ) as unknown; 78 | const resultVm2Fixed = this.vm2ExtraArrayKeysFixup(result); 79 | 80 | log?.('Result: %O', resultVm2Fixed); 81 | 82 | return resultVm2Fixed; 83 | } 84 | 85 | private vm2ExtraArrayKeysFixup(value: T): T { 86 | if (typeof value !== 'object') { 87 | return value; 88 | } 89 | 90 | if (isNone(value)) { 91 | return value; 92 | } 93 | 94 | if ( 95 | Buffer.isBuffer(value) || 96 | value instanceof ArrayBuffer || 97 | isClassInstance(value) || 98 | Symbol.iterator in value || 99 | Symbol.asyncIterator in value 100 | ) { 101 | return value; 102 | } 103 | 104 | if (Array.isArray(value)) { 105 | const newArray: unknown[] = []; 106 | for (let i = 0; i < value.length; i += 1) { 107 | newArray[i] = this.vm2ExtraArrayKeysFixup(value[i]); 108 | } 109 | 110 | return newArray as unknown as T; 111 | } 112 | 113 | const newObject: Record = {}; 114 | const currentObject = value as Record; 115 | for (const key of Object.keys(value)) { 116 | newObject[key] = this.vm2ExtraArrayKeysFixup(currentObject[key]); 117 | } 118 | 119 | return newObject as T; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/node/timers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timers.node'; 2 | -------------------------------------------------------------------------------- /src/node/timers/timers.node.ts: -------------------------------------------------------------------------------- 1 | import type { ITimeout, ITimers } from '../../core'; 2 | 3 | export class NodeTimeout implements ITimeout { 4 | constructor(public value: NodeJS.Timeout) {} 5 | } 6 | 7 | export class NodeTimers implements ITimers { 8 | public setTimeout( 9 | callback: (...args: unknown[]) => unknown, 10 | timeout: number 11 | ): ITimeout { 12 | return new NodeTimeout(setTimeout(callback, timeout)); 13 | } 14 | 15 | public clearTimeout(timeout: NodeTimeout): void { 16 | clearTimeout(timeout.value); 17 | } 18 | 19 | public async sleep(ms: number): Promise { 20 | return new Promise(resolve => setTimeout(resolve, ms)); 21 | } 22 | 23 | public now(): number { 24 | return Date.now(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/private/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../core/services'; 2 | -------------------------------------------------------------------------------- /src/schema-tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './superjson'; 2 | -------------------------------------------------------------------------------- /src/schema-tools/superjson/errors.helpers.ts: -------------------------------------------------------------------------------- 1 | import { SDKExecutionError } from '../../lib'; 2 | 3 | export function superJsonNotFoundError( 4 | path: string, 5 | error?: Error 6 | ): SDKExecutionError { 7 | const errorMessage = [`super.json not found in "${path}"`]; 8 | if (error !== undefined) { 9 | errorMessage.push(error.toString()); 10 | } 11 | 12 | return new SDKExecutionError('Unable to find super.json', errorMessage, []); 13 | } 14 | 15 | export function superJsonNotAFileError(path: string): SDKExecutionError { 16 | return new SDKExecutionError( 17 | 'super.json is not a file', 18 | [`"${path}" is not a file`], 19 | [] 20 | ); 21 | } 22 | 23 | export function superJsonFormatError(error: Error): SDKExecutionError { 24 | return new SDKExecutionError( 25 | 'super.json format is invalid', 26 | [error.toString()], 27 | [] 28 | ); 29 | } 30 | 31 | export function superJsonReadError(error: Error): SDKExecutionError { 32 | return new SDKExecutionError( 33 | 'Unable to read super.json', 34 | [error.toString()], 35 | [] 36 | ); 37 | } 38 | export function profileNotFoundError(profileName: string): SDKExecutionError { 39 | return new SDKExecutionError( 40 | `Profile "${profileName}" not found in super.json`, 41 | [], 42 | [] 43 | ); 44 | } 45 | 46 | export function providersNotSetError(profileName: string): SDKExecutionError { 47 | return new SDKExecutionError( 48 | `Unable to set priority for "${profileName}"`, 49 | [`Providers not set for profile "${profileName}"`], 50 | [`Make sure profile ${profileName} has configured providers.`] 51 | ); 52 | } 53 | 54 | export function invalidProfileProviderError( 55 | profileProviderSettings: string 56 | ): SDKExecutionError { 57 | return new SDKExecutionError( 58 | 'Invalid profile provider entry format', 59 | [`Settings: ${profileProviderSettings}`], 60 | [] 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/schema-tools/superjson/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mutate'; 2 | export * from './normalize'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/schema-tools/superjson/superjson.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import { join as joinPath } from 'path'; 3 | 4 | import { DEFAULT_SUPERFACE_PATH } from '../../core/config'; 5 | import { NodeFileSystem } from '../../node'; 6 | import { loadSuperJson } from './utils'; 7 | 8 | const { unlink, rmdir, mkdir, writeFile } = promises; 9 | const cwd = () => process.cwd(); 10 | const basedir = process.cwd(); 11 | 12 | describe('class SuperJson integration tests', () => { 13 | describe('super.json present', () => { 14 | beforeEach(async () => { 15 | const superJson = `{ 16 | "profiles": { 17 | "send-message": { 18 | "version": "1.0.0", 19 | "providers": { 20 | "acme": { 21 | "mapVariant": "my-bugfix", 22 | "mapRevision": "1113" 23 | } 24 | } 25 | } 26 | }, 27 | "providers": { 28 | "acme": { 29 | "security": [ 30 | { 31 | "id": "myApiKey", 32 | "apikey": "SECRET" 33 | } 34 | ] 35 | } 36 | } 37 | }`; 38 | 39 | await mkdir(joinPath(basedir, 'superface')); 40 | await writeFile(joinPath(basedir, 'superface', 'super.json'), superJson); 41 | }); 42 | 43 | afterEach(async () => { 44 | await unlink(joinPath(basedir, 'superface', 'super.json')); 45 | await rmdir(joinPath(basedir, 'superface')); 46 | }); 47 | 48 | it('correctly parses super.json when it is present', async () => { 49 | const result = await loadSuperJson( 50 | DEFAULT_SUPERFACE_PATH({ path: { join: joinPath, cwd } }), 51 | NodeFileSystem 52 | ); 53 | expect(result.isOk()).toBe(true); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/schema-tools/superjson/superjson.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaceai/one-sdk-js/a56886b60585896d31579e0066858cd8a96ffc7d/src/schema-tools/superjson/superjson.ts -------------------------------------------------------------------------------- /src/schema-tools/superjson/utils.ts: -------------------------------------------------------------------------------- 1 | import type { SuperJsonDocument } from '@superfaceai/ast'; 2 | import { 3 | assertSuperJsonDocument, 4 | FILE_URI_PROTOCOL, 5 | FILE_URI_REGEX, 6 | isFileURIString, 7 | } from '@superfaceai/ast'; 8 | 9 | import type { IFileSystem, ILogger } from '../../interfaces'; 10 | import type { Result, SDKExecutionError } from '../../lib'; 11 | import { ensureErrorSubclass, err, ok } from '../../lib'; 12 | import { 13 | superJsonFormatError, 14 | superJsonNotAFileError, 15 | superJsonNotFoundError, 16 | superJsonReadError, 17 | } from './errors.helpers'; 18 | 19 | const DEBUG_NAMESPACE = 'superjson'; 20 | 21 | export const SUPERFACE_DIR = 'superface'; 22 | export const META_FILE = 'super.json'; 23 | 24 | /** 25 | * Detects the existence of a `super.json` file in specified number of levels 26 | * of parent directories. 27 | * 28 | * @param cwd - currently scanned working directory 29 | * 30 | * Returns relative path to a directory where `super.json` is detected. 31 | */ 32 | export async function detectSuperJson( 33 | cwd: string, 34 | fileSystem: IFileSystem, 35 | level?: number 36 | ): Promise { 37 | // check whether super.json is accessible in cwd 38 | if (await fileSystem.isAccessible(fileSystem.path.join(cwd, META_FILE))) { 39 | return fileSystem.path.normalize( 40 | fileSystem.path.relative(fileSystem.path.cwd(), cwd) 41 | ); 42 | } 43 | 44 | // check whether super.json is accessible in cwd/superface 45 | if ( 46 | await fileSystem.isAccessible( 47 | fileSystem.path.join(cwd, SUPERFACE_DIR, META_FILE) 48 | ) 49 | ) { 50 | return fileSystem.path.normalize( 51 | fileSystem.path.relative( 52 | fileSystem.path.cwd(), 53 | fileSystem.path.join(cwd, SUPERFACE_DIR) 54 | ) 55 | ); 56 | } 57 | 58 | // default behaviour - do not scan outside cwd 59 | if (level === undefined || level < 1) { 60 | return undefined; 61 | } 62 | 63 | // check if user has permissions outside cwd 64 | cwd = fileSystem.path.join(cwd, '..'); 65 | if (!(await fileSystem.isAccessible(cwd))) { 66 | return undefined; 67 | } 68 | 69 | return await detectSuperJson(cwd, fileSystem, --level); 70 | } 71 | 72 | export function parseSuperJson( 73 | input: unknown 74 | ): Result { 75 | try { 76 | const superdocument = assertSuperJsonDocument(input); 77 | 78 | return ok(superdocument); 79 | } catch (e: unknown) { 80 | return err(superJsonFormatError(ensureErrorSubclass(e))); 81 | } 82 | } 83 | 84 | export function loadSuperJsonSync( 85 | path: string, 86 | fileSystem: IFileSystem, 87 | logger?: ILogger 88 | ): Result { 89 | try { 90 | if (!fileSystem.sync.isAccessible(path)) { 91 | return err(superJsonNotFoundError(path)); 92 | } 93 | 94 | if (!fileSystem.sync.isFile(path)) { 95 | return err(superJsonNotAFileError(path)); 96 | } 97 | } catch (e: unknown) { 98 | return err(superJsonNotFoundError(path, ensureErrorSubclass(e))); 99 | } 100 | 101 | let superjson: unknown; 102 | const superraw = fileSystem.sync.readFile(path); 103 | if (superraw.isOk()) { 104 | superjson = JSON.parse(superraw.value); 105 | } else { 106 | return err(superJsonReadError(ensureErrorSubclass(superraw.error))); 107 | } 108 | 109 | const superdocument = parseSuperJson(superjson); 110 | if (superdocument.isErr()) { 111 | return err(superdocument.error); 112 | } 113 | 114 | logger?.log(DEBUG_NAMESPACE, `loaded super.json from ${path}`); 115 | 116 | return superdocument; 117 | } 118 | 119 | /** 120 | * Attempts to load super.json file at `path` 121 | */ 122 | export async function loadSuperJson( 123 | path: string, 124 | fileSystem: IFileSystem, 125 | logger?: ILogger 126 | ): Promise> { 127 | try { 128 | if (!(await fileSystem.isAccessible(path))) { 129 | return err(superJsonNotFoundError(path)); 130 | } 131 | 132 | if (!(await fileSystem.isFile(path))) { 133 | return err(superJsonNotAFileError(path)); 134 | } 135 | } catch (e: unknown) { 136 | return err(superJsonNotFoundError(path, ensureErrorSubclass(e))); 137 | } 138 | 139 | let superjson: unknown; 140 | const superraw = await fileSystem.readFile(path); 141 | if (superraw.isOk()) { 142 | superjson = JSON.parse(superraw.value); 143 | } else { 144 | return err(superJsonReadError(ensureErrorSubclass(superraw.error))); 145 | } 146 | 147 | const superdocument = parseSuperJson(superjson); 148 | if (superdocument.isErr()) { 149 | return err(superdocument.error); 150 | } 151 | 152 | logger?.log(DEBUG_NAMESPACE, `loaded super.json from ${path}`); 153 | 154 | return superdocument; 155 | } 156 | 157 | export const trimFileURI = (path: string): string => 158 | path.replace(FILE_URI_REGEX, ''); 159 | 160 | export const composeFileURI = ( 161 | path: string, 162 | normalize: IFileSystem['path']['normalize'] 163 | ): string => { 164 | if (isFileURIString(path)) { 165 | return path; 166 | } 167 | 168 | const normalized = normalize(path); 169 | 170 | return path.startsWith('../') 171 | ? `${FILE_URI_PROTOCOL}${normalized}` 172 | : `${FILE_URI_PROTOCOL}./${normalized}`; 173 | }; 174 | -------------------------------------------------------------------------------- /src/user-agent.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-var-requires 2 | const packageJson = require('../package.json'); 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 5 | export const VERSION: string = packageJson.version; 6 | export const USER_AGENT = `superfaceai one-sdk-js/${VERSION} (${process.platform}-${process.arch}) ${process.release.name}-${process.version}`; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["ESNext"], 10 | "rootDir": "./src", 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitOverride": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "plugins": [ 20 | { "transform": "typescript-transform-paths", "afterDeclarations": true } 21 | ], 22 | "resolveJsonModule": true, 23 | "sourceMap": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "strictPropertyInitialization": true, 27 | "target": "ES5", 28 | "typeRoots": ["node_modules/@types"] 29 | }, 30 | "include": ["src/**/*.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/*.test.ts", "src/mock/**/*.ts"] 4 | } 5 | --------------------------------------------------------------------------------