├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── plugins ├── auth-basic │ ├── package.json │ └── src │ │ └── index.ts ├── auth-bearer │ ├── package.json │ └── src │ │ └── index.ts ├── auth-jwt │ ├── package.json │ └── src │ │ └── index.ts ├── auth-oauth2 │ ├── package.json │ └── src │ │ ├── getAccessToken.ts │ │ ├── getOrRefreshAccessToken.ts │ │ ├── grants │ │ ├── authorizationCode.ts │ │ ├── clientCredentials.ts │ │ ├── implicit.ts │ │ └── password.ts │ │ ├── index.ts │ │ └── store.ts ├── exporter-curl │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ └── index.test.ts ├── filter-jsonpath │ ├── package.json │ └── src │ │ └── index.ts ├── filter-xpath │ ├── package.json │ └── src │ │ └── index.ts ├── importer-curl │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ └── index.test.ts ├── importer-insomnia │ ├── package.json │ ├── src │ │ ├── common.ts │ │ ├── index.ts │ │ ├── v4.ts │ │ └── v5.ts │ └── tests │ │ ├── fixtures │ │ ├── basic.input.json │ │ ├── basic.output.json │ │ ├── version-5-minimal.input.yaml │ │ ├── version-5-minimal.output.json │ │ ├── version-5.input.yaml │ │ └── version-5.output.json │ │ └── index.test.ts ├── importer-openapi │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ ├── fixtures │ │ └── petstore.yaml │ │ └── index.test.ts ├── importer-postman │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ ├── fixtures │ │ ├── nested.input.json │ │ ├── nested.output.json │ │ ├── params.input.json │ │ └── params.output.json │ │ └── index.test.ts ├── importer-yaak │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ └── index.test.ts ├── template-function-cookie │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-encode │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-fs │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-hash │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-json │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-prompt │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-regex │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-request │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-response │ ├── package.json │ └── src │ │ └── index.ts ├── template-function-uuid │ ├── package.json │ └── src │ │ └── index.ts └── template-function-xml │ ├── package.json │ └── src │ └── index.ts └── tsconfig.json /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: push 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | name: CI 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v4 14 | 15 | - name: Install @yaakapp/cli 16 | run: npm install -g @yaakapp/cli 17 | 18 | - name: Install Dependencies 19 | run: npm install 20 | 21 | - name: Build Plugins 22 | run: npm run build 23 | 24 | - name: Lint 25 | run: npm run lint 26 | 27 | - name: Run Tests 28 | run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | 131 | # Other 132 | build 133 | .idea 134 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yaak App 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Plugins were moved into https://github.com/mountain-loop/yaak/tree/master/plugins 3 | 4 | # Yaak Plugins 5 | 6 | This repository contains all the Yaak plugins that are bundled within the application (can't be uninstalled). 7 | 8 | ## Feedback and Bug Reports 9 | 10 | All feedback, bug reports, questions, and feature requests should be reported to 11 | [feedback.yaak.app](https://feedback.yaak.app). 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/plugins", 3 | "repository": "https://github.com/yaakapp/plugins", 4 | "workspaces": [ 5 | "plugins/*" 6 | ], 7 | "private": true, 8 | "scripts": { 9 | "build": "workspaces-run --parallel -- npm run --workspaces build", 10 | "dev": "workspaces-run --parallel -- npm run --workspaces dev", 11 | "test": "vitest run", 12 | "lint": "tsc" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^22.7.4", 16 | "jsonpath": "^1.1.1", 17 | "typescript": "^5.6.2", 18 | "vitest": "^2.0.4", 19 | "workspaces-run": "^1.0.2", 20 | "@yaakapp/cli": "^0.1.5" 21 | }, 22 | "dependencies": { 23 | "@yaakapp/api": "^0.6.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugins/auth-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/auth-basic", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/auth-basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | authentication: { 5 | name: 'basic', 6 | label: 'Basic Auth', 7 | shortLabel: 'Basic', 8 | args: [{ 9 | type: 'text', 10 | name: 'username', 11 | label: 'Username', 12 | optional: true, 13 | }, { 14 | type: 'text', 15 | name: 'password', 16 | label: 'Password', 17 | optional: true, 18 | password: true, 19 | }], 20 | async onApply(_ctx, { values }) { 21 | const { username, password } = values; 22 | const value = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); 23 | return { setHeaders: [{ name: 'Authorization', value }] }; 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /plugins/auth-bearer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/auth-bearer", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/auth-bearer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | authentication: { 5 | name: 'bearer', 6 | label: 'Bearer Token', 7 | shortLabel: 'Bearer', 8 | args: [{ 9 | type: 'text', 10 | name: 'token', 11 | label: 'Token', 12 | optional: true, 13 | password: true, 14 | }], 15 | async onApply(_ctx, { values }) { 16 | const { token } = values; 17 | const value = `Bearer ${token}`.trim(); 18 | return { setHeaders: [{ name: 'Authorization', value }] }; 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /plugins/auth-jwt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/auth-jwt", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "jsonwebtoken": "^9.0.2" 11 | }, 12 | "devDependencies": { 13 | "@types/jsonwebtoken": "^9.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/auth-jwt/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginDefinition } from '@yaakapp/api'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | const algorithms = [ 5 | 'HS256', 6 | 'HS384', 7 | 'HS512', 8 | 'RS256', 9 | 'RS384', 10 | 'RS512', 11 | 'PS256', 12 | 'PS384', 13 | 'PS512', 14 | 'ES256', 15 | 'ES384', 16 | 'ES512', 17 | 'none', 18 | ] as const; 19 | 20 | const defaultAlgorithm = algorithms[0]; 21 | 22 | export const plugin: PluginDefinition = { 23 | authentication: { 24 | name: 'jwt', 25 | label: 'JWT Bearer', 26 | shortLabel: 'JWT', 27 | args: [ 28 | { 29 | type: 'select', 30 | name: 'algorithm', 31 | label: 'Algorithm', 32 | hideLabel: true, 33 | defaultValue: defaultAlgorithm, 34 | options: algorithms.map(value => ({ label: value === 'none' ? 'None' : value, value })), 35 | }, 36 | { 37 | type: 'text', 38 | name: 'secret', 39 | label: 'Secret or Private Key', 40 | password: true, 41 | optional: true, 42 | multiLine: true, 43 | }, 44 | { 45 | type: 'checkbox', 46 | name: 'secretBase64', 47 | label: 'Secret is base64 encoded', 48 | }, 49 | { 50 | type: 'editor', 51 | name: 'payload', 52 | label: 'Payload', 53 | language: 'json', 54 | defaultValue: '{\n "foo": "bar"\n}', 55 | placeholder: '{ }', 56 | }, 57 | ], 58 | async onApply(_ctx, { values }) { 59 | const { algorithm, secret: _secret, secretBase64, payload } = values; 60 | const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`; 61 | const token = jwt.sign(`${payload}`, secret, { algorithm: algorithm as any }); 62 | const value = `Bearer ${token}`; 63 | return { setHeaders: [{ name: 'Authorization', value }] }; 64 | } 65 | , 66 | }, 67 | } 68 | ; 69 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/auth-oauth2", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/getAccessToken.ts: -------------------------------------------------------------------------------- 1 | import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api'; 2 | import { readFileSync } from 'node:fs'; 3 | import { AccessTokenRawResponse } from './store'; 4 | 5 | export async function getAccessToken( 6 | ctx: Context, { 7 | accessTokenUrl, 8 | scope, 9 | audience, 10 | params, 11 | grantType, 12 | credentialsInBody, 13 | clientId, 14 | clientSecret, 15 | }: { 16 | clientId: string; 17 | clientSecret: string; 18 | grantType: string; 19 | accessTokenUrl: string; 20 | scope: string | null; 21 | audience: string | null; 22 | credentialsInBody: boolean; 23 | params: HttpUrlParameter[]; 24 | }): Promise { 25 | console.log('Getting access token', accessTokenUrl); 26 | const httpRequest: Partial = { 27 | method: 'POST', 28 | url: accessTokenUrl, 29 | bodyType: 'application/x-www-form-urlencoded', 30 | body: { 31 | form: [ 32 | { name: 'grant_type', value: grantType }, 33 | ...params, 34 | ], 35 | }, 36 | headers: [ 37 | { name: 'User-Agent', value: 'yaak' }, 38 | { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' }, 39 | { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, 40 | ], 41 | }; 42 | 43 | if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope }); 44 | if (audience) httpRequest.body!.form.push({ name: 'audience', value: audience }); 45 | 46 | if (credentialsInBody) { 47 | httpRequest.body!.form.push({ name: 'client_id', value: clientId }); 48 | httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret }); 49 | } else { 50 | const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); 51 | httpRequest.headers!.push({ name: 'Authorization', value }); 52 | } 53 | 54 | const resp = await ctx.httpRequest.send({ httpRequest }); 55 | 56 | const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : ''; 57 | 58 | if (resp.status < 200 || resp.status >= 300) { 59 | throw new Error('Failed to fetch access token with status=' + resp.status + ' and body=' + body); 60 | } 61 | 62 | let response; 63 | try { 64 | response = JSON.parse(body); 65 | } catch { 66 | response = Object.fromEntries(new URLSearchParams(body)); 67 | } 68 | 69 | if (response.error) { 70 | throw new Error('Failed to fetch access token with ' + response.error); 71 | } 72 | 73 | return response; 74 | } 75 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/getOrRefreshAccessToken.ts: -------------------------------------------------------------------------------- 1 | import { Context, HttpRequest } from '@yaakapp/api'; 2 | import { readFileSync } from 'node:fs'; 3 | import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store'; 4 | 5 | export async function getOrRefreshAccessToken(ctx: Context, contextId: string, { 6 | scope, 7 | accessTokenUrl, 8 | credentialsInBody, 9 | clientId, 10 | clientSecret, 11 | forceRefresh, 12 | }: { 13 | scope: string | null; 14 | accessTokenUrl: string; 15 | credentialsInBody: boolean; 16 | clientId: string; 17 | clientSecret: string; 18 | forceRefresh?: boolean; 19 | }): Promise { 20 | const token = await getToken(ctx, contextId); 21 | if (token == null) { 22 | return null; 23 | } 24 | 25 | const now = Date.now(); 26 | const isExpired = token.expiresAt && now > token.expiresAt; 27 | 28 | // Return the current access token if it's still valid 29 | if (!isExpired && !forceRefresh) { 30 | return token; 31 | } 32 | 33 | // Token is expired, but there's no refresh token :( 34 | if (!token.response.refresh_token) { 35 | return null; 36 | } 37 | 38 | // Access token is expired, so get a new one 39 | const httpRequest: Partial = { 40 | method: 'POST', 41 | url: accessTokenUrl, 42 | bodyType: 'application/x-www-form-urlencoded', 43 | body: { 44 | form: [ 45 | { name: 'grant_type', value: 'refresh_token' }, 46 | { name: 'refresh_token', value: token.response.refresh_token }, 47 | ], 48 | }, 49 | headers: [ 50 | { name: 'User-Agent', value: 'yaak' }, 51 | { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' }, 52 | { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, 53 | ], 54 | }; 55 | 56 | if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope }); 57 | 58 | if (credentialsInBody) { 59 | httpRequest.body!.form.push({ name: 'client_id', value: clientId }); 60 | httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret }); 61 | } else { 62 | const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); 63 | httpRequest.headers!.push({ name: 'Authorization', value }); 64 | } 65 | 66 | const resp = await ctx.httpRequest.send({ httpRequest }); 67 | 68 | if (resp.status === 401) { 69 | // Bad refresh token, so we'll force it to fetch a fresh access token by deleting 70 | // and returning null; 71 | console.log('Unauthorized refresh_token request'); 72 | await deleteToken(ctx, contextId); 73 | return null; 74 | } 75 | 76 | const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : ''; 77 | 78 | if (resp.status < 200 || resp.status >= 300) { 79 | throw new Error('Failed to refresh access token with status=' + resp.status + ' and body=' + body); 80 | } 81 | 82 | let response; 83 | try { 84 | response = JSON.parse(body); 85 | } catch { 86 | response = Object.fromEntries(new URLSearchParams(body)); 87 | } 88 | 89 | if (response.error) { 90 | throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`); 91 | } 92 | 93 | const newResponse: AccessTokenRawResponse = { 94 | ...response, 95 | // Assign a new one or keep the old one, 96 | refresh_token: response.refresh_token ?? token.response.refresh_token, 97 | }; 98 | return storeToken(ctx, contextId, newResponse); 99 | } 100 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/grants/authorizationCode.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@yaakapp/api'; 2 | import { createHash, randomBytes } from 'node:crypto'; 3 | import { getAccessToken } from '../getAccessToken'; 4 | import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; 5 | import { AccessToken, getDataDirKey, storeToken } from '../store'; 6 | 7 | export const PKCE_SHA256 = 'S256'; 8 | export const PKCE_PLAIN = 'plain'; 9 | export const DEFAULT_PKCE_METHOD = PKCE_SHA256; 10 | 11 | export async function getAuthorizationCode( 12 | ctx: Context, 13 | contextId: string, 14 | { 15 | authorizationUrl: authorizationUrlRaw, 16 | accessTokenUrl, 17 | clientId, 18 | clientSecret, 19 | redirectUri, 20 | scope, 21 | state, 22 | audience, 23 | credentialsInBody, 24 | pkce, 25 | }: { 26 | authorizationUrl: string; 27 | accessTokenUrl: string; 28 | clientId: string; 29 | clientSecret: string; 30 | redirectUri: string | null; 31 | scope: string | null; 32 | state: string | null; 33 | audience: string | null; 34 | credentialsInBody: boolean; 35 | pkce: { 36 | challengeMethod: string | null; 37 | codeVerifier: string | null; 38 | } | null; 39 | }, 40 | ): Promise { 41 | const token = await getOrRefreshAccessToken(ctx, contextId, { 42 | accessTokenUrl, 43 | scope, 44 | clientId, 45 | clientSecret, 46 | credentialsInBody, 47 | }); 48 | if (token != null) { 49 | return token; 50 | } 51 | 52 | const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`); 53 | authorizationUrl.searchParams.set('response_type', 'code'); 54 | authorizationUrl.searchParams.set('client_id', clientId); 55 | if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri); 56 | if (scope) authorizationUrl.searchParams.set('scope', scope); 57 | if (state) authorizationUrl.searchParams.set('state', state); 58 | if (audience) authorizationUrl.searchParams.set('audience', audience); 59 | if (pkce) { 60 | const verifier = pkce.codeVerifier || createPkceCodeVerifier(); 61 | const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD; 62 | authorizationUrl.searchParams.set('code_challenge', createPkceCodeChallenge(verifier, challengeMethod)); 63 | authorizationUrl.searchParams.set('code_challenge_method', challengeMethod); 64 | } 65 | 66 | return new Promise(async (resolve, reject) => { 67 | const authorizationUrlStr = authorizationUrl.toString(); 68 | console.log('Authorizing', authorizationUrlStr); 69 | 70 | let foundCode = false; 71 | 72 | let { close } = await ctx.window.openUrl({ 73 | url: authorizationUrlStr, 74 | label: 'oauth-authorization-url', 75 | dataDirKey: await getDataDirKey(ctx, contextId), 76 | async onClose() { 77 | if (!foundCode) { 78 | reject(new Error('Authorization window closed')); 79 | } 80 | }, 81 | async onNavigate({ url: urlStr }) { 82 | const url = new URL(urlStr); 83 | if (url.searchParams.has('error')) { 84 | return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`)); 85 | } 86 | const code = url.searchParams.get('code'); 87 | if (!code) { 88 | return; // Could be one of many redirects in a chain, so skip it 89 | } 90 | 91 | // Close the window here, because we don't need it anymore! 92 | foundCode = true; 93 | close(); 94 | 95 | const response = await getAccessToken(ctx, { 96 | grantType: 'authorization_code', 97 | accessTokenUrl, 98 | clientId, 99 | clientSecret, 100 | scope, 101 | audience, 102 | credentialsInBody, 103 | params: [ 104 | { name: 'code', value: code }, 105 | ...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []), 106 | ], 107 | }); 108 | 109 | try { 110 | resolve(await storeToken(ctx, contextId, response)); 111 | } catch (err) { 112 | reject(err); 113 | } 114 | }, 115 | }); 116 | }); 117 | } 118 | 119 | function createPkceCodeVerifier() { 120 | return encodeForPkce(randomBytes(32)); 121 | } 122 | 123 | function createPkceCodeChallenge(verifier: string, method: string) { 124 | if (method === 'plain') { 125 | return verifier; 126 | } 127 | 128 | const hash = encodeForPkce(createHash('sha256').update(verifier).digest()); 129 | return hash 130 | .replace(/=/g, '') // Remove padding '=' 131 | .replace(/\+/g, '-') // Replace '+' with '-' 132 | .replace(/\//g, '_'); // Replace '/' with '_' 133 | } 134 | 135 | function encodeForPkce(bytes: Buffer) { 136 | return bytes.toString('base64') 137 | .replace(/=/g, '') // Remove padding '=' 138 | .replace(/\+/g, '-') // Replace '+' with '-' 139 | .replace(/\//g, '_'); // Replace '/' with '_' 140 | } 141 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/grants/clientCredentials.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@yaakapp/api'; 2 | import { getAccessToken } from '../getAccessToken'; 3 | import { getToken, storeToken } from '../store'; 4 | 5 | export async function getClientCredentials( 6 | ctx: Context, 7 | contextId: string, 8 | { 9 | accessTokenUrl, 10 | clientId, 11 | clientSecret, 12 | scope, 13 | audience, 14 | credentialsInBody, 15 | }: { 16 | accessTokenUrl: string; 17 | clientId: string; 18 | clientSecret: string; 19 | scope: string | null; 20 | audience: string | null; 21 | credentialsInBody: boolean; 22 | }, 23 | ) { 24 | const token = await getToken(ctx, contextId); 25 | if (token) { 26 | // resolve(token.response.access_token); 27 | // TODO: Refresh token if expired 28 | // return; 29 | } 30 | 31 | const response = await getAccessToken(ctx, { 32 | grantType: 'client_credentials', 33 | accessTokenUrl, 34 | audience, 35 | clientId, 36 | clientSecret, 37 | scope, 38 | credentialsInBody, 39 | params: [], 40 | }); 41 | 42 | return storeToken(ctx, contextId, response); 43 | } 44 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/grants/implicit.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@yaakapp/api'; 2 | import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store'; 3 | 4 | export function getImplicit( 5 | ctx: Context, 6 | contextId: string, 7 | { 8 | authorizationUrl: authorizationUrlRaw, 9 | responseType, 10 | clientId, 11 | redirectUri, 12 | scope, 13 | state, 14 | audience, 15 | }: { 16 | authorizationUrl: string; 17 | responseType: string; 18 | clientId: string; 19 | redirectUri: string | null; 20 | scope: string | null; 21 | state: string | null; 22 | audience: string | null; 23 | }, 24 | ) :Promise { 25 | return new Promise(async (resolve, reject) => { 26 | const token = await getToken(ctx, contextId); 27 | if (token) { 28 | // resolve(token.response.access_token); 29 | // TODO: Refresh token if expired 30 | // return; 31 | } 32 | 33 | const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`); 34 | authorizationUrl.searchParams.set('response_type', 'token'); 35 | authorizationUrl.searchParams.set('client_id', clientId); 36 | if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri); 37 | if (scope) authorizationUrl.searchParams.set('scope', scope); 38 | if (state) authorizationUrl.searchParams.set('state', state); 39 | if (audience) authorizationUrl.searchParams.set('audience', audience); 40 | if (responseType.includes('id_token')) { 41 | authorizationUrl.searchParams.set('nonce', String(Math.floor(Math.random() * 9999999999999) + 1)); 42 | } 43 | 44 | const authorizationUrlStr = authorizationUrl.toString(); 45 | let foundAccessToken = false; 46 | let { close } = await ctx.window.openUrl({ 47 | url: authorizationUrlStr, 48 | label: 'oauth-authorization-url', 49 | async onClose() { 50 | if (!foundAccessToken) { 51 | reject(new Error('Authorization window closed')); 52 | } 53 | }, 54 | async onNavigate({ url: urlStr }) { 55 | const url = new URL(urlStr); 56 | if (url.searchParams.has('error')) { 57 | return reject(Error(`Failed to authorize: ${url.searchParams.get('error')}`)); 58 | } 59 | 60 | const hash = url.hash.slice(1); 61 | const params = new URLSearchParams(hash); 62 | 63 | const accessToken = params.get('access_token'); 64 | if (!accessToken) { 65 | return; 66 | } 67 | foundAccessToken = true; 68 | 69 | // Close the window here, because we don't need it anymore 70 | close(); 71 | 72 | const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse; 73 | try { 74 | resolve(await storeToken(ctx, contextId, response)); 75 | } catch (err) { 76 | reject(err); 77 | } 78 | }, 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/grants/password.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@yaakapp/api'; 2 | import { getAccessToken } from '../getAccessToken'; 3 | import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; 4 | import { AccessToken, storeToken } from '../store'; 5 | 6 | export async function getPassword( 7 | ctx: Context, 8 | contextId: string, 9 | { 10 | accessTokenUrl, 11 | clientId, 12 | clientSecret, 13 | username, 14 | password, 15 | credentialsInBody, 16 | audience, 17 | scope, 18 | }: { 19 | accessTokenUrl: string; 20 | clientId: string; 21 | clientSecret: string; 22 | username: string; 23 | password: string; 24 | scope: string | null; 25 | audience: string | null; 26 | credentialsInBody: boolean; 27 | }, 28 | ): Promise { 29 | const token = await getOrRefreshAccessToken(ctx, contextId, { 30 | accessTokenUrl, 31 | scope, 32 | clientId, 33 | clientSecret, 34 | credentialsInBody, 35 | }); 36 | if (token != null) { 37 | return token; 38 | } 39 | 40 | const response = await getAccessToken(ctx, { 41 | accessTokenUrl, 42 | clientId, 43 | clientSecret, 44 | scope, 45 | audience, 46 | grantType: 'password', 47 | credentialsInBody, 48 | params: [ 49 | { name: 'username', value: username }, 50 | { name: 'password', value: password }, 51 | ], 52 | }); 53 | 54 | return storeToken(ctx, contextId, response); 55 | } 56 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | FormInputSelectOption, 4 | GetHttpAuthenticationConfigRequest, 5 | JsonPrimitive, 6 | PluginDefinition, 7 | } from '@yaakapp/api'; 8 | import { DEFAULT_PKCE_METHOD, getAuthorizationCode, PKCE_PLAIN, PKCE_SHA256 } from './grants/authorizationCode'; 9 | import { getClientCredentials } from './grants/clientCredentials'; 10 | import { getImplicit } from './grants/implicit'; 11 | import { getPassword } from './grants/password'; 12 | import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store'; 13 | 14 | type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; 15 | 16 | const grantTypes: FormInputSelectOption[] = [ 17 | { label: 'Authorization Code', value: 'authorization_code' }, 18 | { label: 'Implicit', value: 'implicit' }, 19 | { label: 'Resource Owner Password Credential', value: 'password' }, 20 | { label: 'Client Credentials', value: 'client_credentials' }, 21 | ]; 22 | 23 | const defaultGrantType = grantTypes[0]!.value; 24 | 25 | function hiddenIfNot(grantTypes: GrantType[], ...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]) { 26 | return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => { 27 | const hasGrantType = grantTypes.find(t => t === String(values.grantType ?? defaultGrantType)); 28 | const hasOtherBools = other.every(t => t(values)); 29 | const show = hasGrantType && hasOtherBools; 30 | return { hidden: !show }; 31 | }; 32 | } 33 | 34 | const authorizationUrls = [ 35 | 'https://github.com/login/oauth/authorize', 36 | 'https://account.box.com/api/oauth2/authorize', 37 | 'https://accounts.google.com/o/oauth2/v2/auth', 38 | 'https://api.imgur.com/oauth2/authorize', 39 | 'https://bitly.com/oauth/authorize', 40 | 'https://gitlab.example.com/oauth/authorize', 41 | 'https://medium.com/m/oauth/authorize', 42 | 'https://public-api.wordpress.com/oauth2/authorize', 43 | 'https://slack.com/oauth/authorize', 44 | 'https://todoist.com/oauth/authorize', 45 | 'https://www.dropbox.com/oauth2/authorize', 46 | 'https://www.linkedin.com/oauth/v2/authorization', 47 | 'https://MY_SHOP.myshopify.com/admin/oauth/access_token', 48 | ]; 49 | 50 | const accessTokenUrls = [ 51 | 'https://github.com/login/oauth/access_token', 52 | 'https://api-ssl.bitly.com/oauth/access_token', 53 | 'https://api.box.com/oauth2/token', 54 | 'https://api.dropboxapi.com/oauth2/token', 55 | 'https://api.imgur.com/oauth2/token', 56 | 'https://api.medium.com/v1/tokens', 57 | 'https://gitlab.example.com/oauth/token', 58 | 'https://public-api.wordpress.com/oauth2/token', 59 | 'https://slack.com/api/oauth.access', 60 | 'https://todoist.com/oauth/access_token', 61 | 'https://www.googleapis.com/oauth2/v4/token', 62 | 'https://www.linkedin.com/oauth/v2/accessToken', 63 | 'https://MY_SHOP.myshopify.com/admin/oauth/authorize', 64 | ]; 65 | 66 | export const plugin: PluginDefinition = { 67 | authentication: { 68 | name: 'oauth2', 69 | label: 'OAuth 2.0', 70 | shortLabel: 'OAuth 2', 71 | actions: [ 72 | { 73 | label: 'Copy Current Token', 74 | async onSelect(ctx, { contextId }) { 75 | const token = await getToken(ctx, contextId); 76 | if (token == null) { 77 | await ctx.toast.show({ message: 'No token to copy', color: 'warning' }); 78 | } else { 79 | await ctx.clipboard.copyText(token.response.access_token); 80 | await ctx.toast.show({ message: 'Token copied to clipboard', icon: 'copy', color: 'success' }); 81 | } 82 | }, 83 | }, 84 | { 85 | label: 'Delete Token', 86 | async onSelect(ctx, { contextId }) { 87 | if (await deleteToken(ctx, contextId)) { 88 | await ctx.toast.show({ message: 'Token deleted', color: 'success' }); 89 | } else { 90 | await ctx.toast.show({ message: 'No token to delete', color: 'warning' }); 91 | } 92 | }, 93 | }, 94 | { 95 | label: 'Clear Window Session', 96 | async onSelect(ctx, { contextId }) { 97 | await resetDataDirKey(ctx, contextId); 98 | }, 99 | }, 100 | ], 101 | args: [ 102 | { 103 | type: 'select', 104 | name: 'grantType', 105 | label: 'Grant Type', 106 | hideLabel: true, 107 | defaultValue: defaultGrantType, 108 | options: grantTypes, 109 | }, 110 | 111 | // Always-present fields 112 | { 113 | type: 'text', 114 | name: 'clientId', 115 | label: 'Client ID', 116 | optional: true, 117 | }, 118 | { 119 | type: 'text', 120 | name: 'clientSecret', 121 | label: 'Client Secret', 122 | optional: true, 123 | password: true, 124 | dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']), 125 | }, 126 | { 127 | type: 'text', 128 | name: 'authorizationUrl', 129 | optional: true, 130 | label: 'Authorization URL', 131 | dynamic: hiddenIfNot(['authorization_code', 'implicit']), 132 | placeholder: authorizationUrls[0], 133 | completionOptions: authorizationUrls.map(url => ({ label: url, value: url })), 134 | }, 135 | { 136 | type: 'text', 137 | name: 'accessTokenUrl', 138 | optional: true, 139 | label: 'Access Token URL', 140 | placeholder: accessTokenUrls[0], 141 | dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']), 142 | completionOptions: accessTokenUrls.map(url => ({ label: url, value: url })), 143 | }, 144 | { 145 | type: 'text', 146 | name: 'redirectUri', 147 | label: 'Redirect URI', 148 | optional: true, 149 | dynamic: hiddenIfNot(['authorization_code', 'implicit']), 150 | }, 151 | { 152 | type: 'text', 153 | name: 'state', 154 | label: 'State', 155 | optional: true, 156 | dynamic: hiddenIfNot(['authorization_code', 'implicit']), 157 | }, 158 | { 159 | type: 'text', 160 | name: 'audience', 161 | label: 'Audience', 162 | optional: true, 163 | }, 164 | { 165 | type: 'checkbox', 166 | name: 'usePkce', 167 | label: 'Use PKCE', 168 | dynamic: hiddenIfNot(['authorization_code']), 169 | }, 170 | { 171 | type: 'select', 172 | name: 'pkceChallengeMethod', 173 | label: 'Code Challenge Method', 174 | options: [{ label: 'SHA-256', value: PKCE_SHA256 }, { label: 'Plain', value: PKCE_PLAIN }], 175 | defaultValue: DEFAULT_PKCE_METHOD, 176 | dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce), 177 | }, 178 | { 179 | type: 'text', 180 | name: 'pkceCodeVerifier', 181 | label: 'Code Verifier', 182 | placeholder: 'Automatically generated if not provided', 183 | optional: true, 184 | dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce), 185 | }, 186 | { 187 | type: 'text', 188 | name: 'username', 189 | label: 'Username', 190 | optional: true, 191 | dynamic: hiddenIfNot(['password']), 192 | }, 193 | { 194 | type: 'text', 195 | name: 'password', 196 | label: 'Password', 197 | password: true, 198 | optional: true, 199 | dynamic: hiddenIfNot(['password']), 200 | }, 201 | { 202 | type: 'select', 203 | name: 'responseType', 204 | label: 'Response Type', 205 | defaultValue: 'token', 206 | options: [ 207 | { label: 'Access Token', value: 'token' }, 208 | { label: 'ID Token', value: 'id_token' }, 209 | { label: 'ID and Access Token', value: 'id_token token' }, 210 | ], 211 | dynamic: hiddenIfNot(['implicit']), 212 | }, 213 | { 214 | type: 'accordion', 215 | label: 'Advanced', 216 | inputs: [ 217 | { type: 'text', name: 'scope', label: 'Scope', optional: true }, 218 | { type: 'text', name: 'headerPrefix', label: 'Header Prefix', optional: true, defaultValue: 'Bearer' }, 219 | { 220 | type: 'select', name: 'credentials', label: 'Send Credentials', defaultValue: 'body', options: [ 221 | { label: 'In Request Body', value: 'body' }, 222 | { label: 'As Basic Authentication', value: 'basic' }, 223 | ], 224 | }, 225 | ], 226 | }, 227 | { 228 | type: 'accordion', 229 | label: 'Access Token Response', 230 | async dynamic(ctx, { contextId }) { 231 | const token = await getToken(ctx, contextId); 232 | if (token == null) { 233 | return { hidden: true }; 234 | } 235 | return { 236 | label: 'Access Token Response', 237 | inputs: [ 238 | { 239 | type: 'editor', 240 | defaultValue: JSON.stringify(token.response, null, 2), 241 | hideLabel: true, 242 | readOnly: true, 243 | language: 'json', 244 | }, 245 | ], 246 | }; 247 | }, 248 | }, 249 | ], 250 | async onApply(ctx, { values, contextId }) { 251 | const headerPrefix = stringArg(values, 'headerPrefix'); 252 | const grantType = stringArg(values, 'grantType') as GrantType; 253 | const credentialsInBody = values.credentials === 'body'; 254 | 255 | let token: AccessToken; 256 | if (grantType === 'authorization_code') { 257 | const authorizationUrl = stringArg(values, 'authorizationUrl'); 258 | const accessTokenUrl = stringArg(values, 'accessTokenUrl'); 259 | token = await getAuthorizationCode(ctx, contextId, { 260 | accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, 261 | authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, 262 | clientId: stringArg(values, 'clientId'), 263 | clientSecret: stringArg(values, 'clientSecret'), 264 | redirectUri: stringArgOrNull(values, 'redirectUri'), 265 | scope: stringArgOrNull(values, 'scope'), 266 | audience: stringArgOrNull(values, 'audience'), 267 | state: stringArgOrNull(values, 'state'), 268 | credentialsInBody, 269 | pkce: values.usePkce ? { 270 | challengeMethod: stringArg(values, 'pkceChallengeMethod'), 271 | codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'), 272 | } : null, 273 | }); 274 | } else if (grantType === 'implicit') { 275 | const authorizationUrl = stringArg(values, 'authorizationUrl'); 276 | token = await getImplicit(ctx, contextId, { 277 | authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, 278 | clientId: stringArg(values, 'clientId'), 279 | redirectUri: stringArgOrNull(values, 'redirectUri'), 280 | responseType: stringArg(values, 'responseType'), 281 | scope: stringArgOrNull(values, 'scope'), 282 | audience: stringArgOrNull(values, 'audience'), 283 | state: stringArgOrNull(values, 'state'), 284 | }); 285 | } else if (grantType === 'client_credentials') { 286 | const accessTokenUrl = stringArg(values, 'accessTokenUrl'); 287 | token = await getClientCredentials(ctx, contextId, { 288 | accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, 289 | clientId: stringArg(values, 'clientId'), 290 | clientSecret: stringArg(values, 'clientSecret'), 291 | scope: stringArgOrNull(values, 'scope'), 292 | audience: stringArgOrNull(values, 'audience'), 293 | credentialsInBody, 294 | }); 295 | } else if (grantType === 'password') { 296 | const accessTokenUrl = stringArg(values, 'accessTokenUrl'); 297 | token = await getPassword(ctx, contextId, { 298 | accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, 299 | clientId: stringArg(values, 'clientId'), 300 | clientSecret: stringArg(values, 'clientSecret'), 301 | username: stringArg(values, 'username'), 302 | password: stringArg(values, 'password'), 303 | scope: stringArgOrNull(values, 'scope'), 304 | audience: stringArgOrNull(values, 'audience'), 305 | credentialsInBody, 306 | }); 307 | } else { 308 | throw new Error('Invalid grant type ' + grantType); 309 | } 310 | 311 | const headerValue = `${headerPrefix} ${token.response.access_token}`.trim(); 312 | return { 313 | setHeaders: [{ 314 | name: 'Authorization', 315 | value: headerValue, 316 | }], 317 | }; 318 | }, 319 | }, 320 | }; 321 | 322 | function stringArgOrNull(values: Record, name: string): string | null { 323 | const arg = values[name]; 324 | if (arg == null || arg == '') return null; 325 | return `${arg}`; 326 | } 327 | 328 | function stringArg(values: Record, name: string): string { 329 | const arg = stringArgOrNull(values, name); 330 | if (!arg) return ''; 331 | return arg; 332 | } 333 | -------------------------------------------------------------------------------- /plugins/auth-oauth2/src/store.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@yaakapp/api'; 2 | 3 | export async function storeToken(ctx: Context, contextId: string, response: AccessTokenRawResponse) { 4 | if (!response.access_token) { 5 | throw new Error(`Token not found in response`); 6 | } 7 | 8 | const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1000 : null; 9 | const token: AccessToken = { 10 | response, 11 | expiresAt, 12 | }; 13 | await ctx.store.set(tokenStoreKey(contextId), token); 14 | return token; 15 | } 16 | 17 | export async function getToken(ctx: Context, contextId: string) { 18 | return ctx.store.get(tokenStoreKey(contextId)); 19 | } 20 | 21 | export async function deleteToken(ctx: Context, contextId: string) { 22 | return ctx.store.delete(tokenStoreKey(contextId)); 23 | } 24 | 25 | export async function resetDataDirKey(ctx: Context, contextId: string) { 26 | const key = new Date().toISOString(); 27 | return ctx.store.set(dataDirStoreKey(contextId), key); 28 | } 29 | 30 | export async function getDataDirKey(ctx: Context, contextId: string) { 31 | const key = (await ctx.store.get(dataDirStoreKey(contextId))) ?? 'default'; 32 | return `${contextId}::${key}`; 33 | } 34 | 35 | function tokenStoreKey(context_id: string) { 36 | return ['token', context_id].join('::'); 37 | } 38 | 39 | function dataDirStoreKey(context_id: string) { 40 | return ['data_dir', context_id].join('::'); 41 | } 42 | 43 | export interface AccessToken { 44 | response: AccessTokenRawResponse, 45 | expiresAt: number | null; 46 | } 47 | 48 | export interface AccessTokenRawResponse { 49 | access_token: string; 50 | token_type?: string; 51 | expires_in?: number; 52 | refresh_token?: string; 53 | error?: string; 54 | error_description?: string; 55 | scope?: string; 56 | } 57 | -------------------------------------------------------------------------------- /plugins/exporter-curl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/exporter-curl", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/exporter-curl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, PluginDefinition } from '@yaakapp/api'; 2 | 3 | const NEWLINE = '\\\n '; 4 | 5 | export const plugin: PluginDefinition = { 6 | httpRequestActions: [{ 7 | label: 'Copy as Curl', 8 | icon: 'copy', 9 | async onSelect(ctx, args) { 10 | const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: 'preview' }); 11 | const data = await convertToCurl(rendered_request); 12 | await ctx.clipboard.copyText(data); 13 | await ctx.toast.show({ message: 'Curl copied to clipboard', icon: 'copy', color: 'success' }); 14 | }, 15 | }], 16 | }; 17 | 18 | export async function convertToCurl(request: Partial) { 19 | const xs = ['curl']; 20 | 21 | // Add method and URL all on first line 22 | if (request.method) xs.push('-X', request.method); 23 | if (request.url) xs.push(quote(request.url)); 24 | 25 | 26 | xs.push(NEWLINE); 27 | 28 | // Add URL params 29 | for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) { 30 | xs.push('--url-query', quote(`${p.name}=${p.value}`)); 31 | xs.push(NEWLINE); 32 | } 33 | 34 | // Add headers 35 | for (const h of (request.headers ?? []).filter(onlyEnabled)) { 36 | xs.push('--header', quote(`${h.name}: ${h.value}`)); 37 | xs.push(NEWLINE); 38 | } 39 | 40 | // Add form params 41 | if (Array.isArray(request.body?.form)) { 42 | const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data'; 43 | for (const p of (request.body?.form ?? []).filter(onlyEnabled)) { 44 | if (p.file) { 45 | let v = `${p.name}=@${p.file}`; 46 | v += p.contentType ? `;type=${p.contentType}` : ''; 47 | xs.push(flag, v); 48 | } else { 49 | xs.push(flag, quote(`${p.name}=${p.value}`)); 50 | } 51 | xs.push(NEWLINE); 52 | } 53 | } else if (typeof request.body?.query === 'string') { 54 | const body = { query: request.body.query || '', variables: maybeParseJSON(request.body.variables, undefined) }; 55 | xs.push('--data-raw', `${quote(JSON.stringify(body))}`); 56 | xs.push(NEWLINE); 57 | } else if (typeof request.body?.text === 'string') { 58 | xs.push('--data-raw', `${quote(request.body.text)}`); 59 | xs.push(NEWLINE); 60 | } 61 | 62 | // Add basic/digest authentication 63 | if (request.authenticationType === 'basic' || request.authenticationType === 'digest') { 64 | if (request.authenticationType === 'digest') xs.push('--digest'); 65 | xs.push( 66 | '--user', 67 | quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`), 68 | ); 69 | xs.push(NEWLINE); 70 | } 71 | 72 | // Add bearer authentication 73 | if (request.authenticationType === 'bearer') { 74 | xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`)); 75 | xs.push(NEWLINE); 76 | } 77 | 78 | // Remove trailing newline 79 | if (xs[xs.length - 1] === NEWLINE) { 80 | xs.splice(xs.length - 1, 1); 81 | } 82 | 83 | return xs.join(' '); 84 | } 85 | 86 | function quote(arg: string): string { 87 | const escaped = arg.replace(/'/g, '\\\''); 88 | return `'${escaped}'`; 89 | } 90 | 91 | function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean { 92 | return v.enabled !== false && !!v.name; 93 | } 94 | 95 | function maybeParseJSON(v: any, fallback: any): string { 96 | try { 97 | return JSON.parse(v); 98 | } catch (err) { 99 | return fallback; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /plugins/exporter-curl/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { convertToCurl } from '../src'; 3 | 4 | describe('exporter-curl', () => { 5 | test('Exports GET with params', async () => { 6 | expect( 7 | await convertToCurl({ 8 | url: 'https://yaak.app', 9 | urlParameters: [ 10 | { name: 'a', value: 'aaa' }, 11 | { name: 'b', value: 'bbb', enabled: true }, 12 | { name: 'c', value: 'ccc', enabled: false }, 13 | ], 14 | }), 15 | ).toEqual( 16 | [`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `), 17 | ); 18 | }); 19 | test('Exports POST with url form data', async () => { 20 | expect( 21 | await convertToCurl({ 22 | url: 'https://yaak.app', 23 | method: 'POST', 24 | bodyType: 'application/x-www-form-urlencoded', 25 | body: { 26 | form: [ 27 | { name: 'a', value: 'aaa' }, 28 | { name: 'b', value: 'bbb', enabled: true }, 29 | { name: 'c', value: 'ccc', enabled: false }, 30 | ], 31 | }, 32 | }), 33 | ).toEqual( 34 | [`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `), 35 | ); 36 | }); 37 | 38 | test('Exports POST with GraphQL data', async () => { 39 | expect( 40 | await convertToCurl({ 41 | url: 'https://yaak.app', 42 | method: 'POST', 43 | bodyType: 'graphql', 44 | body: { 45 | query: '{foo,bar}', 46 | variables: '{"a": "aaa", "b": "bbb"}', 47 | }, 48 | }), 49 | ).toEqual( 50 | [`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`].join(` \\\n `), 51 | ); 52 | }); 53 | 54 | test('Exports POST with GraphQL data no variables', async () => { 55 | expect( 56 | await convertToCurl({ 57 | url: 'https://yaak.app', 58 | method: 'POST', 59 | bodyType: 'graphql', 60 | body: { 61 | query: '{foo,bar}', 62 | }, 63 | }), 64 | ).toEqual( 65 | [`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}"}'`].join(` \\\n `), 66 | ); 67 | }); 68 | 69 | test('Exports PUT with multipart form', async () => { 70 | expect( 71 | await convertToCurl({ 72 | url: 'https://yaak.app', 73 | method: 'PUT', 74 | bodyType: 'multipart/form-data', 75 | body: { 76 | form: [ 77 | { name: 'a', value: 'aaa' }, 78 | { name: 'b', value: 'bbb', enabled: true }, 79 | { name: 'c', value: 'ccc', enabled: false }, 80 | { name: 'f', file: '/foo/bar.png', contentType: 'image/png' }, 81 | ], 82 | }, 83 | }), 84 | ).toEqual( 85 | [ 86 | `curl -X PUT 'https://yaak.app'`, 87 | `--form 'a=aaa'`, 88 | `--form 'b=bbb'`, 89 | `--form f=@/foo/bar.png;type=image/png`, 90 | ].join(` \\\n `), 91 | ); 92 | }); 93 | 94 | test('Exports JSON body', async () => { 95 | expect( 96 | await convertToCurl({ 97 | url: 'https://yaak.app', 98 | method: 'POST', 99 | bodyType: 'application/json', 100 | body: { 101 | text: `{"foo":"bar's"}`, 102 | }, 103 | headers: [{ name: 'Content-Type', value: 'application/json' }], 104 | }), 105 | ).toEqual( 106 | [ 107 | `curl -X POST 'https://yaak.app'`, 108 | `--header 'Content-Type: application/json'`, 109 | `--data-raw '{"foo":"bar\\'s"}'`, 110 | ].join(` \\\n `), 111 | ); 112 | }); 113 | 114 | test('Exports multi-line JSON body', async () => { 115 | expect( 116 | await convertToCurl({ 117 | url: 'https://yaak.app', 118 | method: 'POST', 119 | bodyType: 'application/json', 120 | body: { 121 | text: `{"foo":"bar",\n"baz":"qux"}`, 122 | }, 123 | headers: [{ name: 'Content-Type', value: 'application/json' }], 124 | }), 125 | ).toEqual( 126 | [ 127 | `curl -X POST 'https://yaak.app'`, 128 | `--header 'Content-Type: application/json'`, 129 | `--data-raw '{"foo":"bar",\n"baz":"qux"}'`, 130 | ].join(` \\\n `), 131 | ); 132 | }); 133 | 134 | test('Exports headers', async () => { 135 | expect( 136 | await convertToCurl({ 137 | headers: [ 138 | { name: 'a', value: 'aaa' }, 139 | { name: 'b', value: 'bbb', enabled: true }, 140 | { name: 'c', value: 'ccc', enabled: false }, 141 | ], 142 | }), 143 | ).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `)); 144 | }); 145 | 146 | test('Basic auth', async () => { 147 | expect( 148 | await convertToCurl({ 149 | url: 'https://yaak.app', 150 | authenticationType: 'basic', 151 | authentication: { 152 | username: 'user', 153 | password: 'pass', 154 | }, 155 | }), 156 | ).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `)); 157 | }); 158 | 159 | test('Broken basic auth', async () => { 160 | expect( 161 | await convertToCurl({ 162 | url: 'https://yaak.app', 163 | authenticationType: 'basic', 164 | authentication: {}, 165 | }), 166 | ).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `)); 167 | }); 168 | 169 | test('Digest auth', async () => { 170 | expect( 171 | await convertToCurl({ 172 | url: 'https://yaak.app', 173 | authenticationType: 'digest', 174 | authentication: { 175 | username: 'user', 176 | password: 'pass', 177 | }, 178 | }), 179 | ).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `)); 180 | }); 181 | 182 | test('Bearer auth', async () => { 183 | expect( 184 | await convertToCurl({ 185 | url: 'https://yaak.app', 186 | authenticationType: 'bearer', 187 | authentication: { 188 | token: 'tok', 189 | }, 190 | }), 191 | ).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `)); 192 | }); 193 | 194 | test('Broken bearer auth', async () => { 195 | expect( 196 | await convertToCurl({ 197 | url: 'https://yaak.app', 198 | authenticationType: 'bearer', 199 | authentication: { 200 | username: 'user', 201 | password: 'pass', 202 | }, 203 | }), 204 | ).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `)); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /plugins/filter-jsonpath/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/filter-jsonpath", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "jsonpath-plus": "^10.3.0" 11 | }, 12 | "devDependencies": { 13 | "@types/jsonpath": "^0.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/filter-jsonpath/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginDefinition } from '@yaakapp/api'; 2 | import { JSONPath } from 'jsonpath-plus'; 3 | 4 | export const plugin: PluginDefinition = { 5 | filter: { 6 | name: 'JSONPath', 7 | description: 'Filter JSONPath', 8 | onFilter(_ctx, args) { 9 | const parsed = JSON.parse(args.payload); 10 | const filtered = JSONPath({ path: args.filter, json: parsed }); 11 | return { filtered: JSON.stringify(filtered, null, 2) }; 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /plugins/filter-xpath/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/filter-xpath", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "@xmldom/xmldom": "^0.8.10", 11 | "xpath": "^0.0.34" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/filter-xpath/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom'; 2 | import { PluginDefinition } from '@yaakapp/api'; 3 | import xpath from 'xpath'; 4 | 5 | export const plugin: PluginDefinition = { 6 | filter: { 7 | name: 'XPath', 8 | description: 'Filter XPath', 9 | onFilter(_ctx, args) { 10 | const doc = new DOMParser().parseFromString(args.payload, 'text/xml'); 11 | const result = xpath.select(args.filter, doc, false); 12 | 13 | if (Array.isArray(result)) { 14 | return { filtered: result.map(r => String(r)).join('\n') }; 15 | } else { 16 | // Not sure what cases this happens in (?) 17 | return { filtered: String(result) }; 18 | } 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /plugins/importer-curl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/importer-curl", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "shell-quote": "^1.8.1" 11 | }, 12 | "devDependencies": { 13 | "@types/shell-quote": "^1.7.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/importer-curl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api'; 2 | import { ControlOperator, parse, ParseEntry } from 'shell-quote'; 3 | 4 | type AtLeast = Partial & Pick; 5 | 6 | interface ExportResources { 7 | workspaces: AtLeast[]; 8 | environments: AtLeast[]; 9 | httpRequests: AtLeast[]; 10 | folders: AtLeast[]; 11 | } 12 | 13 | const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii']; 14 | const SUPPORTED_FLAGS = [ 15 | ['cookie', 'b'], 16 | ['d', 'data'], // Add url encoded data 17 | ['data-ascii'], 18 | ['data-binary'], 19 | ['data-raw'], 20 | ['data-urlencode'], 21 | ['digest'], // Apply auth as digest 22 | ['form', 'F'], // Add multipart data 23 | ['get', 'G'], // Put the post data in the URL 24 | ['header', 'H'], 25 | ['request', 'X'], // Request method 26 | ['url'], // Specify the URL explicitly 27 | ['url-query'], 28 | ['user', 'u'], // Authentication 29 | DATA_FLAGS, 30 | ].flatMap((v) => v); 31 | 32 | const BOOLEAN_FLAGS = ['G', 'get', 'digest']; 33 | 34 | type FlagValue = string | boolean; 35 | 36 | type FlagsByName = Record; 37 | 38 | export const plugin: PluginDefinition = { 39 | importer: { 40 | name: 'cURL', 41 | description: 'Import cURL commands', 42 | onImport(_ctx: Context, args: { text: string }) { 43 | return convertCurl(args.text) as any; 44 | }, 45 | }, 46 | }; 47 | 48 | export function convertCurl(rawData: string) { 49 | if (!rawData.match(/^\s*curl /)) { 50 | return null; 51 | } 52 | 53 | const commands: ParseEntry[][] = []; 54 | 55 | // Replace non-escaped newlines with semicolons to make parsing easier 56 | // NOTE: This is really slow in debug build but fast in release mode 57 | const normalizedData = rawData.replace(/\ncurl/g, '; curl'); 58 | 59 | let currentCommand: ParseEntry[] = []; 60 | 61 | const parsed = parse(normalizedData); 62 | 63 | // Break up `-XPOST` into `-X POST` 64 | const normalizedParseEntries = parsed.flatMap((entry) => { 65 | if ( 66 | typeof entry === 'string' && 67 | entry.startsWith('-') && 68 | !entry.startsWith('--') && 69 | entry.length > 2 70 | ) { 71 | return [entry.slice(0, 2), entry.slice(2)]; 72 | } 73 | return entry; 74 | }); 75 | 76 | for (const parseEntry of normalizedParseEntries) { 77 | if (typeof parseEntry === 'string') { 78 | if (parseEntry.startsWith('$')) { 79 | currentCommand.push(parseEntry.slice(1)); 80 | } else { 81 | currentCommand.push(parseEntry); 82 | } 83 | continue; 84 | } 85 | 86 | if ('comment' in parseEntry) { 87 | continue; 88 | } 89 | 90 | const { op } = parseEntry as { op: 'glob'; pattern: string } | { op: ControlOperator }; 91 | 92 | // `;` separates commands 93 | if (op === ';') { 94 | commands.push(currentCommand); 95 | currentCommand = []; 96 | continue; 97 | } 98 | 99 | if (op?.startsWith('$')) { 100 | // Handle the case where literal like -H $'Header: \'Some Quoted Thing\'' 101 | const str = op.slice(2, op.length - 1).replace(/\\'/g, '\''); 102 | 103 | currentCommand.push(str); 104 | continue; 105 | } 106 | 107 | if (op === 'glob') { 108 | currentCommand.push((parseEntry as { op: 'glob'; pattern: string }).pattern); 109 | } 110 | } 111 | 112 | commands.push(currentCommand); 113 | 114 | const workspace: ExportResources['workspaces'][0] = { 115 | model: 'workspace', 116 | id: generateId('workspace'), 117 | name: 'Curl Import', 118 | }; 119 | 120 | const requests: ExportResources['httpRequests'] = commands 121 | .filter((command) => command[0] === 'curl') 122 | .map((v) => importCommand(v, workspace.id)); 123 | 124 | return { 125 | resources: { 126 | httpRequests: requests, 127 | workspaces: [workspace], 128 | }, 129 | }; 130 | } 131 | 132 | function importCommand(parseEntries: ParseEntry[], workspaceId: string) { 133 | // ~~~~~~~~~~~~~~~~~~~~~ // 134 | // Collect all the flags // 135 | // ~~~~~~~~~~~~~~~~~~~~~ // 136 | const flagsByName: FlagsByName = {}; 137 | const singletons: ParseEntry[] = []; 138 | 139 | // Start at 1 so we can skip the ^curl part 140 | for (let i = 1; i < parseEntries.length; i++) { 141 | let parseEntry = parseEntries[i]; 142 | if (typeof parseEntry === 'string') { 143 | parseEntry = parseEntry.trim(); 144 | } 145 | 146 | if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) { 147 | const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-'; 148 | let name = parseEntry.replace(/^-{1,2}/, ''); 149 | 150 | if (!SUPPORTED_FLAGS.includes(name)) { 151 | continue; 152 | } 153 | 154 | let value; 155 | const nextEntry = parseEntries[i + 1]; 156 | const hasValue = !BOOLEAN_FLAGS.includes(name); 157 | if (isSingleDash && name.length > 1) { 158 | // Handle squished arguments like -XPOST 159 | value = name.slice(1); 160 | name = name.slice(0, 1); 161 | } else if (typeof nextEntry === 'string' && hasValue && !nextEntry.startsWith('-')) { 162 | // Next arg is not a flag, so assign it as the value 163 | value = nextEntry; 164 | i++; // Skip next one 165 | } else { 166 | value = true; 167 | } 168 | 169 | flagsByName[name] = flagsByName[name] || []; 170 | flagsByName[name]!.push(value); 171 | } else if (parseEntry) { 172 | singletons.push(parseEntry); 173 | } 174 | } 175 | 176 | // ~~~~~~~~~~~~~~~~~ // 177 | // Build the request // 178 | // ~~~~~~~~~~~~~~~~~ // 179 | 180 | // Url and Parameters 181 | let urlParameters: HttpUrlParameter[]; 182 | let url: string; 183 | 184 | const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']); 185 | const [baseUrl, search] = splitOnce(urlArg, '?'); 186 | urlParameters = 187 | search?.split('&').map((p) => { 188 | const v = splitOnce(p, '='); 189 | return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true }; 190 | }) ?? []; 191 | 192 | url = baseUrl ?? urlArg; 193 | 194 | // Query params 195 | for (const p of flagsByName['url-query'] ?? []) { 196 | if (typeof p !== 'string') { 197 | continue; 198 | } 199 | const [name, value] = p.split('='); 200 | urlParameters.push({ 201 | name: name ?? '', 202 | value: value ?? '', 203 | enabled: true, 204 | }); 205 | } 206 | 207 | // Authentication 208 | const [username, password] = getPairValue(flagsByName, '', ['u', 'user']).split(/:(.*)$/); 209 | 210 | const isDigest = getPairValue(flagsByName, false, ['digest']); 211 | const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null; 212 | const authentication = username 213 | ? { 214 | username: username.trim(), 215 | password: (password ?? '').trim(), 216 | } 217 | : {}; 218 | 219 | // Headers 220 | const headers = [ 221 | ...((flagsByName['header'] as string[] | undefined) || []), 222 | ...((flagsByName['H'] as string[] | undefined) || []), 223 | ].map((header) => { 224 | const [name, value] = header.split(/:(.*)$/); 225 | // remove final colon from header name if present 226 | if (!value) { 227 | return { 228 | name: (name ?? '').trim().replace(/;$/, ''), 229 | value: '', 230 | enabled: true, 231 | }; 232 | } 233 | return { 234 | name: (name ?? '').trim(), 235 | value: value.trim(), 236 | enabled: true, 237 | }; 238 | }); 239 | 240 | // Cookies 241 | const cookieHeaderValue = [ 242 | ...((flagsByName['cookie'] as string[] | undefined) || []), 243 | ...((flagsByName['b'] as string[] | undefined) || []), 244 | ] 245 | .map((str) => { 246 | const name = str.split('=', 1)[0]; 247 | const value = str.replace(`${name}=`, ''); 248 | return `${name}=${value}`; 249 | }) 250 | .join('; '); 251 | 252 | // Convert cookie value to header 253 | const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie'); 254 | 255 | if (cookieHeaderValue && existingCookieHeader) { 256 | // Has existing cookie header, so let's update it 257 | existingCookieHeader.value += `; ${cookieHeaderValue}`; 258 | } else if (cookieHeaderValue) { 259 | // No existing cookie header, so let's make a new one 260 | headers.push({ 261 | name: 'Cookie', 262 | value: cookieHeaderValue, 263 | enabled: true, 264 | }); 265 | } 266 | 267 | // Body (Text or Blob) 268 | const dataParameters = pairsToDataParameters(flagsByName); 269 | const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type'); 270 | const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0] : null; 271 | 272 | // Body (Multipart Form Data) 273 | const formDataParams = [ 274 | ...((flagsByName['form'] as string[] | undefined) || []), 275 | ...((flagsByName['F'] as string[] | undefined) || []), 276 | ].map((str) => { 277 | const parts = str.split('='); 278 | const name = parts[0] ?? ''; 279 | const value = parts[1] ?? ''; 280 | const item: { name: string; value?: string; file?: string; enabled: boolean } = { 281 | name, 282 | enabled: true, 283 | }; 284 | 285 | if (value.indexOf('@') === 0) { 286 | item['file'] = value.slice(1); 287 | } else { 288 | item['value'] = value; 289 | } 290 | 291 | return item; 292 | }); 293 | 294 | // Body 295 | let body = {}; 296 | let bodyType: string | null = null; 297 | const bodyAsGET = getPairValue(flagsByName, false, ['G', 'get']); 298 | 299 | if (dataParameters.length > 0 && bodyAsGET) { 300 | urlParameters.push(...dataParameters); 301 | } else if ( 302 | dataParameters.length > 0 && 303 | (mimeType == null || mimeType === 'application/x-www-form-urlencoded') 304 | ) { 305 | bodyType = mimeType ?? 'application/x-www-form-urlencoded'; 306 | body = { 307 | form: dataParameters.map((parameter) => ({ 308 | ...parameter, 309 | name: decodeURIComponent(parameter.name || ''), 310 | value: decodeURIComponent(parameter.value || ''), 311 | })), 312 | }; 313 | headers.push({ 314 | name: 'Content-Type', 315 | value: 'application/x-www-form-urlencoded', 316 | enabled: true, 317 | }); 318 | } else if (dataParameters.length > 0) { 319 | bodyType = 320 | mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain' 321 | ? mimeType 322 | : 'other'; 323 | body = { 324 | text: dataParameters 325 | .map(({ name, value }) => (name && value ? `${name}=${value}` : name || value)) 326 | .join('&'), 327 | }; 328 | } else if (formDataParams.length) { 329 | bodyType = mimeType ?? 'multipart/form-data'; 330 | body = { 331 | form: formDataParams, 332 | }; 333 | if (mimeType == null) { 334 | headers.push({ 335 | name: 'Content-Type', 336 | value: 'multipart/form-data', 337 | enabled: true, 338 | }); 339 | } 340 | } 341 | 342 | // Method 343 | let method = getPairValue(flagsByName, '', ['X', 'request']).toUpperCase(); 344 | 345 | if (method === '' && body) { 346 | method = 'text' in body || 'form' in body ? 'POST' : 'GET'; 347 | } 348 | 349 | const request: ExportResources['httpRequests'][0] = { 350 | id: generateId('http_request'), 351 | model: 'http_request', 352 | workspaceId, 353 | name: '', 354 | urlParameters, 355 | url, 356 | method, 357 | headers, 358 | authentication, 359 | authenticationType, 360 | body, 361 | bodyType, 362 | folderId: null, 363 | sortPriority: 0, 364 | }; 365 | 366 | return request; 367 | } 368 | 369 | interface DataParameter { 370 | name: string; 371 | value: string; 372 | contentType?: string; 373 | filePath?: string; 374 | enabled?: boolean; 375 | } 376 | 377 | function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] { 378 | let dataParameters: DataParameter[] = []; 379 | 380 | for (const flagName of DATA_FLAGS) { 381 | const pairs = keyedPairs[flagName]; 382 | 383 | if (!pairs || pairs.length === 0) { 384 | continue; 385 | } 386 | 387 | for (const p of pairs) { 388 | if (typeof p !== 'string') continue; 389 | let params = p.split("&"); 390 | for (const param of params) { 391 | const [name, value] = param.split('='); 392 | if (param.startsWith('@')) { 393 | // Yaak doesn't support files in url-encoded data, so 394 | dataParameters.push({ 395 | name: name ?? '', 396 | value: '', 397 | filePath: param.slice(1), 398 | enabled: true, 399 | }); 400 | } else { 401 | dataParameters.push({ 402 | name: name ?? '', 403 | value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '', 404 | enabled: true, 405 | }); 406 | } 407 | } 408 | } 409 | } 410 | 411 | return dataParameters; 412 | } 413 | 414 | const getPairValue = ( 415 | pairsByName: FlagsByName, 416 | defaultValue: T, 417 | names: string[], 418 | ) => { 419 | for (const name of names) { 420 | if (pairsByName[name] && pairsByName[name]!.length) { 421 | return pairsByName[name]![0] as T; 422 | } 423 | } 424 | 425 | return defaultValue; 426 | }; 427 | 428 | function splitOnce(str: string, sep: string): string[] { 429 | const index = str.indexOf(sep); 430 | if (index > -1) { 431 | return [str.slice(0, index), str.slice(index + 1)]; 432 | } 433 | return [str]; 434 | } 435 | 436 | const idCount: Partial> = {}; 437 | 438 | function generateId(model: string): string { 439 | idCount[model] = (idCount[model] ?? -1) + 1; 440 | return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`; 441 | } 442 | -------------------------------------------------------------------------------- /plugins/importer-curl/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, Workspace } from '@yaakapp/api'; 2 | import { describe, expect, test } from 'vitest'; 3 | import { convertCurl } from '../src'; 4 | 5 | describe('importer-curl', () => { 6 | test('Imports basic GET', () => { 7 | expect(convertCurl('curl https://yaak.app')).toEqual({ 8 | resources: { 9 | workspaces: [baseWorkspace()], 10 | httpRequests: [ 11 | baseRequest({ 12 | url: 'https://yaak.app', 13 | }), 14 | ], 15 | }, 16 | }); 17 | }); 18 | 19 | test('Explicit URL', () => { 20 | expect(convertCurl('curl --url https://yaak.app')).toEqual({ 21 | resources: { 22 | workspaces: [baseWorkspace()], 23 | httpRequests: [ 24 | baseRequest({ 25 | url: 'https://yaak.app', 26 | }), 27 | ], 28 | }, 29 | }); 30 | }); 31 | 32 | test('Missing URL', () => { 33 | expect(convertCurl('curl -X POST')).toEqual({ 34 | resources: { 35 | workspaces: [baseWorkspace()], 36 | httpRequests: [ 37 | baseRequest({ 38 | method: 'POST', 39 | }), 40 | ], 41 | }, 42 | }); 43 | }); 44 | 45 | test('URL between', () => { 46 | expect(convertCurl('curl -v https://yaak.app -X POST')).toEqual({ 47 | resources: { 48 | workspaces: [baseWorkspace()], 49 | httpRequests: [ 50 | baseRequest({ 51 | url: 'https://yaak.app', 52 | method: 'POST', 53 | }), 54 | ], 55 | }, 56 | }); 57 | }); 58 | 59 | test('Random flags', () => { 60 | expect(convertCurl('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({ 61 | resources: { 62 | workspaces: [baseWorkspace()], 63 | httpRequests: [ 64 | baseRequest({ 65 | url: 'https://yaak.app', 66 | }), 67 | ], 68 | }, 69 | }); 70 | }); 71 | 72 | test('Imports --request method', () => { 73 | expect(convertCurl('curl --request POST https://yaak.app')).toEqual({ 74 | resources: { 75 | workspaces: [baseWorkspace()], 76 | httpRequests: [ 77 | baseRequest({ 78 | url: 'https://yaak.app', 79 | method: 'POST', 80 | }), 81 | ], 82 | }, 83 | }); 84 | }); 85 | 86 | test('Imports -XPOST method', () => { 87 | expect(convertCurl('curl -XPOST --request POST https://yaak.app')).toEqual({ 88 | resources: { 89 | workspaces: [baseWorkspace()], 90 | httpRequests: [ 91 | baseRequest({ 92 | url: 'https://yaak.app', 93 | method: 'POST', 94 | }), 95 | ], 96 | }, 97 | }); 98 | }); 99 | 100 | test('Imports multiple requests', () => { 101 | expect( 102 | convertCurl('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'), 103 | ).toEqual({ 104 | resources: { 105 | workspaces: [baseWorkspace()], 106 | httpRequests: [ 107 | baseRequest({ url: 'https://yaak.app' }), 108 | baseRequest({ url: 'example.com' }), 109 | baseRequest({ url: 'foo.com' }), 110 | ], 111 | }, 112 | }); 113 | }); 114 | 115 | test('Imports form data', () => { 116 | expect( 117 | convertCurl('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'), 118 | ).toEqual({ 119 | resources: { 120 | workspaces: [baseWorkspace()], 121 | httpRequests: [ 122 | baseRequest({ 123 | method: 'POST', 124 | url: 'https://yaak.app', 125 | headers: [ 126 | { 127 | name: 'Content-Type', 128 | value: 'multipart/form-data', 129 | enabled: true, 130 | }, 131 | ], 132 | bodyType: 'multipart/form-data', 133 | body: { 134 | form: [ 135 | { enabled: true, name: 'a', value: 'aaa' }, 136 | { enabled: true, name: 'b', value: 'bbb' }, 137 | { enabled: true, name: 'f', file: 'filepath' }, 138 | ], 139 | }, 140 | }), 141 | ], 142 | }, 143 | }); 144 | }); 145 | 146 | test('Imports data params as form url-encoded', () => { 147 | expect(convertCurl('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({ 148 | resources: { 149 | workspaces: [baseWorkspace()], 150 | httpRequests: [ 151 | baseRequest({ 152 | method: 'POST', 153 | url: 'https://yaak.app', 154 | bodyType: 'application/x-www-form-urlencoded', 155 | headers: [ 156 | { 157 | name: 'Content-Type', 158 | value: 'application/x-www-form-urlencoded', 159 | enabled: true, 160 | }, 161 | ], 162 | body: { 163 | form: [ 164 | { name: 'a', value: '', enabled: true }, 165 | { name: 'b', value: '', enabled: true }, 166 | { name: 'c', value: 'ccc', enabled: true }, 167 | ], 168 | }, 169 | }), 170 | ], 171 | }, 172 | }); 173 | }); 174 | 175 | test('Imports combined data params as form url-encoded', () => { 176 | expect(convertCurl(`curl -d 'a=aaa&b=bbb&c' https://yaak.app`)).toEqual({ 177 | resources: { 178 | workspaces: [baseWorkspace()], 179 | httpRequests: [ 180 | baseRequest({ 181 | method: 'POST', 182 | url: 'https://yaak.app', 183 | bodyType: 'application/x-www-form-urlencoded', 184 | headers: [ 185 | { 186 | name: 'Content-Type', 187 | value: 'application/x-www-form-urlencoded', 188 | enabled: true, 189 | }, 190 | ], 191 | body: { 192 | form: [ 193 | { name: 'a', value: 'aaa', enabled: true }, 194 | { name: 'b', value: 'bbb', enabled: true }, 195 | { name: 'c', value: '', enabled: true }, 196 | ], 197 | }, 198 | }), 199 | ], 200 | }, 201 | }); 202 | }); 203 | 204 | test('Imports data params as text', () => { 205 | expect( 206 | convertCurl('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'), 207 | ).toEqual({ 208 | resources: { 209 | workspaces: [baseWorkspace()], 210 | httpRequests: [ 211 | baseRequest({ 212 | method: 'POST', 213 | url: 'https://yaak.app', 214 | headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }], 215 | bodyType: 'text/plain', 216 | body: { text: 'a&b&c=ccc' }, 217 | }), 218 | ], 219 | }, 220 | }); 221 | }); 222 | 223 | test('Imports post data into URL', () => { 224 | expect( 225 | convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3'), 226 | ).toEqual({ 227 | resources: { 228 | workspaces: [baseWorkspace()], 229 | httpRequests: [ 230 | baseRequest({ 231 | method: 'GET', 232 | url: 'https://api.stripe.com/v1/payment_links', 233 | urlParameters: [{ 234 | enabled: true, 235 | name: 'limit', 236 | value: '3', 237 | }], 238 | }), 239 | ], 240 | }, 241 | }); 242 | }); 243 | 244 | test('Imports multi-line JSON', () => { 245 | expect( 246 | convertCurl(`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`), 247 | ).toEqual({ 248 | resources: { 249 | workspaces: [baseWorkspace()], 250 | httpRequests: [ 251 | baseRequest({ 252 | method: 'POST', 253 | url: 'https://yaak.app', 254 | headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }], 255 | bodyType: 'application/json', 256 | body: { text: '{\n "foo":"bar"\n}' }, 257 | }), 258 | ], 259 | }, 260 | }); 261 | }); 262 | 263 | test('Imports multiple headers', () => { 264 | expect( 265 | convertCurl('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'), 266 | ).toEqual({ 267 | resources: { 268 | workspaces: [baseWorkspace()], 269 | httpRequests: [ 270 | baseRequest({ 271 | url: 'https://yaak.app', 272 | headers: [ 273 | { name: 'Name', value: '', enabled: true }, 274 | { name: 'Foo', value: 'bar', enabled: true }, 275 | { name: 'AAA', value: 'bbb', enabled: true }, 276 | { name: '', value: 'ccc', enabled: true }, 277 | ], 278 | }), 279 | ], 280 | }, 281 | }); 282 | }); 283 | 284 | test('Imports basic auth', () => { 285 | expect(convertCurl('curl --user user:pass https://yaak.app')).toEqual({ 286 | resources: { 287 | workspaces: [baseWorkspace()], 288 | httpRequests: [ 289 | baseRequest({ 290 | url: 'https://yaak.app', 291 | authenticationType: 'basic', 292 | authentication: { 293 | username: 'user', 294 | password: 'pass', 295 | }, 296 | }), 297 | ], 298 | }, 299 | }); 300 | }); 301 | 302 | test('Imports digest auth', () => { 303 | expect(convertCurl('curl --digest --user user:pass https://yaak.app')).toEqual({ 304 | resources: { 305 | workspaces: [baseWorkspace()], 306 | httpRequests: [ 307 | baseRequest({ 308 | url: 'https://yaak.app', 309 | authenticationType: 'digest', 310 | authentication: { 311 | username: 'user', 312 | password: 'pass', 313 | }, 314 | }), 315 | ], 316 | }, 317 | }); 318 | }); 319 | 320 | test('Imports cookie as header', () => { 321 | expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({ 322 | resources: { 323 | workspaces: [baseWorkspace()], 324 | httpRequests: [ 325 | baseRequest({ 326 | url: 'https://yaak.app', 327 | headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }], 328 | }), 329 | ], 330 | }, 331 | }); 332 | }); 333 | 334 | test('Imports query params', () => { 335 | expect(convertCurl('curl "https://yaak.app" --url-query foo=bar --url-query baz=qux')).toEqual({ 336 | resources: { 337 | workspaces: [baseWorkspace()], 338 | httpRequests: [ 339 | baseRequest({ 340 | url: 'https://yaak.app', 341 | urlParameters: [ 342 | { name: 'foo', value: 'bar', enabled: true }, 343 | { name: 'baz', value: 'qux', enabled: true }, 344 | ], 345 | }), 346 | ], 347 | }, 348 | }); 349 | }); 350 | 351 | test('Imports query params from the URL', () => { 352 | expect(convertCurl('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({ 353 | resources: { 354 | workspaces: [baseWorkspace()], 355 | httpRequests: [ 356 | baseRequest({ 357 | url: 'https://yaak.app', 358 | urlParameters: [ 359 | { name: 'foo', value: 'bar', enabled: true }, 360 | { name: 'baz', value: 'a a', enabled: true }, 361 | ], 362 | }), 363 | ], 364 | }, 365 | }); 366 | }); 367 | }); 368 | 369 | const idCount: Partial> = {}; 370 | 371 | function baseRequest(mergeWith: Partial) { 372 | idCount.http_request = (idCount.http_request ?? -1) + 1; 373 | return { 374 | id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`, 375 | model: 'http_request', 376 | authentication: {}, 377 | authenticationType: null, 378 | body: {}, 379 | bodyType: null, 380 | folderId: null, 381 | headers: [], 382 | method: 'GET', 383 | name: '', 384 | sortPriority: 0, 385 | url: '', 386 | urlParameters: [], 387 | workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`, 388 | ...mergeWith, 389 | }; 390 | } 391 | 392 | function baseWorkspace(mergeWith: Partial = {}) { 393 | idCount.workspace = (idCount.workspace ?? -1) + 1; 394 | return { 395 | id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`, 396 | model: 'workspace', 397 | name: 'Curl Import', 398 | ...mergeWith, 399 | }; 400 | } 401 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/importer-insomnia", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "yaml": "^2.4.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/src/common.ts: -------------------------------------------------------------------------------- 1 | 2 | export function convertSyntax(variable: string): string { 3 | if (!isJSString(variable)) return variable; 4 | return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}'); 5 | } 6 | 7 | export function isJSObject(obj: any) { 8 | return Object.prototype.toString.call(obj) === '[object Object]'; 9 | } 10 | 11 | export function isJSString(obj: any) { 12 | return Object.prototype.toString.call(obj) === '[object String]'; 13 | } 14 | 15 | export function convertId(id: string): string { 16 | if (id.startsWith('GENERATE_ID::')) { 17 | return id; 18 | } 19 | return `GENERATE_ID::${id}`; 20 | } 21 | 22 | export function deleteUndefinedAttrs(obj: T): T { 23 | if (Array.isArray(obj) && obj != null) { 24 | return obj.map(deleteUndefinedAttrs) as T; 25 | } else if (typeof obj === 'object' && obj != null) { 26 | return Object.fromEntries( 27 | Object.entries(obj) 28 | .filter(([, v]) => v !== undefined) 29 | .map(([k, v]) => [k, deleteUndefinedAttrs(v)]), 30 | ) as T; 31 | } else { 32 | return obj; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, PluginDefinition } from '@yaakapp/api'; 2 | import YAML from 'yaml'; 3 | import { deleteUndefinedAttrs, isJSObject } from './common'; 4 | import { convertInsomniaV4 } from './v4'; 5 | import { convertInsomniaV5 } from './v5'; 6 | 7 | export const plugin: PluginDefinition = { 8 | importer: { 9 | name: 'Insomnia', 10 | description: 'Import Insomnia workspaces', 11 | async onImport(_ctx: Context, args: { text: string }) { 12 | return convertInsomnia(args.text); 13 | }, 14 | }, 15 | }; 16 | 17 | export function convertInsomnia(contents: string) { 18 | let parsed: any; 19 | 20 | try { 21 | parsed = JSON.parse(contents); 22 | } catch (e) { 23 | } 24 | 25 | try { 26 | parsed = parsed ?? YAML.parse(contents); 27 | } catch (e) { 28 | } 29 | 30 | if (!isJSObject(parsed)) return null; 31 | 32 | const result = convertInsomniaV5(parsed) ?? convertInsomniaV4(parsed); 33 | 34 | return deleteUndefinedAttrs(result); 35 | } 36 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/src/v4.ts: -------------------------------------------------------------------------------- 1 | import { PartialImportResources } from '@yaakapp/api'; 2 | import { convertId, convertSyntax, isJSObject } from './common'; 3 | 4 | export function convertInsomniaV4(parsed: Record) { 5 | if (!Array.isArray(parsed.resources)) return null; 6 | 7 | const resources: PartialImportResources = { 8 | environments: [], 9 | folders: [], 10 | grpcRequests: [], 11 | httpRequests: [], 12 | websocketRequests: [], 13 | workspaces: [], 14 | }; 15 | 16 | // Import workspaces 17 | const workspacesToImport = parsed.resources.filter(r => isJSObject(r) && r._type === 'workspace'); 18 | for (const w of workspacesToImport) { 19 | resources.workspaces.push({ 20 | id: convertId(w._id), 21 | createdAt: w.created ? new Date(w.created).toISOString().replace('Z', '') : undefined, 22 | updatedAt: w.updated ? new Date(w.updated).toISOString().replace('Z', '') : undefined, 23 | model: 'workspace', 24 | name: w.name, 25 | description: w.description || undefined, 26 | }); 27 | const environmentsToImport = parsed.resources.filter( 28 | (r: any) => isJSObject(r) && r._type === 'environment', 29 | ); 30 | resources.environments.push( 31 | ...environmentsToImport.map((r: any) => importEnvironment(r, w._id)), 32 | ); 33 | 34 | const nextFolder = (parentId: string) => { 35 | const children = parsed.resources.filter((r: any) => r.parentId === parentId); 36 | for (const child of children) { 37 | if (!isJSObject(child)) continue; 38 | 39 | if (child._type === 'request_group') { 40 | resources.folders.push(importFolder(child, w._id)); 41 | nextFolder(child._id); 42 | } else if (child._type === 'request') { 43 | resources.httpRequests.push( 44 | importHttpRequest(child, w._id), 45 | ); 46 | } else if (child._type === 'grpc_request') { 47 | resources.grpcRequests.push( 48 | importGrpcRequest(child, w._id), 49 | ); 50 | } 51 | } 52 | }; 53 | 54 | // Import folders 55 | nextFolder(w._id); 56 | } 57 | 58 | // Filter out any `null` values 59 | resources.httpRequests = resources.httpRequests.filter(Boolean); 60 | resources.grpcRequests = resources.grpcRequests.filter(Boolean); 61 | resources.environments = resources.environments.filter(Boolean); 62 | resources.workspaces = resources.workspaces.filter(Boolean); 63 | 64 | return { resources }; 65 | } 66 | 67 | function importHttpRequest( 68 | r: any, 69 | workspaceId: string, 70 | ): PartialImportResources['httpRequests'][0] { 71 | let bodyType: string | null = null; 72 | let body = {}; 73 | if (r.body.mimeType === 'application/octet-stream') { 74 | bodyType = 'binary'; 75 | body = { filePath: r.body.fileName ?? '' }; 76 | } else if (r.body?.mimeType === 'application/x-www-form-urlencoded') { 77 | bodyType = 'application/x-www-form-urlencoded'; 78 | body = { 79 | form: (r.body.params ?? []).map((p: any) => ({ 80 | enabled: !p.disabled, 81 | name: p.name ?? '', 82 | value: p.value ?? '', 83 | })), 84 | }; 85 | } else if (r.body?.mimeType === 'multipart/form-data') { 86 | bodyType = 'multipart/form-data'; 87 | body = { 88 | form: (r.body.params ?? []).map((p: any) => ({ 89 | enabled: !p.disabled, 90 | name: p.name ?? '', 91 | value: p.value ?? '', 92 | file: p.fileName ?? null, 93 | })), 94 | }; 95 | } else if (r.body?.mimeType === 'application/graphql') { 96 | bodyType = 'graphql'; 97 | body = { text: convertSyntax(r.body.text ?? '') }; 98 | } else if (r.body?.mimeType === 'application/json') { 99 | bodyType = 'application/json'; 100 | body = { text: convertSyntax(r.body.text ?? '') }; 101 | } 102 | 103 | let authenticationType: string | null = null; 104 | let authentication = {}; 105 | if (r.authentication.type === 'bearer') { 106 | authenticationType = 'bearer'; 107 | authentication = { 108 | token: convertSyntax(r.authentication.token), 109 | }; 110 | } else if (r.authentication.type === 'basic') { 111 | authenticationType = 'basic'; 112 | authentication = { 113 | username: convertSyntax(r.authentication.username), 114 | password: convertSyntax(r.authentication.password), 115 | }; 116 | } 117 | 118 | return { 119 | id: convertId(r.meta?.id ?? r._id), 120 | createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined, 121 | updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined, 122 | workspaceId: convertId(workspaceId), 123 | folderId: r.parentId === workspaceId ? null : convertId(r.parentId), 124 | model: 'http_request', 125 | sortPriority: r.metaSortKey, 126 | name: r.name, 127 | description: r.description || undefined, 128 | url: convertSyntax(r.url), 129 | body, 130 | bodyType, 131 | authentication, 132 | authenticationType, 133 | method: r.method, 134 | headers: (r.headers ?? []) 135 | .map((h: any) => ({ 136 | enabled: !h.disabled, 137 | name: h.name ?? '', 138 | value: h.value ?? '', 139 | })) 140 | .filter(({ name, value }: any) => name !== '' || value !== ''), 141 | }; 142 | } 143 | 144 | function importGrpcRequest( 145 | r: any, 146 | workspaceId: string, 147 | ): PartialImportResources['grpcRequests'][0] { 148 | const parts = r.protoMethodName.split('/').filter((p: any) => p !== ''); 149 | const service = parts[0] ?? null; 150 | const method = parts[1] ?? null; 151 | 152 | return { 153 | id: convertId(r.meta?.id ?? r._id), 154 | createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined, 155 | updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined, 156 | workspaceId: convertId(workspaceId), 157 | folderId: r.parentId === workspaceId ? null : convertId(r.parentId), 158 | model: 'grpc_request', 159 | sortPriority: r.metaSortKey, 160 | name: r.name, 161 | description: r.description || undefined, 162 | url: convertSyntax(r.url), 163 | service, 164 | method, 165 | message: r.body?.text ?? '', 166 | metadata: (r.metadata ?? []) 167 | .map((h: any) => ({ 168 | enabled: !h.disabled, 169 | name: h.name ?? '', 170 | value: h.value ?? '', 171 | })) 172 | .filter(({ name, value }: any) => name !== '' || value !== ''), 173 | }; 174 | } 175 | 176 | function importFolder(f: any, workspaceId: string): PartialImportResources['folders'][0] { 177 | return { 178 | id: convertId(f._id), 179 | createdAt: f.created ? new Date(f.created).toISOString().replace('Z', '') : undefined, 180 | updatedAt: f.modified ? new Date(f.modified).toISOString().replace('Z', '') : undefined, 181 | folderId: f.parentId === workspaceId ? null : convertId(f.parentId), 182 | workspaceId: convertId(workspaceId), 183 | description: f.description || undefined, 184 | model: 'folder', 185 | name: f.name, 186 | }; 187 | } 188 | 189 | function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] { 190 | return { 191 | id: convertId(e._id), 192 | createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined, 193 | updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined, 194 | workspaceId: convertId(workspaceId), 195 | // @ts-ignore 196 | sortPriority: e.metaSortKey, // Will be added to Yaak later 197 | base: isParent ?? e.parentId === workspaceId, 198 | model: 'environment', 199 | name: e.name, 200 | variables: Object.entries(e.data).map(([name, value]) => ({ 201 | enabled: true, 202 | name, 203 | value: `${value}`, 204 | })), 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/src/v5.ts: -------------------------------------------------------------------------------- 1 | import { PartialImportResources } from '@yaakapp/api'; 2 | import { convertId, convertSyntax, isJSObject } from './common'; 3 | 4 | export function convertInsomniaV5(parsed: Record) { 5 | if (!Array.isArray(parsed.collection)) return null; 6 | 7 | const resources: PartialImportResources = { 8 | environments: [], 9 | folders: [], 10 | grpcRequests: [], 11 | httpRequests: [], 12 | websocketRequests: [], 13 | workspaces: [], 14 | }; 15 | 16 | // Import workspaces 17 | const meta: Record = parsed.meta ?? {}; 18 | resources.workspaces.push({ 19 | id: convertId(meta.id ?? 'collection'), 20 | createdAt: meta.created ? new Date(meta.created).toISOString().replace('Z', '') : undefined, 21 | updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace('Z', '') : undefined, 22 | model: 'workspace', 23 | name: parsed.name, 24 | description: meta.description || undefined, 25 | }); 26 | resources.environments.push( 27 | importEnvironment(parsed.environments, meta.id, true), 28 | ...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)), 29 | ); 30 | 31 | const nextFolder = (children: any[], parentId: string) => { 32 | for (const child of children ?? []) { 33 | if (!isJSObject(child)) continue; 34 | 35 | if (Array.isArray(child.children)) { 36 | resources.folders.push(importFolder(child, meta.id, parentId)); 37 | nextFolder(child.children, child.meta.id); 38 | } else if (child.method) { 39 | resources.httpRequests.push( 40 | importHttpRequest(child, meta.id, parentId), 41 | ); 42 | } else if (child.protoFileId) { 43 | resources.grpcRequests.push( 44 | importGrpcRequest(child, meta.id, parentId), 45 | ); 46 | } else if (child.url) { 47 | resources.websocketRequests.push( 48 | importWebsocketRequest(child, meta.id, parentId), 49 | ); 50 | } 51 | } 52 | }; 53 | 54 | // Import folders 55 | nextFolder(parsed.collection ?? [], meta.id); 56 | 57 | // Filter out any `null` values 58 | resources.httpRequests = resources.httpRequests.filter(Boolean); 59 | resources.grpcRequests = resources.grpcRequests.filter(Boolean); 60 | resources.environments = resources.environments.filter(Boolean); 61 | resources.workspaces = resources.workspaces.filter(Boolean); 62 | 63 | return { resources }; 64 | } 65 | 66 | function importHttpRequest( 67 | r: any, 68 | workspaceId: string, 69 | parentId: string, 70 | ): PartialImportResources['httpRequests'][0] { 71 | const id = r.meta?.id ?? r._id; 72 | const created = r.meta?.created ?? r.created; 73 | const updated = r.meta?.modified ?? r.updated; 74 | const sortKey = r.meta?.sortKey ?? r.sortKey; 75 | 76 | let bodyType: string | null = null; 77 | let body = {}; 78 | if (r.body?.mimeType === 'application/octet-stream') { 79 | bodyType = 'binary'; 80 | body = { filePath: r.body.fileName ?? '' }; 81 | } else if (r.body?.mimeType === 'application/x-www-form-urlencoded') { 82 | bodyType = 'application/x-www-form-urlencoded'; 83 | body = { 84 | form: (r.body.params ?? []).map((p: any) => ({ 85 | enabled: !p.disabled, 86 | name: p.name ?? '', 87 | value: p.value ?? '', 88 | })), 89 | }; 90 | } else if (r.body?.mimeType === 'multipart/form-data') { 91 | bodyType = 'multipart/form-data'; 92 | body = { 93 | form: (r.body.params ?? []).map((p: any) => ({ 94 | enabled: !p.disabled, 95 | name: p.name ?? '', 96 | value: p.value ?? '', 97 | file: p.fileName ?? null, 98 | })), 99 | }; 100 | } else if (r.body?.mimeType === 'application/graphql') { 101 | bodyType = 'graphql'; 102 | body = { text: convertSyntax(r.body.text ?? '') }; 103 | } else if (r.body?.mimeType === 'application/json') { 104 | bodyType = 'application/json'; 105 | body = { text: convertSyntax(r.body.text ?? '') }; 106 | } 107 | 108 | return { 109 | id: convertId(id), 110 | workspaceId: convertId(workspaceId), 111 | createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined, 112 | updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, 113 | folderId: parentId === workspaceId ? null : convertId(parentId), 114 | sortPriority: sortKey, 115 | model: 'http_request', 116 | name: r.name, 117 | description: r.meta?.description || undefined, 118 | url: convertSyntax(r.url), 119 | body, 120 | bodyType, 121 | method: r.method, 122 | ...importHeaders(r), 123 | ...importAuthentication(r), 124 | }; 125 | } 126 | 127 | function importGrpcRequest( 128 | r: any, 129 | workspaceId: string, 130 | parentId: string, 131 | ): PartialImportResources['grpcRequests'][0] { 132 | const id = r.meta?.id ?? r._id; 133 | const created = r.meta?.created ?? r.created; 134 | const updated = r.meta?.modified ?? r.updated; 135 | const sortKey = r.meta?.sortKey ?? r.sortKey; 136 | 137 | const parts = r.protoMethodName.split('/').filter((p: any) => p !== ''); 138 | const service = parts[0] ?? null; 139 | const method = parts[1] ?? null; 140 | 141 | return { 142 | model: 'grpc_request', 143 | id: convertId(id), 144 | workspaceId: convertId(workspaceId), 145 | createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined, 146 | updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, 147 | folderId: parentId === workspaceId ? null : convertId(parentId), 148 | sortPriority: sortKey, 149 | name: r.name, 150 | description: r.description || undefined, 151 | url: convertSyntax(r.url), 152 | service, 153 | method, 154 | message: r.body?.text ?? '', 155 | metadata: (r.metadata ?? []) 156 | .map((h: any) => ({ 157 | enabled: !h.disabled, 158 | name: h.name ?? '', 159 | value: h.value ?? '', 160 | })) 161 | .filter(({ name, value }: any) => name !== '' || value !== ''), 162 | }; 163 | } 164 | 165 | function importWebsocketRequest( 166 | r: any, 167 | workspaceId: string, 168 | parentId: string, 169 | ): PartialImportResources['websocketRequests'][0] { 170 | const id = r.meta?.id ?? r._id; 171 | const created = r.meta?.created ?? r.created; 172 | const updated = r.meta?.modified ?? r.updated; 173 | const sortKey = r.meta?.sortKey ?? r.sortKey; 174 | 175 | return { 176 | model: 'websocket_request', 177 | id: convertId(id), 178 | workspaceId: convertId(workspaceId), 179 | createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined, 180 | updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, 181 | folderId: parentId === workspaceId ? null : convertId(parentId), 182 | sortPriority: sortKey, 183 | name: r.name, 184 | description: r.description || undefined, 185 | url: convertSyntax(r.url), 186 | message: r.body?.text ?? '', 187 | ...importHeaders(r), 188 | ...importAuthentication(r), 189 | }; 190 | } 191 | 192 | function importHeaders(r: any) { 193 | const headers = (r.headers ?? []) 194 | .map((h: any) => ({ 195 | enabled: !h.disabled, 196 | name: h.name ?? '', 197 | value: h.value ?? '', 198 | })) 199 | .filter(({ name, value }: any) => name !== '' || value !== ''); 200 | return { headers } as const; 201 | } 202 | 203 | function importAuthentication(r: any) { 204 | let authenticationType: string | null = null; 205 | let authentication = {}; 206 | if (r.authentication?.type === 'bearer') { 207 | authenticationType = 'bearer'; 208 | authentication = { 209 | token: convertSyntax(r.authentication.token), 210 | }; 211 | } else if (r.authentication?.type === 'basic') { 212 | authenticationType = 'basic'; 213 | authentication = { 214 | username: convertSyntax(r.authentication.username), 215 | password: convertSyntax(r.authentication.password), 216 | }; 217 | } 218 | 219 | return { authenticationType, authentication } as const; 220 | } 221 | 222 | function importFolder(f: any, workspaceId: string, parentId: string): PartialImportResources['folders'][0] { 223 | const id = f.meta?.id ?? f._id; 224 | const created = f.meta?.created ?? f.created; 225 | const updated = f.meta?.modified ?? f.updated; 226 | const sortKey = f.meta?.sortKey ?? f.sortKey; 227 | 228 | return { 229 | model: 'folder', 230 | id: convertId(id), 231 | createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined, 232 | updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, 233 | folderId: parentId === workspaceId ? null : convertId(parentId), 234 | sortPriority: sortKey, 235 | workspaceId: convertId(workspaceId), 236 | description: f.description || undefined, 237 | name: f.name, 238 | }; 239 | } 240 | 241 | 242 | function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] { 243 | const id = e.meta?.id ?? e._id; 244 | const created = e.meta?.created ?? e.created; 245 | const updated = e.meta?.modified ?? e.updated; 246 | const sortKey = e.meta?.sortKey ?? e.sortKey; 247 | 248 | return { 249 | id: convertId(id), 250 | createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined, 251 | updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, 252 | workspaceId: convertId(workspaceId), 253 | public: !e.isPrivate, 254 | // @ts-ignore 255 | sortPriority: sortKey, // Will be added to Yaak later 256 | base: isParent ?? e.parentId === workspaceId, 257 | model: 'environment', 258 | name: e.name, 259 | variables: Object.entries(e.data ?? {}).map(([name, value]) => ({ 260 | enabled: true, 261 | name, 262 | value: `${value}`, 263 | })), 264 | }; 265 | } 266 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/basic.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "export", 3 | "__export_format": 4, 4 | "__export_date": "2025-01-13T15:19:18.330Z", 5 | "__export_source": "insomnia.desktop.app:v10.3.0", 6 | "resources": [ 7 | { 8 | "_id": "req_84cd9ae4bd034dd8bb730e856a665cbb", 9 | "parentId": "fld_859d1df78261463480b6a3a1419517e3", 10 | "modified": 1736781473176, 11 | "created": 1736781406672, 12 | "url": "{{ _.BASE_URL }}/foo/:id", 13 | "name": "New Request", 14 | "description": "My description of the request", 15 | "method": "GET", 16 | "body": { 17 | "mimeType": "multipart/form-data", 18 | "params": [ 19 | { 20 | "id": "pair_7c86036ae8ef499dbbc0b43d0800c5a3", 21 | "name": "form", 22 | "value": "data", 23 | "description": "", 24 | "disabled": false 25 | } 26 | ] 27 | }, 28 | "parameters": [ 29 | { 30 | "id": "pair_b22f6ff611cd4250a6e405ca7b713d09", 31 | "name": "query", 32 | "value": "qqq", 33 | "description": "", 34 | "disabled": false 35 | } 36 | ], 37 | "headers": [ 38 | { 39 | "name": "Content-Type", 40 | "value": "multipart/form-data", 41 | "id": "pair_4af845963bd14256b98716617971eecd" 42 | }, 43 | { 44 | "name": "User-Agent", 45 | "value": "insomnia/10.3.0", 46 | "id": "pair_535ffd00ce48462cb1b7258832ade65a" 47 | }, 48 | { 49 | "id": "pair_ab4b870278e943cba6babf5a73e213e3", 50 | "name": "X-Header", 51 | "value": "xxxx", 52 | "description": "", 53 | "disabled": false 54 | } 55 | ], 56 | "authentication": { 57 | "type": "basic", 58 | "useISO88591": false, 59 | "disabled": false, 60 | "username": "user", 61 | "password": "pass" 62 | }, 63 | "metaSortKey": -1736781406672, 64 | "isPrivate": false, 65 | "pathParameters": [ 66 | { 67 | "name": "id", 68 | "value": "iii" 69 | } 70 | ], 71 | "settingStoreCookies": true, 72 | "settingSendCookies": true, 73 | "settingDisableRenderRequestBody": false, 74 | "settingEncodeUrl": true, 75 | "settingRebuildPath": true, 76 | "settingFollowRedirects": "global", 77 | "_type": "request" 78 | }, 79 | { 80 | "_id": "fld_859d1df78261463480b6a3a1419517e3", 81 | "parentId": "wrk_d4d92f7c0ee947b89159243506687019", 82 | "modified": 1736781404718, 83 | "created": 1736781404718, 84 | "name": "Top Level", 85 | "description": "", 86 | "environment": {}, 87 | "environmentPropertyOrder": null, 88 | "metaSortKey": -1736781404718, 89 | "environmentType": "kv", 90 | "_type": "request_group" 91 | }, 92 | { 93 | "_id": "wrk_d4d92f7c0ee947b89159243506687019", 94 | "parentId": null, 95 | "modified": 1736781343765, 96 | "created": 1736781343765, 97 | "name": "Dummy", 98 | "description": "", 99 | "scope": "collection", 100 | "_type": "workspace" 101 | }, 102 | { 103 | "_id": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900", 104 | "parentId": "wrk_d4d92f7c0ee947b89159243506687019", 105 | "modified": 1736781355209, 106 | "created": 1736781343767, 107 | "name": "Base Environment", 108 | "data": { 109 | "BASE_VAR": "hello" 110 | }, 111 | "dataPropertyOrder": null, 112 | "color": null, 113 | "isPrivate": false, 114 | "metaSortKey": 1736781343767, 115 | "environmentType": "kv", 116 | "kvPairData": [ 117 | { 118 | "id": "envPair_61c1be66d42241b5a28306d2cd92d3e3", 119 | "name": "BASE_VAR", 120 | "value": "hello", 121 | "type": "str", 122 | "enabled": true 123 | } 124 | ], 125 | "_type": "environment" 126 | }, 127 | { 128 | "_id": "jar_16c0dec5b77c414ae0e419b8f10c3701300c5900", 129 | "parentId": "wrk_d4d92f7c0ee947b89159243506687019", 130 | "modified": 1736781343768, 131 | "created": 1736781343768, 132 | "name": "Default Jar", 133 | "cookies": [], 134 | "_type": "cookie_jar" 135 | }, 136 | { 137 | "_id": "env_799ae3d723ef44af91b4817e5d057e6d", 138 | "parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900", 139 | "modified": 1736781394705, 140 | "created": 1736781358515, 141 | "name": "Production", 142 | "data": { 143 | "BASE_URL": "https://api.yaak.app" 144 | }, 145 | "dataPropertyOrder": null, 146 | "color": "#f22c2c", 147 | "isPrivate": false, 148 | "metaSortKey": 1736781358515, 149 | "environmentType": "kv", 150 | "kvPairData": [ 151 | { 152 | "id": "envPair_4d97b569b7e845ccbf488e1b26637cbc", 153 | "name": "BASE_URL", 154 | "value": "https://api.yaak.app", 155 | "type": "str", 156 | "enabled": true 157 | } 158 | ], 159 | "_type": "environment" 160 | }, 161 | { 162 | "_id": "env_030fbfdbb274426ebd78e2e6518f8553", 163 | "parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900", 164 | "modified": 1736781391078, 165 | "created": 1736781374707, 166 | "name": "Staging", 167 | "data": { 168 | "BASE_URL": "https://api.staging.yaak.app" 169 | }, 170 | "dataPropertyOrder": null, 171 | "color": "#206fac", 172 | "isPrivate": false, 173 | "metaSortKey": 1736781358565, 174 | "environmentType": "kv", 175 | "kvPairData": [ 176 | { 177 | "id": "envPair_4d97b569b7e845ccbf488e1b26637cbc", 178 | "name": "BASE_URL", 179 | "value": "https://api.staging.yaak.app", 180 | "type": "str", 181 | "enabled": true 182 | } 183 | ], 184 | "_type": "environment" 185 | } 186 | ] 187 | } 188 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/basic.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "environments": [ 4 | { 5 | "createdAt": "2025-01-13T15:15:43.767", 6 | "updatedAt": "2025-01-13T15:15:55.209", 7 | "sortPriority": 1736781343767, 8 | "base": true, 9 | "id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900", 10 | "model": "environment", 11 | "name": "Base Environment", 12 | "variables": [ 13 | { 14 | "enabled": true, 15 | "name": "BASE_VAR", 16 | "value": "hello" 17 | } 18 | ], 19 | "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" 20 | }, 21 | { 22 | "createdAt": "2025-01-13T15:15:58.515", 23 | "updatedAt": "2025-01-13T15:16:34.705", 24 | "sortPriority": 1736781358515, 25 | "base": false, 26 | "id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d", 27 | "model": "environment", 28 | "name": "Production", 29 | "variables": [ 30 | { 31 | "enabled": true, 32 | "name": "BASE_URL", 33 | "value": "https://api.yaak.app" 34 | } 35 | ], 36 | "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" 37 | }, 38 | { 39 | "createdAt": "2025-01-13T15:16:14.707", 40 | "updatedAt": "2025-01-13T15:16:31.078", 41 | "sortPriority": 1736781358565, 42 | "base": false, 43 | "id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553", 44 | "model": "environment", 45 | "name": "Staging", 46 | "variables": [ 47 | { 48 | "enabled": true, 49 | "name": "BASE_URL", 50 | "value": "https://api.staging.yaak.app" 51 | } 52 | ], 53 | "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" 54 | } 55 | ], 56 | "folders": [ 57 | { 58 | "createdAt": "2025-01-13T15:16:44.718", 59 | "updatedAt": "2025-01-13T15:16:44.718", 60 | "folderId": null, 61 | "id": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3", 62 | "model": "folder", 63 | "name": "Top Level", 64 | "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" 65 | } 66 | ], 67 | "grpcRequests": [], 68 | "httpRequests": [ 69 | { 70 | "authentication": { 71 | "password": "pass", 72 | "username": "user" 73 | }, 74 | "authenticationType": "basic", 75 | "body": { 76 | "form": [ 77 | { 78 | "enabled": true, 79 | "file": null, 80 | "name": "form", 81 | "value": "data" 82 | } 83 | ] 84 | }, 85 | "bodyType": "multipart/form-data", 86 | "createdAt": "2025-01-13T15:16:46.672", 87 | "sortPriority": -1736781406672, 88 | "updatedAt": "2025-01-13T15:17:53.176", 89 | "description": "My description of the request", 90 | "folderId": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3", 91 | "headers": [ 92 | { 93 | "enabled": true, 94 | "name": "Content-Type", 95 | "value": "multipart/form-data" 96 | }, 97 | { 98 | "enabled": true, 99 | "name": "User-Agent", 100 | "value": "insomnia/10.3.0" 101 | }, 102 | { 103 | "enabled": true, 104 | "name": "X-Header", 105 | "value": "xxxx" 106 | } 107 | ], 108 | "id": "GENERATE_ID::req_84cd9ae4bd034dd8bb730e856a665cbb", 109 | "method": "GET", 110 | "model": "http_request", 111 | "name": "New Request", 112 | "url": "${[BASE_URL ]}/foo/:id", 113 | "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" 114 | } 115 | ], 116 | "websocketRequests": [], 117 | "workspaces": [ 118 | { 119 | "createdAt": "2025-01-13T15:15:43.765", 120 | "id": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019", 121 | "model": "workspace", 122 | "name": "Dummy" 123 | } 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/version-5-minimal.input.yaml: -------------------------------------------------------------------------------- 1 | type: collection.insomnia.rest/5.0 2 | name: Debugging 3 | meta: 4 | id: wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c 5 | created: 1747197924902 6 | modified: 1747197924902 7 | collection: 8 | - name: My Folder 9 | meta: 10 | id: fld_296933ea4ea84783a775d199997e9be7 11 | created: 1747414092298 12 | modified: 1747414142427 13 | sortKey: -1747414092298 14 | children: 15 | - url: https://httpbin.org/post 16 | name: New Request 17 | meta: 18 | id: req_9a80320365ac4509ade406359dbc6a71 19 | created: 1747197928502 20 | modified: 1747414129313 21 | isPrivate: false 22 | sortKey: -1747414129276 23 | method: GET 24 | headers: 25 | - name: User-Agent 26 | value: insomnia/11.1.0 27 | id: pair_6ae87d1620a9494f8e5b29cd9f92d087 28 | settings: 29 | renderRequestBody: true 30 | encodeUrl: true 31 | followRedirects: global 32 | cookies: 33 | send: true 34 | store: true 35 | rebuildPath: true 36 | headers: 37 | - id: pair_f2b330e3914f4c11b209318aef94325c 38 | name: foo 39 | value: bar 40 | disabled: false 41 | - name: New Request 42 | meta: 43 | id: req_e3f8cdbd58784a539dd4c1e127d73451 44 | created: 1747414160497 45 | modified: 1747414160497 46 | isPrivate: false 47 | sortKey: -1747414160498 48 | method: GET 49 | headers: 50 | - name: User-Agent 51 | value: insomnia/11.1.0 52 | settings: 53 | renderRequestBody: true 54 | encodeUrl: true 55 | followRedirects: global 56 | cookies: 57 | send: true 58 | store: true 59 | rebuildPath: true 60 | cookieJar: 61 | name: Default Jar 62 | meta: 63 | id: jar_e46dc73e8ccda30ca132153e8f11183bd08119ce 64 | created: 1747197924904 65 | modified: 1747197924904 66 | environments: 67 | name: Base Environment 68 | meta: 69 | id: env_e46dc73e8ccda30ca132153e8f11183bd08119ce 70 | created: 1747197924903 71 | modified: 1747197924903 72 | isPrivate: false 73 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/version-5-minimal.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "environments": [ 4 | { 5 | "base": true, 6 | "createdAt": "2025-05-14T04:45:24.903", 7 | "id": "GENERATE_ID::env_e46dc73e8ccda30ca132153e8f11183bd08119ce", 8 | "model": "environment", 9 | "name": "Base Environment", 10 | "public": true, 11 | "updatedAt": "2025-05-14T04:45:24.903", 12 | "variables": [], 13 | "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" 14 | } 15 | ], 16 | "folders": [ 17 | { 18 | "createdAt": "2025-05-16T16:48:12.298", 19 | "folderId": null, 20 | "id": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7", 21 | "model": "folder", 22 | "name": "My Folder", 23 | "sortPriority": -1747414092298, 24 | "updatedAt": "2025-05-16T16:49:02.427", 25 | "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" 26 | } 27 | ], 28 | "grpcRequests": [], 29 | "httpRequests": [ 30 | { 31 | "authentication": {}, 32 | "authenticationType": null, 33 | "body": {}, 34 | "bodyType": null, 35 | "createdAt": "2025-05-14T04:45:28.502", 36 | "folderId": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7", 37 | "headers": [ 38 | { 39 | "enabled": true, 40 | "name": "User-Agent", 41 | "value": "insomnia/11.1.0" 42 | } 43 | ], 44 | "id": "GENERATE_ID::req_9a80320365ac4509ade406359dbc6a71", 45 | "method": "GET", 46 | "model": "http_request", 47 | "name": "New Request", 48 | "sortPriority": -1747414129276, 49 | "updatedAt": "2025-05-16T16:48:49.313", 50 | "url": "https://httpbin.org/post", 51 | "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" 52 | }, 53 | { 54 | "authentication": {}, 55 | "authenticationType": null, 56 | "body": {}, 57 | "bodyType": null, 58 | "createdAt": "2025-05-16T16:49:20.497", 59 | "folderId": null, 60 | "headers": [ 61 | { 62 | "enabled": true, 63 | "name": "User-Agent", 64 | "value": "insomnia/11.1.0" 65 | } 66 | ], 67 | "id": "GENERATE_ID::req_e3f8cdbd58784a539dd4c1e127d73451", 68 | "method": "GET", 69 | "model": "http_request", 70 | "name": "New Request", 71 | "sortPriority": -1747414160498, 72 | "updatedAt": "2025-05-16T16:49:20.497", 73 | "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" 74 | } 75 | ], 76 | "websocketRequests": [], 77 | "workspaces": [ 78 | { 79 | "createdAt": "2025-05-14T04:45:24.902", 80 | "id": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c", 81 | "model": "workspace", 82 | "name": "Debugging", 83 | "updatedAt": "2025-05-14T04:45:24.902" 84 | } 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/version-5.input.yaml: -------------------------------------------------------------------------------- 1 | type: collection.insomnia.rest/5.0 2 | name: Dummy 3 | meta: 4 | id: wrk_c1eacfa750a04f3ea9985ef28043fa53 5 | created: 1746799305927 6 | modified: 1746843054272 7 | description: This is the description 8 | collection: 9 | - name: Top Level 10 | meta: 11 | id: fld_42eb2e2bb22b4cedacbd3d057634e80c 12 | created: 1736781404718 13 | modified: 1736781404718 14 | sortKey: -1736781404718 15 | children: 16 | - url: "{{ _.BASE_URL }}/foo/:id" 17 | name: New Request 18 | meta: 19 | id: req_d72fff2a6b104b91a2ebe9de9edd2785 20 | created: 1736781406672 21 | modified: 1736781473176 22 | isPrivate: false 23 | description: My description of the request 24 | sortKey: -1736781406672 25 | method: GET 26 | body: 27 | mimeType: multipart/form-data 28 | params: 29 | - id: pair_7c86036ae8ef499dbbc0b43d0800c5a3 30 | name: form 31 | value: data 32 | disabled: false 33 | parameters: 34 | - id: pair_b22f6ff611cd4250a6e405ca7b713d09 35 | name: query 36 | value: qqq 37 | disabled: false 38 | headers: 39 | - name: Content-Type 40 | value: multipart/form-data 41 | id: pair_4af845963bd14256b98716617971eecd 42 | - name: User-Agent 43 | value: insomnia/10.3.0 44 | id: pair_535ffd00ce48462cb1b7258832ade65a 45 | - id: pair_ab4b870278e943cba6babf5a73e213e3 46 | name: X-Header 47 | value: xxxx 48 | disabled: false 49 | authentication: 50 | type: basic 51 | useISO88591: false 52 | disabled: false 53 | username: user 54 | password: pass 55 | settings: 56 | renderRequestBody: true 57 | encodeUrl: true 58 | followRedirects: global 59 | cookies: 60 | send: true 61 | store: true 62 | rebuildPath: true 63 | pathParameters: 64 | - name: id 65 | value: iii 66 | - url: grpcb.in:9000 67 | name: New Request 68 | meta: 69 | id: greq_06d659324df94504a4d64632be7106b3 70 | created: 1746799344864 71 | modified: 1746799544082 72 | isPrivate: false 73 | sortKey: -1746799344864 74 | body: 75 | text: |- 76 | { 77 | "greeting": "Greg" 78 | } 79 | protoFileId: pf_9d45b0dfaccc4bcc9d930746716786c5 80 | protoMethodName: /hello.HelloService/SayHello 81 | reflectionApi: 82 | enabled: false 83 | url: https://buf.build 84 | module: buf.build/connectrpc/eliza 85 | - url: wss://echo.websocket.org 86 | name: New WebSocket Request 87 | meta: 88 | id: ws-req_5d1a4c7c79494743962e5176f6add270 89 | created: 1746799553909 90 | modified: 1746887120958 91 | sortKey: -1746799553909 92 | settings: 93 | encodeUrl: true 94 | followRedirects: global 95 | cookies: 96 | send: true 97 | store: true 98 | authentication: 99 | type: basic 100 | useISO88591: false 101 | disabled: false 102 | username: user 103 | password: password 104 | headers: 105 | - name: User-Agent 106 | value: insomnia/11.1.0 107 | cookieJar: 108 | name: Default Jar 109 | meta: 110 | id: jar_663d5741b072441aa2709a6113371510 111 | created: 1736781343768 112 | modified: 1736781343768 113 | environments: 114 | name: Base Environment 115 | meta: 116 | id: env_20945044d3c8497ca8b717bef750987e 117 | created: 1736781343767 118 | modified: 1736781355209 119 | isPrivate: false 120 | data: 121 | BASE_VAR: hello 122 | subEnvironments: 123 | - name: Production 124 | meta: 125 | id: env_6f7728bb7fc04d558d668e954d756ea2 126 | created: 1736781358515 127 | modified: 1736781394705 128 | isPrivate: false 129 | sortKey: 1736781358515 130 | data: 131 | BASE_URL: https://api.yaak.app 132 | color: "#f22c2c" 133 | - name: Staging 134 | meta: 135 | id: env_976a8b6eb5d44fb6a20150f65c32d243 136 | created: 1736781374707 137 | modified: 1736781391078 138 | isPrivate: false 139 | sortKey: 1736781358565 140 | data: 141 | BASE_URL: https://api.staging.yaak.app 142 | color: "#206fac" 143 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/fixtures/version-5.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "environments": [ 4 | { 5 | "createdAt": "2025-01-13T15:15:43.767", 6 | "updatedAt": "2025-01-13T15:15:55.209", 7 | "base": true, 8 | "public": true, 9 | "id": "GENERATE_ID::env_20945044d3c8497ca8b717bef750987e", 10 | "model": "environment", 11 | "name": "Base Environment", 12 | "variables": [ 13 | { 14 | "enabled": true, 15 | "name": "BASE_VAR", 16 | "value": "hello" 17 | } 18 | ], 19 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 20 | }, 21 | { 22 | "createdAt": "2025-01-13T15:15:58.515", 23 | "updatedAt": "2025-01-13T15:16:34.705", 24 | "base": false, 25 | "public": true, 26 | "id": "GENERATE_ID::env_6f7728bb7fc04d558d668e954d756ea2", 27 | "model": "environment", 28 | "name": "Production", 29 | "sortPriority": 1736781358515, 30 | "variables": [ 31 | { 32 | "enabled": true, 33 | "name": "BASE_URL", 34 | "value": "https://api.yaak.app" 35 | } 36 | ], 37 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 38 | }, 39 | { 40 | "createdAt": "2025-01-13T15:16:14.707", 41 | "updatedAt": "2025-01-13T15:16:31.078", 42 | "base": false, 43 | "public": true, 44 | "id": "GENERATE_ID::env_976a8b6eb5d44fb6a20150f65c32d243", 45 | "model": "environment", 46 | "name": "Staging", 47 | "sortPriority": 1736781358565, 48 | "variables": [ 49 | { 50 | "enabled": true, 51 | "name": "BASE_URL", 52 | "value": "https://api.staging.yaak.app" 53 | } 54 | ], 55 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 56 | } 57 | ], 58 | "folders": [ 59 | { 60 | "createdAt": "2025-01-13T15:16:44.718", 61 | "updatedAt": "2025-01-13T15:16:44.718", 62 | "folderId": null, 63 | "id": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c", 64 | "model": "folder", 65 | "name": "Top Level", 66 | "sortPriority": -1736781404718, 67 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 68 | } 69 | ], 70 | "grpcRequests": [ 71 | { 72 | "model": "grpc_request", 73 | "createdAt": "2025-05-09T14:02:24.864", 74 | "folderId": null, 75 | "id": "GENERATE_ID::greq_06d659324df94504a4d64632be7106b3", 76 | "message": "{\n\t\"greeting\": \"Greg\"\n}", 77 | "metadata": [], 78 | "method": "SayHello", 79 | "name": "New Request", 80 | "service": "hello.HelloService", 81 | "sortPriority": -1746799344864, 82 | "updatedAt": "2025-05-09T14:05:44.082", 83 | "url": "grpcb.in:9000", 84 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 85 | } 86 | ], 87 | "httpRequests": [ 88 | { 89 | "authentication": { 90 | "password": "pass", 91 | "username": "user" 92 | }, 93 | "authenticationType": "basic", 94 | "body": { 95 | "form": [ 96 | { 97 | "enabled": true, 98 | "file": null, 99 | "name": "form", 100 | "value": "data" 101 | } 102 | ] 103 | }, 104 | "bodyType": "multipart/form-data", 105 | "createdAt": "2025-01-13T15:16:46.672", 106 | "updatedAt": "2025-01-13T15:17:53.176", 107 | "description": "My description of the request", 108 | "folderId": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c", 109 | "headers": [ 110 | { 111 | "enabled": true, 112 | "name": "Content-Type", 113 | "value": "multipart/form-data" 114 | }, 115 | { 116 | "enabled": true, 117 | "name": "User-Agent", 118 | "value": "insomnia/10.3.0" 119 | }, 120 | { 121 | "enabled": true, 122 | "name": "X-Header", 123 | "value": "xxxx" 124 | } 125 | ], 126 | "id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785", 127 | "method": "GET", 128 | "model": "http_request", 129 | "name": "New Request", 130 | "sortPriority": -1736781406672, 131 | "url": "${[BASE_URL ]}/foo/:id", 132 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 133 | } 134 | ], 135 | "websocketRequests": [ 136 | { 137 | "id": "GENERATE_ID::ws-req_5d1a4c7c79494743962e5176f6add270", 138 | "createdAt": "2025-05-09T14:05:53.909", 139 | "updatedAt": "2025-05-10T14:25:20.958", 140 | "message": "", 141 | "model": "websocket_request", 142 | "name": "New WebSocket Request", 143 | "sortPriority": -1746799553909, 144 | "authenticationType": "basic", 145 | "authentication": { 146 | "password": "password", 147 | "username": "user" 148 | }, 149 | "folderId": null, 150 | "headers": [ 151 | { 152 | "enabled": true, 153 | "name": "User-Agent", 154 | "value": "insomnia/11.1.0" 155 | } 156 | ], 157 | "url": "wss://echo.websocket.org", 158 | "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" 159 | } 160 | ], 161 | "workspaces": [ 162 | { 163 | "createdAt": "2025-05-09T14:01:45.927", 164 | "updatedAt": "2025-05-10T02:10:54.272", 165 | "description": "This is the description", 166 | "id": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53", 167 | "model": "workspace", 168 | "name": "Dummy" 169 | } 170 | ] 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /plugins/importer-insomnia/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { describe, expect, test } from 'vitest'; 4 | import YAML from 'yaml'; 5 | import { convertInsomnia } from '../src'; 6 | 7 | describe('importer-yaak', () => { 8 | const p = path.join(__dirname, 'fixtures'); 9 | const fixtures = fs.readdirSync(p); 10 | 11 | for (const fixture of fixtures) { 12 | if (fixture.includes('.output')) { 13 | continue; 14 | } 15 | 16 | test('Imports ' + fixture, () => { 17 | const contents = fs.readFileSync(path.join(p, fixture), 'utf-8'); 18 | const expected = fs.readFileSync(path.join(p, fixture.replace(/.input\..*/, '.output.json')), 'utf-8'); 19 | const result = convertInsomnia(contents); 20 | // console.log(JSON.stringify(result, null, 2)) 21 | expect(result).toEqual(parseJsonOrYaml(expected)); 22 | }); 23 | } 24 | }); 25 | 26 | function parseJsonOrYaml(text: string): unknown { 27 | try { 28 | return JSON.parse(text); 29 | } catch { 30 | return YAML.parse(text); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugins/importer-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/importer-openapi", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "openapi-to-postmanv2": "^5.0.0", 11 | "yaml": "^2.4.2" 12 | }, 13 | "devDependencies": { 14 | "@types/openapi-to-postmanv2": "^3.2.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plugins/importer-openapi/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, Environment, Folder, HttpRequest, PluginDefinition, Workspace } from '@yaakapp/api'; 2 | import { convert } from 'openapi-to-postmanv2'; 3 | import { convertPostman } from '@yaakapp/importer-postman/src'; 4 | 5 | type AtLeast = Partial & Pick; 6 | 7 | interface ExportResources { 8 | workspaces: AtLeast[]; 9 | environments: AtLeast[]; 10 | httpRequests: AtLeast[]; 11 | folders: AtLeast[]; 12 | } 13 | 14 | export const plugin: PluginDefinition = { 15 | importer: { 16 | name: 'OpenAPI', 17 | description: 'Import OpenAPI collections', 18 | onImport(_ctx: Context, args: { text: string }) { 19 | return convertOpenApi(args.text) as any; 20 | }, 21 | }, 22 | }; 23 | 24 | export async function convertOpenApi( 25 | contents: string, 26 | ): Promise<{ resources: ExportResources } | undefined> { 27 | let postmanCollection; 28 | try { 29 | postmanCollection = await new Promise((resolve, reject) => { 30 | convert({ type: 'string', data: contents }, {}, (err, result: any) => { 31 | if (err != null) reject(err); 32 | 33 | if (Array.isArray(result.output) && result.output.length > 0) { 34 | resolve(result.output[0].data); 35 | } 36 | }); 37 | }); 38 | } catch (err) { 39 | // Probably not an OpenAPI file, so skip it 40 | return undefined; 41 | } 42 | 43 | return convertPostman(JSON.stringify(postmanCollection)); 44 | } 45 | -------------------------------------------------------------------------------- /plugins/importer-openapi/tests/fixtures/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | servers: 3 | - url: /v3 4 | info: 5 | description: |- 6 | This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about 7 | Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! 8 | You can now help us improve the API whether it's by making changes to the definition itself or to the code. 9 | That way, with time, we can improve the API in general, and expose some of the new features in OAS3. 10 | 11 | Some useful links: 12 | - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) 13 | - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) 14 | version: 1.0.20-SNAPSHOT 15 | title: Swagger Petstore - OpenAPI 3.0 16 | termsOfService: 'http://swagger.io/terms/' 17 | contact: 18 | email: apiteam@swagger.io 19 | license: 20 | name: Apache 2.0 21 | url: 'http://www.apache.org/licenses/LICENSE-2.0.html' 22 | tags: 23 | - name: pet 24 | description: Everything about your Pets 25 | externalDocs: 26 | description: Find out more 27 | url: 'http://swagger.io' 28 | - name: store 29 | description: Access to Petstore orders 30 | externalDocs: 31 | description: Find out more about our store 32 | url: 'http://swagger.io' 33 | - name: user 34 | description: Operations about user 35 | paths: 36 | /pet: 37 | post: 38 | tags: 39 | - pet 40 | summary: Add a new pet to the store 41 | description: Add a new pet to the store 42 | operationId: addPet 43 | responses: 44 | '200': 45 | description: Successful operation 46 | content: 47 | application/xml: 48 | schema: 49 | $ref: '#/components/schemas/Pet' 50 | application/json: 51 | schema: 52 | $ref: '#/components/schemas/Pet' 53 | '405': 54 | description: Invalid input 55 | security: 56 | - petstore_auth: 57 | - 'write:pets' 58 | - 'read:pets' 59 | requestBody: 60 | description: Create a new pet in the store 61 | required: true 62 | content: 63 | application/json: 64 | schema: 65 | $ref: '#/components/schemas/Pet' 66 | application/xml: 67 | schema: 68 | $ref: '#/components/schemas/Pet' 69 | application/x-www-form-urlencoded: 70 | schema: 71 | $ref: '#/components/schemas/Pet' 72 | put: 73 | tags: 74 | - pet 75 | summary: Update an existing pet 76 | description: Update an existing pet by Id 77 | operationId: updatePet 78 | responses: 79 | '200': 80 | description: Successful operation 81 | content: 82 | application/xml: 83 | schema: 84 | $ref: '#/components/schemas/Pet' 85 | application/json: 86 | schema: 87 | $ref: '#/components/schemas/Pet' 88 | '400': 89 | description: Invalid ID supplied 90 | '404': 91 | description: Pet not found 92 | '405': 93 | description: Validation exception 94 | security: 95 | - petstore_auth: 96 | - 'write:pets' 97 | - 'read:pets' 98 | requestBody: 99 | description: Update an existent pet in the store 100 | required: true 101 | content: 102 | application/json: 103 | schema: 104 | $ref: '#/components/schemas/Pet' 105 | application/xml: 106 | schema: 107 | $ref: '#/components/schemas/Pet' 108 | application/x-www-form-urlencoded: 109 | schema: 110 | $ref: '#/components/schemas/Pet' 111 | /pet/findByStatus: 112 | get: 113 | tags: 114 | - pet 115 | summary: Finds Pets by status 116 | description: Multiple status values can be provided with comma separated strings 117 | operationId: findPetsByStatus 118 | parameters: 119 | - name: status 120 | in: query 121 | description: Status values that need to be considered for filter 122 | required: false 123 | explode: true 124 | schema: 125 | type: string 126 | enum: 127 | - available 128 | - pending 129 | - sold 130 | default: available 131 | responses: 132 | '200': 133 | description: successful operation 134 | content: 135 | application/xml: 136 | schema: 137 | type: array 138 | items: 139 | $ref: '#/components/schemas/Pet' 140 | application/json: 141 | schema: 142 | type: array 143 | items: 144 | $ref: '#/components/schemas/Pet' 145 | '400': 146 | description: Invalid status value 147 | security: 148 | - petstore_auth: 149 | - 'write:pets' 150 | - 'read:pets' 151 | /pet/findByTags: 152 | get: 153 | tags: 154 | - pet 155 | summary: Finds Pets by tags 156 | description: >- 157 | Multiple tags can be provided with comma separated strings. Use tag1, 158 | tag2, tag3 for testing. 159 | operationId: findPetsByTags 160 | parameters: 161 | - name: tags 162 | in: query 163 | description: Tags to filter by 164 | required: false 165 | explode: true 166 | schema: 167 | type: array 168 | items: 169 | type: string 170 | responses: 171 | '200': 172 | description: successful operation 173 | content: 174 | application/xml: 175 | schema: 176 | type: array 177 | items: 178 | $ref: '#/components/schemas/Pet' 179 | application/json: 180 | schema: 181 | type: array 182 | items: 183 | $ref: '#/components/schemas/Pet' 184 | '400': 185 | description: Invalid tag value 186 | security: 187 | - petstore_auth: 188 | - 'write:pets' 189 | - 'read:pets' 190 | '/pet/{petId}': 191 | get: 192 | tags: 193 | - pet 194 | summary: Find pet by ID 195 | description: Returns a single pet 196 | operationId: getPetById 197 | parameters: 198 | - name: petId 199 | in: path 200 | description: ID of pet to return 201 | required: true 202 | schema: 203 | type: integer 204 | format: int64 205 | responses: 206 | '200': 207 | description: successful operation 208 | content: 209 | application/xml: 210 | schema: 211 | $ref: '#/components/schemas/Pet' 212 | application/json: 213 | schema: 214 | $ref: '#/components/schemas/Pet' 215 | '400': 216 | description: Invalid ID supplied 217 | '404': 218 | description: Pet not found 219 | security: 220 | - api_key: [] 221 | - petstore_auth: 222 | - 'write:pets' 223 | - 'read:pets' 224 | post: 225 | tags: 226 | - pet 227 | summary: Updates a pet in the store with form data 228 | description: '' 229 | operationId: updatePetWithForm 230 | parameters: 231 | - name: petId 232 | in: path 233 | description: ID of pet that needs to be updated 234 | required: true 235 | schema: 236 | type: integer 237 | format: int64 238 | - name: name 239 | in: query 240 | description: Name of pet that needs to be updated 241 | schema: 242 | type: string 243 | - name: status 244 | in: query 245 | description: Status of pet that needs to be updated 246 | schema: 247 | type: string 248 | responses: 249 | '405': 250 | description: Invalid input 251 | security: 252 | - petstore_auth: 253 | - 'write:pets' 254 | - 'read:pets' 255 | delete: 256 | tags: 257 | - pet 258 | summary: Deletes a pet 259 | description: '' 260 | operationId: deletePet 261 | parameters: 262 | - name: api_key 263 | in: header 264 | description: '' 265 | required: false 266 | schema: 267 | type: string 268 | - name: petId 269 | in: path 270 | description: Pet id to delete 271 | required: true 272 | schema: 273 | type: integer 274 | format: int64 275 | responses: 276 | '400': 277 | description: Invalid pet value 278 | security: 279 | - petstore_auth: 280 | - 'write:pets' 281 | - 'read:pets' 282 | '/pet/{petId}/uploadImage': 283 | post: 284 | tags: 285 | - pet 286 | summary: uploads an image 287 | description: '' 288 | operationId: uploadFile 289 | parameters: 290 | - name: petId 291 | in: path 292 | description: ID of pet to update 293 | required: true 294 | schema: 295 | type: integer 296 | format: int64 297 | - name: additionalMetadata 298 | in: query 299 | description: Additional Metadata 300 | required: false 301 | schema: 302 | type: string 303 | responses: 304 | '200': 305 | description: successful operation 306 | content: 307 | application/json: 308 | schema: 309 | $ref: '#/components/schemas/ApiResponse' 310 | security: 311 | - petstore_auth: 312 | - 'write:pets' 313 | - 'read:pets' 314 | requestBody: 315 | content: 316 | application/octet-stream: 317 | schema: 318 | type: string 319 | format: binary 320 | /store/inventory: 321 | get: 322 | tags: 323 | - store 324 | summary: Returns pet inventories by status 325 | description: Returns a map of status codes to quantities 326 | operationId: getInventory 327 | x-swagger-router-controller: OrderController 328 | responses: 329 | '200': 330 | description: successful operation 331 | content: 332 | application/json: 333 | schema: 334 | type: object 335 | additionalProperties: 336 | type: integer 337 | format: int32 338 | security: 339 | - api_key: [] 340 | /store/order: 341 | post: 342 | tags: 343 | - store 344 | summary: Place an order for a pet 345 | description: Place a new order in the store 346 | operationId: placeOrder 347 | x-swagger-router-controller: OrderController 348 | responses: 349 | '200': 350 | description: successful operation 351 | content: 352 | application/json: 353 | schema: 354 | $ref: '#/components/schemas/Order' 355 | '405': 356 | description: Invalid input 357 | requestBody: 358 | content: 359 | application/json: 360 | schema: 361 | $ref: '#/components/schemas/Order' 362 | application/xml: 363 | schema: 364 | $ref: '#/components/schemas/Order' 365 | application/x-www-form-urlencoded: 366 | schema: 367 | $ref: '#/components/schemas/Order' 368 | '/store/order/{orderId}': 369 | get: 370 | tags: 371 | - store 372 | summary: Find purchase order by ID 373 | x-swagger-router-controller: OrderController 374 | description: >- 375 | For valid response try integer IDs with value <= 5 or > 10. Other values 376 | will generate exceptions. 377 | operationId: getOrderById 378 | parameters: 379 | - name: orderId 380 | in: path 381 | description: ID of order that needs to be fetched 382 | required: true 383 | schema: 384 | type: integer 385 | format: int64 386 | responses: 387 | '200': 388 | description: successful operation 389 | content: 390 | application/xml: 391 | schema: 392 | $ref: '#/components/schemas/Order' 393 | application/json: 394 | schema: 395 | $ref: '#/components/schemas/Order' 396 | '400': 397 | description: Invalid ID supplied 398 | '404': 399 | description: Order not found 400 | delete: 401 | tags: 402 | - store 403 | summary: Delete purchase order by ID 404 | x-swagger-router-controller: OrderController 405 | description: >- 406 | For valid response try integer IDs with value < 1000. Anything above 407 | 1000 or nonintegers will generate API errors 408 | operationId: deleteOrder 409 | parameters: 410 | - name: orderId 411 | in: path 412 | description: ID of the order that needs to be deleted 413 | required: true 414 | schema: 415 | type: integer 416 | format: int64 417 | responses: 418 | '400': 419 | description: Invalid ID supplied 420 | '404': 421 | description: Order not found 422 | /user: 423 | post: 424 | tags: 425 | - user 426 | summary: Create user 427 | description: This can only be done by the logged in user. 428 | operationId: createUser 429 | responses: 430 | default: 431 | description: successful operation 432 | content: 433 | application/json: 434 | schema: 435 | $ref: '#/components/schemas/User' 436 | application/xml: 437 | schema: 438 | $ref: '#/components/schemas/User' 439 | requestBody: 440 | content: 441 | application/json: 442 | schema: 443 | $ref: '#/components/schemas/User' 444 | application/xml: 445 | schema: 446 | $ref: '#/components/schemas/User' 447 | application/x-www-form-urlencoded: 448 | schema: 449 | $ref: '#/components/schemas/User' 450 | description: Created user object 451 | /user/createWithList: 452 | post: 453 | tags: 454 | - user 455 | summary: Creates list of users with given input array 456 | description: 'Creates list of users with given input array' 457 | x-swagger-router-controller: UserController 458 | operationId: createUsersWithListInput 459 | responses: 460 | '200': 461 | description: Successful operation 462 | content: 463 | application/xml: 464 | schema: 465 | $ref: '#/components/schemas/User' 466 | application/json: 467 | schema: 468 | $ref: '#/components/schemas/User' 469 | default: 470 | description: successful operation 471 | requestBody: 472 | content: 473 | application/json: 474 | schema: 475 | type: array 476 | items: 477 | $ref: '#/components/schemas/User' 478 | /user/login: 479 | get: 480 | tags: 481 | - user 482 | summary: Logs user into the system 483 | description: '' 484 | operationId: loginUser 485 | parameters: 486 | - name: username 487 | in: query 488 | description: The user name for login 489 | required: false 490 | schema: 491 | type: string 492 | - name: password 493 | in: query 494 | description: The password for login in clear text 495 | required: false 496 | schema: 497 | type: string 498 | responses: 499 | '200': 500 | description: successful operation 501 | headers: 502 | X-Rate-Limit: 503 | description: calls per hour allowed by the user 504 | schema: 505 | type: integer 506 | format: int32 507 | X-Expires-After: 508 | description: date in UTC when token expires 509 | schema: 510 | type: string 511 | format: date-time 512 | content: 513 | application/xml: 514 | schema: 515 | type: string 516 | application/json: 517 | schema: 518 | type: string 519 | '400': 520 | description: Invalid username/password supplied 521 | /user/logout: 522 | get: 523 | tags: 524 | - user 525 | summary: Logs out current logged in user session 526 | description: '' 527 | operationId: logoutUser 528 | parameters: [] 529 | responses: 530 | default: 531 | description: successful operation 532 | '/user/{username}': 533 | get: 534 | tags: 535 | - user 536 | summary: Get user by user name 537 | description: '' 538 | operationId: getUserByName 539 | parameters: 540 | - name: username 541 | in: path 542 | description: 'The name that needs to be fetched. Use user1 for testing. ' 543 | required: true 544 | schema: 545 | type: string 546 | responses: 547 | '200': 548 | description: successful operation 549 | content: 550 | application/xml: 551 | schema: 552 | $ref: '#/components/schemas/User' 553 | application/json: 554 | schema: 555 | $ref: '#/components/schemas/User' 556 | '400': 557 | description: Invalid username supplied 558 | '404': 559 | description: User not found 560 | put: 561 | tags: 562 | - user 563 | summary: Update user 564 | x-swagger-router-controller: UserController 565 | description: This can only be done by the logged in user. 566 | operationId: updateUser 567 | parameters: 568 | - name: username 569 | in: path 570 | description: name that needs to be updated 571 | required: true 572 | schema: 573 | type: string 574 | responses: 575 | default: 576 | description: successful operation 577 | requestBody: 578 | description: Update an existent user in the store 579 | content: 580 | application/json: 581 | schema: 582 | $ref: '#/components/schemas/User' 583 | application/xml: 584 | schema: 585 | $ref: '#/components/schemas/User' 586 | application/x-www-form-urlencoded: 587 | schema: 588 | $ref: '#/components/schemas/User' 589 | delete: 590 | tags: 591 | - user 592 | summary: Delete user 593 | description: This can only be done by the logged in user. 594 | operationId: deleteUser 595 | parameters: 596 | - name: username 597 | in: path 598 | description: The name that needs to be deleted 599 | required: true 600 | schema: 601 | type: string 602 | responses: 603 | '400': 604 | description: Invalid username supplied 605 | '404': 606 | description: User not found 607 | externalDocs: 608 | description: Find out more about Swagger 609 | url: 'http://swagger.io' 610 | components: 611 | schemas: 612 | Order: 613 | x-swagger-router-model: io.swagger.petstore.model.Order 614 | properties: 615 | id: 616 | type: integer 617 | format: int64 618 | example: 10 619 | petId: 620 | type: integer 621 | format: int64 622 | example: 198772 623 | quantity: 624 | type: integer 625 | format: int32 626 | example: 7 627 | shipDate: 628 | type: string 629 | format: date-time 630 | status: 631 | type: string 632 | description: Order Status 633 | enum: 634 | - placed 635 | - approved 636 | - delivered 637 | example: approved 638 | complete: 639 | type: boolean 640 | xml: 641 | name: order 642 | type: object 643 | Customer: 644 | properties: 645 | id: 646 | type: integer 647 | format: int64 648 | example: 100000 649 | username: 650 | type: string 651 | example: fehguy 652 | address: 653 | type: array 654 | items: 655 | $ref: '#/components/schemas/Address' 656 | xml: 657 | wrapped: true 658 | name: addresses 659 | xml: 660 | name: customer 661 | type: object 662 | Address: 663 | properties: 664 | street: 665 | type: string 666 | example: 437 Lytton 667 | city: 668 | type: string 669 | example: Palo Alto 670 | state: 671 | type: string 672 | example: CA 673 | zip: 674 | type: string 675 | example: 94301 676 | xml: 677 | name: address 678 | type: object 679 | Category: 680 | x-swagger-router-model: io.swagger.petstore.model.Category 681 | properties: 682 | id: 683 | type: integer 684 | format: int64 685 | example: 1 686 | name: 687 | type: string 688 | example: Dogs 689 | xml: 690 | name: category 691 | type: object 692 | User: 693 | x-swagger-router-model: io.swagger.petstore.model.User 694 | properties: 695 | id: 696 | type: integer 697 | format: int64 698 | example: 10 699 | username: 700 | type: string 701 | example: theUser 702 | firstName: 703 | type: string 704 | example: John 705 | lastName: 706 | type: string 707 | example: James 708 | email: 709 | type: string 710 | example: john@email.com 711 | password: 712 | type: string 713 | example: 12345 714 | phone: 715 | type: string 716 | example: 12345 717 | userStatus: 718 | type: integer 719 | format: int32 720 | example: 1 721 | description: User Status 722 | xml: 723 | name: user 724 | type: object 725 | Tag: 726 | x-swagger-router-model: io.swagger.petstore.model.Tag 727 | properties: 728 | id: 729 | type: integer 730 | format: int64 731 | name: 732 | type: string 733 | xml: 734 | name: tag 735 | type: object 736 | Pet: 737 | x-swagger-router-model: io.swagger.petstore.model.Pet 738 | required: 739 | - name 740 | - photoUrls 741 | properties: 742 | id: 743 | type: integer 744 | format: int64 745 | example: 10 746 | name: 747 | type: string 748 | example: doggie 749 | category: 750 | $ref: '#/components/schemas/Category' 751 | photoUrls: 752 | type: array 753 | xml: 754 | wrapped: true 755 | items: 756 | type: string 757 | xml: 758 | name: photoUrl 759 | tags: 760 | type: array 761 | xml: 762 | wrapped: true 763 | items: 764 | $ref: '#/components/schemas/Tag' 765 | xml: 766 | name: tag 767 | status: 768 | type: string 769 | description: pet status in the store 770 | enum: 771 | - available 772 | - pending 773 | - sold 774 | xml: 775 | name: pet 776 | type: object 777 | ApiResponse: 778 | properties: 779 | code: 780 | type: integer 781 | format: int32 782 | type: 783 | type: string 784 | message: 785 | type: string 786 | xml: 787 | name: '##default' 788 | type: object 789 | requestBodies: 790 | Pet: 791 | content: 792 | application/json: 793 | schema: 794 | $ref: '#/components/schemas/Pet' 795 | application/xml: 796 | schema: 797 | $ref: '#/components/schemas/Pet' 798 | description: Pet object that needs to be added to the store 799 | UserArray: 800 | content: 801 | application/json: 802 | schema: 803 | type: array 804 | items: 805 | $ref: '#/components/schemas/User' 806 | description: List of user object 807 | securitySchemes: 808 | petstore_auth: 809 | type: oauth2 810 | flows: 811 | implicit: 812 | authorizationUrl: 'https://petstore.swagger.io/oauth/authorize' 813 | scopes: 814 | 'write:pets': modify pets in your account 815 | 'read:pets': read your pets 816 | api_key: 817 | type: apiKey 818 | name: api_key 819 | in: header 820 | -------------------------------------------------------------------------------- /plugins/importer-openapi/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { describe, expect, test } from 'vitest'; 4 | import { convertOpenApi } from '../src'; 5 | 6 | describe('importer-openapi', () => { 7 | const p = path.join(__dirname, 'fixtures'); 8 | const fixtures = fs.readdirSync(p); 9 | 10 | test('Skips invalid file', async () => { 11 | const imported = await convertOpenApi('{}'); 12 | expect(imported).toBeUndefined(); 13 | }); 14 | 15 | for (const fixture of fixtures) { 16 | test('Imports ' + fixture, async () => { 17 | const contents = fs.readFileSync(path.join(p, fixture), 'utf-8'); 18 | const imported = await convertOpenApi(contents); 19 | expect(imported?.resources.workspaces).toEqual([ 20 | expect.objectContaining({ 21 | name: 'Swagger Petstore - OpenAPI 3.0', 22 | description: expect.stringContaining('This is a sample Pet Store Server'), 23 | }), 24 | ]); 25 | expect(imported?.resources.httpRequests.length).toBe(19); 26 | expect(imported?.resources.folders.length).toBe(7); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /plugins/importer-postman/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/importer-postman", 3 | "private": true, 4 | "version": "0.0.1", 5 | "main": "./build/index.js", 6 | "scripts": { 7 | "build": "yaakcli build ./src/index.js", 8 | "dev": "yaakcli dev ./src/index.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /plugins/importer-postman/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | Environment, 4 | Folder, 5 | HttpRequest, 6 | HttpRequestHeader, 7 | HttpUrlParameter, 8 | PluginDefinition, 9 | Workspace, 10 | } from '@yaakapp/api'; 11 | 12 | const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'; 13 | const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json'; 14 | const VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA]; 15 | 16 | type AtLeast = Partial & Pick; 17 | 18 | interface ExportResources { 19 | workspaces: AtLeast[]; 20 | environments: AtLeast[]; 21 | httpRequests: AtLeast[]; 22 | folders: AtLeast[]; 23 | } 24 | 25 | export const plugin: PluginDefinition = { 26 | importer: { 27 | name: 'Postman', 28 | description: 'Import postman collections', 29 | onImport(_ctx: Context, args: { text: string }) { 30 | return convertPostman(args.text) as any; 31 | }, 32 | }, 33 | }; 34 | 35 | export function convertPostman( 36 | contents: string, 37 | ): { resources: ExportResources } | undefined { 38 | const root = parseJSONToRecord(contents); 39 | if (root == null) return; 40 | 41 | const info = toRecord(root.info); 42 | const isValidSchema = VALID_SCHEMAS.includes(info.schema); 43 | if (!isValidSchema || !Array.isArray(root.item)) { 44 | return; 45 | } 46 | 47 | const globalAuth = importAuth(root.auth); 48 | 49 | const exportResources: ExportResources = { 50 | workspaces: [], 51 | environments: [], 52 | httpRequests: [], 53 | folders: [], 54 | }; 55 | 56 | const workspace: ExportResources['workspaces'][0] = { 57 | model: 'workspace', 58 | id: generateId('workspace'), 59 | name: info.name || 'Postman Import', 60 | description: info.description?.content ?? info.description, 61 | }; 62 | exportResources.workspaces.push(workspace); 63 | 64 | // Create the base environment 65 | const environment: ExportResources['environments'][0] = { 66 | model: 'environment', 67 | id: generateId('environment'), 68 | name: 'Global Variables', 69 | workspaceId: workspace.id, 70 | variables: 71 | root.variable?.map((v: any) => ({ 72 | name: v.key, 73 | value: v.value, 74 | })) ?? [], 75 | }; 76 | exportResources.environments.push(environment); 77 | 78 | const importItem = (v: Record, folderId: string | null = null) => { 79 | if (typeof v.name === 'string' && Array.isArray(v.item)) { 80 | const folder: ExportResources['folders'][0] = { 81 | model: 'folder', 82 | workspaceId: workspace.id, 83 | id: generateId('folder'), 84 | name: v.name, 85 | folderId, 86 | }; 87 | exportResources.folders.push(folder); 88 | for (const child of v.item) { 89 | importItem(child, folder.id); 90 | } 91 | } else if (typeof v.name === 'string' && 'request' in v) { 92 | const r = toRecord(v.request); 93 | const bodyPatch = importBody(r.body); 94 | const requestAuthPath = importAuth(r.auth); 95 | const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath; 96 | 97 | const headers: HttpRequestHeader[] = toArray(r.header).map((h) => { 98 | return { 99 | name: h.key, 100 | value: h.value, 101 | enabled: !h.disabled, 102 | }; 103 | }); 104 | 105 | // Add body headers only if they don't already exist 106 | for (const bodyPatchHeader of bodyPatch.headers) { 107 | const existingHeader = headers.find(h => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase()); 108 | if (existingHeader) { 109 | continue; 110 | } 111 | headers.push(bodyPatchHeader); 112 | } 113 | 114 | const { url, urlParameters } = convertUrl(r.url); 115 | 116 | const request: ExportResources['httpRequests'][0] = { 117 | model: 'http_request', 118 | id: generateId('http_request'), 119 | workspaceId: workspace.id, 120 | folderId, 121 | name: v.name, 122 | description: v.description || undefined, 123 | method: r.method || 'GET', 124 | url, 125 | urlParameters, 126 | body: bodyPatch.body, 127 | bodyType: bodyPatch.bodyType, 128 | authentication: authPatch.authentication, 129 | authenticationType: authPatch.authenticationType, 130 | headers, 131 | }; 132 | exportResources.httpRequests.push(request); 133 | } else { 134 | console.log('Unknown item', v, folderId); 135 | } 136 | }; 137 | 138 | for (const item of root.item) { 139 | importItem(item); 140 | } 141 | 142 | const resources = deleteUndefinedAttrs(convertTemplateSyntax(exportResources)); 143 | 144 | return { resources }; 145 | } 146 | 147 | function convertUrl(url: string | any): Pick { 148 | if (typeof url === 'string') { 149 | return { url, urlParameters: [] }; 150 | } 151 | 152 | url = toRecord(url); 153 | 154 | let v = ''; 155 | 156 | if ('protocol' in url && typeof url.protocol === 'string') { 157 | v += `${url.protocol}://`; 158 | } 159 | 160 | if ('host' in url) { 161 | v += `${Array.isArray(url.host) ? url.host.join('.') : url.host}`; 162 | } 163 | 164 | if ('port' in url && typeof url.port === 'string') { 165 | v += `:${url.port}`; 166 | } 167 | 168 | if ('path' in url && Array.isArray(url.path) && url.path.length > 0) { 169 | v += `/${Array.isArray(url.path) ? url.path.join('/') : url.path}`; 170 | } 171 | 172 | const params: HttpUrlParameter[] = []; 173 | if ('query' in url && Array.isArray(url.query) && url.query.length > 0) { 174 | for (const query of url.query) { 175 | params.push({ 176 | name: query.key ?? '', 177 | value: query.value ?? '', 178 | enabled: !query.disabled, 179 | }); 180 | } 181 | } 182 | 183 | if ('variable' in url && Array.isArray(url.variable) && url.variable.length > 0) { 184 | for (const v of url.variable) { 185 | params.push({ 186 | name: ':' + (v.key ?? ''), 187 | value: v.value ?? '', 188 | enabled: !v.disabled, 189 | }); 190 | } 191 | } 192 | 193 | if ('hash' in url && typeof url.hash === 'string') { 194 | v += `#${url.hash}`; 195 | } 196 | 197 | // TODO: Implement url.variables (path variables) 198 | 199 | return { url: v, urlParameters: params }; 200 | } 201 | 202 | function importAuth( 203 | rawAuth: any, 204 | ): Pick { 205 | const auth = toRecord(rawAuth); 206 | if ('basic' in auth) { 207 | return { 208 | authenticationType: 'basic', 209 | authentication: { 210 | username: auth.basic.username || '', 211 | password: auth.basic.password || '', 212 | }, 213 | }; 214 | } else if ('bearer' in auth) { 215 | return { 216 | authenticationType: 'bearer', 217 | authentication: { 218 | token: auth.bearer.token || '', 219 | }, 220 | }; 221 | } else { 222 | return { authenticationType: null, authentication: {} }; 223 | } 224 | } 225 | 226 | function importBody(rawBody: any): Pick { 227 | const body = toRecord(rawBody); 228 | if (body.mode === 'graphql') { 229 | return { 230 | headers: [ 231 | { 232 | name: 'Content-Type', 233 | value: 'application/json', 234 | enabled: true, 235 | }, 236 | ], 237 | bodyType: 'graphql', 238 | body: { 239 | text: JSON.stringify( 240 | { query: body.graphql.query, variables: parseJSONToRecord(body.graphql.variables) }, 241 | null, 242 | 2, 243 | ), 244 | }, 245 | }; 246 | } else if (body.mode === 'urlencoded') { 247 | return { 248 | headers: [ 249 | { 250 | name: 'Content-Type', 251 | value: 'application/x-www-form-urlencoded', 252 | enabled: true, 253 | }, 254 | ], 255 | bodyType: 'application/x-www-form-urlencoded', 256 | body: { 257 | form: toArray(body.urlencoded).map((f) => ({ 258 | enabled: !f.disabled, 259 | name: f.key ?? '', 260 | value: f.value ?? '', 261 | })), 262 | }, 263 | }; 264 | } else if (body.mode === 'formdata') { 265 | return { 266 | headers: [ 267 | { 268 | name: 'Content-Type', 269 | value: 'multipart/form-data', 270 | enabled: true, 271 | }, 272 | ], 273 | bodyType: 'multipart/form-data', 274 | body: { 275 | form: toArray(body.formdata).map((f) => 276 | f.src != null 277 | ? { 278 | enabled: !f.disabled, 279 | contentType: f.contentType ?? null, 280 | name: f.key ?? '', 281 | file: f.src ?? '', 282 | } 283 | : { 284 | enabled: !f.disabled, 285 | name: f.key ?? '', 286 | value: f.value ?? '', 287 | }, 288 | ), 289 | }, 290 | }; 291 | } else if (body.mode === 'raw') { 292 | return { 293 | headers: [ 294 | { 295 | name: 'Content-Type', 296 | value: body.options?.raw?.language === 'json' ? 'application/json' : '', 297 | enabled: true, 298 | }, 299 | ], 300 | bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other', 301 | body: { 302 | text: body.raw ?? '', 303 | }, 304 | }; 305 | } else if (body.mode === 'file') { 306 | return { 307 | headers: [], 308 | bodyType: 'binary', 309 | body: { 310 | filePath: body.file?.src, 311 | }, 312 | }; 313 | } else { 314 | return { headers: [], bodyType: null, body: {} }; 315 | } 316 | } 317 | 318 | function parseJSONToRecord(jsonStr: string): Record | null { 319 | try { 320 | return toRecord(JSON.parse(jsonStr)); 321 | } catch (err) { 322 | } 323 | return null; 324 | } 325 | 326 | function toRecord(value: any): Record { 327 | if (Object.prototype.toString.call(value) === '[object Object]') return value; 328 | else return {}; 329 | } 330 | 331 | function toArray(value: any): any[] { 332 | if (Object.prototype.toString.call(value) === '[object Array]') return value; 333 | else return []; 334 | } 335 | 336 | /** Recursively render all nested object properties */ 337 | function convertTemplateSyntax(obj: T): T { 338 | if (typeof obj === 'string') { 339 | return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T; 340 | } else if (Array.isArray(obj) && obj != null) { 341 | return obj.map(convertTemplateSyntax) as T; 342 | } else if (typeof obj === 'object' && obj != null) { 343 | return Object.fromEntries( 344 | Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]), 345 | ) as T; 346 | } else { 347 | return obj; 348 | } 349 | } 350 | 351 | function deleteUndefinedAttrs(obj: T): T { 352 | if (Array.isArray(obj) && obj != null) { 353 | return obj.map(deleteUndefinedAttrs) as T; 354 | } else if (typeof obj === 'object' && obj != null) { 355 | return Object.fromEntries( 356 | Object.entries(obj) 357 | .filter(([, v]) => v !== undefined) 358 | .map(([k, v]) => [k, deleteUndefinedAttrs(v)]), 359 | ) as T; 360 | } else { 361 | return obj; 362 | } 363 | } 364 | 365 | const idCount: Partial> = {}; 366 | 367 | function generateId(model: string): string { 368 | idCount[model] = (idCount[model] ?? -1) + 1; 369 | return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`; 370 | } 371 | -------------------------------------------------------------------------------- /plugins/importer-postman/tests/fixtures/nested.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d", 4 | "name": "New Collection", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", 6 | "_exporter_id": "18798" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Top Folder", 11 | "item": [ 12 | { 13 | "name": "Nested Folder", 14 | "item": [ 15 | { 16 | "name": "Request 1", 17 | "request": { 18 | "method": "GET" 19 | } 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "Request 2", 25 | "request": { 26 | "method": "GET" 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "Request 3", 33 | "request": { 34 | "method": "GET" 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /plugins/importer-postman/tests/fixtures/nested.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "workspaces": [ 4 | { 5 | "model": "workspace", 6 | "id": "GENERATE_ID::WORKSPACE_0", 7 | "name": "New Collection" 8 | } 9 | ], 10 | "environments": [ 11 | { 12 | "id": "GENERATE_ID::ENVIRONMENT_0", 13 | "model": "environment", 14 | "name": "Global Variables", 15 | "variables": [], 16 | "workspaceId": "GENERATE_ID::WORKSPACE_0" 17 | } 18 | ], 19 | "httpRequests": [ 20 | { 21 | "model": "http_request", 22 | "id": "GENERATE_ID::HTTP_REQUEST_0", 23 | "workspaceId": "GENERATE_ID::WORKSPACE_0", 24 | "folderId": "GENERATE_ID::FOLDER_1", 25 | "name": "Request 1", 26 | "method": "GET", 27 | "url": "", 28 | "urlParameters": [], 29 | "body": {}, 30 | "bodyType": null, 31 | "authentication": {}, 32 | "authenticationType": null, 33 | "headers": [] 34 | }, 35 | { 36 | "model": "http_request", 37 | "id": "GENERATE_ID::HTTP_REQUEST_1", 38 | "workspaceId": "GENERATE_ID::WORKSPACE_0", 39 | "folderId": "GENERATE_ID::FOLDER_0", 40 | "name": "Request 2", 41 | "method": "GET", 42 | "url": "", 43 | "urlParameters": [], 44 | "body": {}, 45 | "bodyType": null, 46 | "authentication": {}, 47 | "authenticationType": null, 48 | "headers": [] 49 | }, 50 | { 51 | "model": "http_request", 52 | "id": "GENERATE_ID::HTTP_REQUEST_2", 53 | "workspaceId": "GENERATE_ID::WORKSPACE_0", 54 | "folderId": null, 55 | "name": "Request 3", 56 | "method": "GET", 57 | "url": "", 58 | "urlParameters": [], 59 | "body": {}, 60 | "bodyType": null, 61 | "authentication": {}, 62 | "authenticationType": null, 63 | "headers": [] 64 | } 65 | ], 66 | "folders": [ 67 | { 68 | "model": "folder", 69 | "workspaceId": "GENERATE_ID::WORKSPACE_0", 70 | "id": "GENERATE_ID::FOLDER_0", 71 | "name": "Top Folder", 72 | "folderId": null 73 | }, 74 | { 75 | "model": "folder", 76 | "workspaceId": "GENERATE_ID::WORKSPACE_0", 77 | "id": "GENERATE_ID::FOLDER_1", 78 | "name": "Nested Folder", 79 | "folderId": "GENERATE_ID::FOLDER_0" 80 | } 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugins/importer-postman/tests/fixtures/params.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d", 4 | "name": "New Collection", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "18798" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Form URL", 11 | "request": { 12 | "auth": { 13 | "type": "bearer", 14 | "bearer": [ 15 | { 16 | "key": "token", 17 | "value": "baeare", 18 | "type": "string" 19 | } 20 | ] 21 | }, 22 | "method": "POST", 23 | "header": [ 24 | { 25 | "key": "X-foo", 26 | "value": "bar", 27 | "description": "description" 28 | }, 29 | { 30 | "key": "Disabled", 31 | "value": "tnroant", 32 | "description": "ntisorantosra", 33 | "disabled": true 34 | } 35 | ], 36 | "body": { 37 | "mode": "formdata", 38 | "formdata": [ 39 | { 40 | "key": "Key", 41 | "contentType": "Custom/COntent", 42 | "description": "DEscription", 43 | "type": "file", 44 | "src": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11 PM.png" 45 | } 46 | ] 47 | }, 48 | "url": { 49 | "raw": "example.com/:foo/:bar?q=qqq&", 50 | "host": [ 51 | "example", 52 | "com" 53 | ], 54 | "path": [ 55 | ":foo", 56 | ":bar" 57 | ], 58 | "query": [ 59 | { 60 | "key": "disabled", 61 | "value": "secondvalue", 62 | "description": "this is disabled", 63 | "disabled": true 64 | }, 65 | { 66 | "key": "q", 67 | "value": "qqq", 68 | "description": "hello" 69 | }, 70 | { 71 | "key": "", 72 | "value": null 73 | } 74 | ], 75 | "variable": [ 76 | { 77 | "key": "foo", 78 | "value": "fff", 79 | "description": "Description" 80 | }, 81 | { 82 | "key": "bar", 83 | "value": "bbb", 84 | "description": "bbb description" 85 | } 86 | ] 87 | } 88 | }, 89 | "response": [] 90 | } 91 | ], 92 | "auth": { 93 | "type": "basic", 94 | "basic": [ 95 | { 96 | "key": "password", 97 | "value": "globalpass", 98 | "type": "string" 99 | }, 100 | { 101 | "key": "username", 102 | "value": "globaluser", 103 | "type": "string" 104 | } 105 | ] 106 | }, 107 | "event": [ 108 | { 109 | "listen": "prerequest", 110 | "script": { 111 | "type": "text/javascript", 112 | "packages": {}, 113 | "exec": [ 114 | "" 115 | ] 116 | } 117 | }, 118 | { 119 | "listen": "test", 120 | "script": { 121 | "type": "text/javascript", 122 | "packages": {}, 123 | "exec": [ 124 | "" 125 | ] 126 | } 127 | } 128 | ], 129 | "variable": [ 130 | { 131 | "key": "COLLECTION VARIABLE", 132 | "value": "collection variable", 133 | "type": "string" 134 | } 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /plugins/importer-postman/tests/fixtures/params.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "workspaces": [ 4 | { 5 | "model": "workspace", 6 | "id": "GENERATE_ID::WORKSPACE_1", 7 | "name": "New Collection" 8 | } 9 | ], 10 | "environments": [ 11 | { 12 | "id": "GENERATE_ID::ENVIRONMENT_1", 13 | "workspaceId": "GENERATE_ID::WORKSPACE_1", 14 | "model": "environment", 15 | "name": "Global Variables", 16 | "variables": [ 17 | { 18 | "name": "COLLECTION VARIABLE", 19 | "value": "collection variable" 20 | } 21 | ] 22 | } 23 | ], 24 | "httpRequests": [ 25 | { 26 | "model": "http_request", 27 | "id": "GENERATE_ID::HTTP_REQUEST_3", 28 | "workspaceId": "GENERATE_ID::WORKSPACE_1", 29 | "folderId": null, 30 | "name": "Form URL", 31 | "method": "POST", 32 | "url": "example.com/:foo/:bar", 33 | "urlParameters": [ 34 | { 35 | "name": "disabled", 36 | "value": "secondvalue", 37 | "enabled": false 38 | }, 39 | { 40 | "name": "q", 41 | "value": "qqq", 42 | "enabled": true 43 | }, 44 | { 45 | "name": "", 46 | "value": "", 47 | "enabled": true 48 | }, 49 | { 50 | "name": ":foo", 51 | "value": "fff", 52 | "enabled": true 53 | }, 54 | { 55 | "name": ":bar", 56 | "value": "bbb", 57 | "enabled": true 58 | } 59 | ], 60 | "body": { 61 | "form": [ 62 | { 63 | "enabled": true, 64 | "contentType": "Custom/COntent", 65 | "name": "Key", 66 | "file": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11 PM.png" 67 | } 68 | ] 69 | }, 70 | "bodyType": "multipart/form-data", 71 | "authentication": { 72 | "token": "" 73 | }, 74 | "authenticationType": "bearer", 75 | "headers": [ 76 | { 77 | "name": "X-foo", 78 | "value": "bar", 79 | "enabled": true 80 | }, 81 | { 82 | "name": "Disabled", 83 | "value": "tnroant", 84 | "enabled": false 85 | }, 86 | { 87 | "name": "Content-Type", 88 | "value": "multipart/form-data", 89 | "enabled": true 90 | } 91 | ] 92 | } 93 | ], 94 | "folders": [] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /plugins/importer-postman/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { describe, expect, test } from 'vitest'; 4 | import { convertPostman } from '../src'; 5 | 6 | describe('importer-postman', () => { 7 | const p = path.join(__dirname, 'fixtures'); 8 | const fixtures = fs.readdirSync(p); 9 | 10 | for (const fixture of fixtures) { 11 | if (fixture.includes('.output')) { 12 | continue; 13 | } 14 | 15 | test('Imports ' + fixture, () => { 16 | const contents = fs.readFileSync(path.join(p, fixture), 'utf-8'); 17 | const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8'); 18 | const result = convertPostman(contents); 19 | // console.log(JSON.stringify(result, null, 2)) 20 | expect(result).toEqual(JSON.parse(expected)); 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /plugins/importer-yaak/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/importer-yaak", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.js", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/importer-yaak/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Environment, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | importer: { 5 | name: 'Yaak', 6 | description: 'Yaak official format', 7 | onImport(_ctx, args) { 8 | return migrateImport(args.text) as any; 9 | }, 10 | }, 11 | }; 12 | 13 | export function migrateImport(contents: string) { 14 | let parsed; 15 | try { 16 | parsed = JSON.parse(contents); 17 | } catch (err) { 18 | return undefined; 19 | } 20 | 21 | if (!isJSObject(parsed)) { 22 | return undefined; 23 | } 24 | 25 | const isYaakExport = 'yaakSchema' in parsed; 26 | if (!isYaakExport) { 27 | return; 28 | } 29 | 30 | // Migrate v1 to v2 -- changes requests to httpRequests 31 | if ('requests' in parsed.resources) { 32 | parsed.resources.httpRequests = parsed.resources.requests; 33 | delete parsed.resources['requests']; 34 | } 35 | 36 | // Migrate v2 to v3 37 | for (const workspace of parsed.resources.workspaces ?? []) { 38 | if ('variables' in workspace) { 39 | // Create the base environment 40 | const baseEnvironment: Partial = { 41 | id: `GENERATE_ID::base_env_${workspace['id']}`, 42 | name: 'Global Variables', 43 | variables: workspace.variables, 44 | workspaceId: workspace.id, 45 | }; 46 | parsed.resources.environments = parsed.resources.environments ?? []; 47 | parsed.resources.environments.push(baseEnvironment); 48 | 49 | // Delete variables key from the workspace 50 | delete workspace.variables; 51 | 52 | // Add environmentId to relevant environments 53 | for (const environment of parsed.resources.environments) { 54 | if (environment.workspaceId === workspace.id && environment.id !== baseEnvironment.id) { 55 | environment.environmentId = baseEnvironment.id; 56 | } 57 | } 58 | } 59 | } 60 | 61 | // Migrate v3 to v4 62 | for (const environment of parsed.resources.environments ?? []) { 63 | if ('environmentId' in environment) { 64 | environment.base = environment.environmentId == null; 65 | delete environment.environmentId; 66 | } 67 | } 68 | 69 | return { resources: parsed.resources }; // Should already be in the correct format 70 | } 71 | 72 | function isJSObject(obj: any) { 73 | return Object.prototype.toString.call(obj) === '[object Object]'; 74 | } 75 | -------------------------------------------------------------------------------- /plugins/importer-yaak/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { migrateImport } from '../src'; 3 | 4 | describe('importer-yaak', () => { 5 | test('Skips invalid imports', () => { 6 | expect(migrateImport('not JSON')).toBeUndefined(); 7 | expect(migrateImport('[]')).toBeUndefined(); 8 | expect(migrateImport(JSON.stringify({ resources: {} }))).toBeUndefined(); 9 | }); 10 | 11 | test('converts schema 1 to 2', () => { 12 | const imported = migrateImport( 13 | JSON.stringify({ 14 | yaakSchema: 1, 15 | resources: { 16 | requests: [], 17 | }, 18 | }), 19 | ); 20 | 21 | expect(imported).toEqual( 22 | expect.objectContaining({ 23 | resources: { 24 | httpRequests: [], 25 | }, 26 | }), 27 | ); 28 | }); 29 | test('converts schema 2 to 3', () => { 30 | const imported = migrateImport( 31 | JSON.stringify({ 32 | yaakSchema: 2, 33 | resources: { 34 | environments: [{ 35 | id: 'e_1', 36 | workspaceId: 'w_1', 37 | name: 'Production', 38 | variables: [{ name: 'E1', value: 'E1!' }], 39 | }], 40 | workspaces: [{ 41 | id: 'w_1', 42 | variables: [{ name: 'W1', value: 'W1!' }], 43 | }], 44 | }, 45 | }), 46 | ); 47 | 48 | expect(imported).toEqual( 49 | expect.objectContaining({ 50 | resources: { 51 | workspaces: [{ 52 | id: 'w_1', 53 | }], 54 | environments: [{ 55 | id: 'e_1', 56 | base: false, 57 | workspaceId: 'w_1', 58 | name: 'Production', 59 | variables: [{ name: 'E1', value: 'E1!' }], 60 | }, { 61 | id: 'GENERATE_ID::base_env_w_1', 62 | workspaceId: 'w_1', 63 | name: 'Global Variables', 64 | variables: [{ name: 'W1', value: 'W1!' }], 65 | }], 66 | }, 67 | }), 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /plugins/template-function-cookie/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-cookie", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-cookie/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | templateFunctions: [ 5 | { 6 | name: 'cookie.value', 7 | description: 'Read the value of a cookie in the jar, by name', 8 | args: [ 9 | { 10 | type: 'text', 11 | name: 'cookie_name', 12 | label: 'Cookie Name', 13 | }, 14 | ], 15 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 16 | return ctx.cookies.getValue({ name: String(args.values.cookie_name) }); 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /plugins/template-function-encode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-encode", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-encode/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | templateFunctions: [ 5 | { 6 | name: 'base64.encode', 7 | description: 'Encode a value to base64', 8 | args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }], 9 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 10 | return Buffer.from(args.values.value ?? '').toString('base64'); 11 | }, 12 | }, 13 | { 14 | name: 'base64.decode', 15 | description: 'Decode a value from base64', 16 | args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }], 17 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 18 | return Buffer.from(args.values.value ?? '', 'base64').toString('utf-8'); 19 | }, 20 | }, 21 | { 22 | name: 'url.encode', 23 | description: 'Encode a value for use in a URL (percent-encoding)', 24 | args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }], 25 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 26 | return encodeURIComponent(args.values.value ?? ''); 27 | }, 28 | }, 29 | { 30 | name: 'url.decode', 31 | description: 'Decode a percent-encoded URL value', 32 | args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }], 33 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 34 | try { 35 | return decodeURIComponent(args.values.value ?? ''); 36 | } catch { 37 | return ''; 38 | } 39 | }, 40 | }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /plugins/template-function-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-fs", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-fs/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | import fs from 'node:fs'; 3 | 4 | export const plugin: PluginDefinition = { 5 | templateFunctions: [{ 6 | name: 'fs.readFile', 7 | description: 'Read the contents of a file as utf-8', 8 | args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }], 9 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 10 | if (!args.values.path) return null; 11 | 12 | try { 13 | return fs.promises.readFile(args.values.path, 'utf-8'); 14 | } catch (err) { 15 | return null; 16 | } 17 | }, 18 | }], 19 | }; 20 | -------------------------------------------------------------------------------- /plugins/template-function-hash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-hash", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-hash/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | import { createHash, createHmac } from 'node:crypto'; 3 | 4 | const algorithms = ['md5', 'sha1', 'sha256', 'sha512'] as const; 5 | const encodings = ['base64', 'hex'] as const; 6 | 7 | type TemplateFunctionPlugin = NonNullable[number]; 8 | 9 | const hashFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({ 10 | name: `hash.${algorithm}`, 11 | description: 'Hash a value to its hexidecimal representation', 12 | args: [ 13 | { 14 | type: 'text', 15 | name: 'input', 16 | label: 'Input', 17 | placeholder: 'input text', 18 | multiLine: true, 19 | }, 20 | { 21 | type: 'select', 22 | name: 'encoding', 23 | label: 'Encoding', 24 | defaultValue: 'base64', 25 | options: encodings.map(encoding => ({ 26 | label: capitalize(encoding), 27 | value: encoding, 28 | })), 29 | }, 30 | ], 31 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 32 | const input = String(args.values.input); 33 | const encoding = String(args.values.encoding) as typeof encodings[number]; 34 | 35 | return createHash(algorithm) 36 | .update(input, 'utf-8') 37 | .digest(encoding); 38 | }, 39 | })); 40 | 41 | const hmacFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({ 42 | name: `hmac.${algorithm}`, 43 | description: 'Compute the HMAC of a value', 44 | args: [ 45 | { 46 | type: 'text', 47 | name: 'input', 48 | label: 'Input', 49 | placeholder: 'input text', 50 | multiLine: true, 51 | }, 52 | { 53 | type: 'text', 54 | name: 'key', 55 | label: 'Key', 56 | password: true, 57 | }, 58 | { 59 | type: 'select', 60 | name: 'encoding', 61 | label: 'Encoding', 62 | defaultValue: 'base64', 63 | options: encodings.map(encoding => ({ 64 | value: encoding, 65 | label: capitalize(encoding), 66 | })), 67 | }, 68 | ], 69 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 70 | const input = String(args.values.input); 71 | const key = String(args.values.key); 72 | const encoding = String(args.values.encoding) as typeof encodings[number]; 73 | 74 | return createHmac(algorithm, key, {}) 75 | .update(input) 76 | .digest(encoding); 77 | }, 78 | })); 79 | 80 | export const plugin: PluginDefinition = { 81 | templateFunctions: [...hashFunctions, ...hmacFunctions], 82 | }; 83 | 84 | function capitalize(str: string): string { 85 | return str.charAt(0).toUpperCase() + str.slice(1); 86 | } 87 | -------------------------------------------------------------------------------- /plugins/template-function-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-json", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "jsonpath-plus": "^10.3.0" 11 | }, 12 | "devDependencies": { 13 | "@types/jsonpath": "^0.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/template-function-json/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | import { JSONPath } from 'jsonpath-plus'; 3 | 4 | export const plugin: PluginDefinition = { 5 | templateFunctions: [ 6 | { 7 | name: 'json.jsonpath', 8 | description: 'Filter JSON-formatted text using JSONPath syntax', 9 | args: [ 10 | { type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: '{ "foo": "bar" }' }, 11 | { type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' }, 12 | { type: 'checkbox', name: 'formatted', label: 'Format Output' }, 13 | ], 14 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 15 | try { 16 | const parsed = JSON.parse(String(args.values.input)); 17 | const query = String(args.values.query ?? '$').trim(); 18 | let filtered = JSONPath({ path: query, json: parsed }); 19 | if (Array.isArray(filtered)) { 20 | filtered = filtered[0]; 21 | } 22 | if (typeof filtered === 'string') { 23 | return filtered; 24 | } 25 | 26 | if (args.values.formatted) { 27 | return JSON.stringify(filtered, null, 2); 28 | } else { 29 | return JSON.stringify(filtered); 30 | } 31 | } catch (e) { 32 | return null; 33 | } 34 | }, 35 | }, 36 | { 37 | name: 'json.escape', 38 | description: 'Escape a JSON string, useful when using the output in JSON values', 39 | args: [ 40 | { type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: 'Hello "World"' }, 41 | ], 42 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 43 | const input = String(args.values.input ?? ''); 44 | return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 45 | }, 46 | }, 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /plugins/template-function-prompt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-prompt", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-prompt/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | templateFunctions: [{ 5 | name: 'prompt.text', 6 | description: 'Prompt the user for input when sending a request', 7 | args: [ 8 | { type: 'text', name: 'title', label: 'Title' }, 9 | { type: 'text', name: 'label', label: 'Label', optional: true }, 10 | { type: 'text', name: 'defaultValue', label: 'Default Value', optional: true }, 11 | { type: 'text', name: 'placeholder', label: 'Placeholder', optional: true }, 12 | ], 13 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 14 | if (args.purpose !== 'send') return null; 15 | 16 | return await ctx.prompt.text({ 17 | id: `prompt-${args.values.label}`, 18 | label: args.values.title ?? '', 19 | title: args.values.title ?? '', 20 | defaultValue: args.values.defaultValue, 21 | placeholder: args.values.placeholder, 22 | }); 23 | }, 24 | }], 25 | }; 26 | -------------------------------------------------------------------------------- /plugins/template-function-regex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-regex", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-regex/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | templateFunctions: [{ 5 | name: 'regex.match', 6 | description: 'Extract', 7 | args: [ 8 | { 9 | type: 'text', 10 | name: 'regex', 11 | label: 'Regular Expression', 12 | placeholder: '^\w+=(?\w*)$', 13 | defaultValue: '^(.*)$', 14 | description: 'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.', 15 | }, 16 | { type: 'text', name: 'input', label: 'Input Text', multiLine: true }, 17 | ], 18 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 19 | if (!args.values.regex) return ''; 20 | 21 | const regex = new RegExp(String(args.values.regex)); 22 | const match = args.values.input?.match(regex); 23 | return match?.groups 24 | ? Object.values(match.groups)[0] ?? '' 25 | : match?.[1] ?? match?.[0] ?? ''; 26 | }, 27 | }], 28 | }; 29 | -------------------------------------------------------------------------------- /plugins/template-function-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-request", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/template-function-request/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | 3 | export const plugin: PluginDefinition = { 4 | templateFunctions: [ 5 | { 6 | name: 'request.body', 7 | args: [{ 8 | name: 'requestId', 9 | label: 'Http Request', 10 | type: 'http_request', 11 | }], 12 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 13 | const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' }); 14 | if (httpRequest == null) return null; 15 | return String(await ctx.templates.render({ 16 | data: httpRequest.body?.text ?? '', 17 | purpose: args.purpose, 18 | })); 19 | }, 20 | }, 21 | { 22 | name: 'request.header', 23 | args: [ 24 | { 25 | name: 'requestId', 26 | label: 'Http Request', 27 | type: 'http_request', 28 | }, 29 | { 30 | name: 'header', 31 | label: 'Header Name', 32 | type: 'text', 33 | }], 34 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 35 | const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' }); 36 | if (httpRequest == null) return null; 37 | const header = httpRequest.headers.find(h => h.name.toLowerCase() === args.values.header?.toLowerCase()); 38 | return String(await ctx.templates.render({ 39 | data: header?.value ?? '', 40 | purpose: args.purpose, 41 | })); 42 | }, 43 | }, 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /plugins/template-function-response/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-response", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "jsonpath-plus": "^9.0.0", 11 | "xpath": "^0.0.34", 12 | "@xmldom/xmldom": "^0.8.10" 13 | }, 14 | "devDependencies": { 15 | "@types/jsonpath": "^0.2.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugins/template-function-response/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom'; 2 | import { 3 | CallTemplateFunctionArgs, 4 | Context, 5 | FormInput, 6 | HttpResponse, 7 | PluginDefinition, 8 | RenderPurpose, 9 | } from '@yaakapp/api'; 10 | import { JSONPath } from 'jsonpath-plus'; 11 | import { readFileSync } from 'node:fs'; 12 | import xpath from 'xpath'; 13 | 14 | const behaviorArg: FormInput = { 15 | type: 'select', 16 | name: 'behavior', 17 | label: 'Sending Behavior', 18 | defaultValue: 'smart', 19 | options: [ 20 | { label: 'When no responses', value: 'smart' }, 21 | { label: 'Always', value: 'always' }, 22 | ], 23 | }; 24 | 25 | const requestArg: FormInput = { 26 | type: 'http_request', 27 | name: 'request', 28 | label: 'Request', 29 | }; 30 | 31 | export const plugin: PluginDefinition = { 32 | templateFunctions: [ 33 | { 34 | name: 'response.header', 35 | description: 'Read the value of a response header, by name', 36 | args: [ 37 | requestArg, 38 | { 39 | type: 'text', 40 | name: 'header', 41 | label: 'Header Name', 42 | placeholder: 'Content-Type', 43 | }, 44 | behaviorArg, 45 | ], 46 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 47 | if (!args.values.request || !args.values.header) return null; 48 | 49 | const response = await getResponse(ctx, { 50 | requestId: args.values.request, 51 | purpose: args.purpose, 52 | behavior: args.values.behavior ?? null, 53 | }); 54 | if (response == null) return null; 55 | 56 | const header = response.headers.find( 57 | h => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(), 58 | ); 59 | return header?.value ?? null; 60 | }, 61 | }, 62 | { 63 | name: 'response.body.path', 64 | description: 'Access a field of the response body using JsonPath or XPath', 65 | aliases: ['response'], 66 | args: [ 67 | requestArg, 68 | { 69 | type: 'text', 70 | name: 'path', 71 | label: 'JSONPath or XPath', 72 | placeholder: '$.books[0].id or /books[0]/id', 73 | }, 74 | behaviorArg, 75 | ], 76 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 77 | if (!args.values.request || !args.values.path) return null; 78 | 79 | const response = await getResponse(ctx, { 80 | requestId: args.values.request, 81 | purpose: args.purpose, 82 | behavior: args.values.behavior ?? null, 83 | }); 84 | if (response == null) return null; 85 | 86 | if (response.bodyPath == null) { 87 | return null; 88 | } 89 | 90 | let body; 91 | try { 92 | body = readFileSync(response.bodyPath, 'utf-8'); 93 | } catch (_) { 94 | return null; 95 | } 96 | 97 | try { 98 | return filterJSONPath(body, args.values.path); 99 | } catch (err) { 100 | // Probably not JSON, try XPath 101 | } 102 | 103 | try { 104 | return filterXPath(body, args.values.path); 105 | } catch (err) { 106 | // Probably not XML 107 | } 108 | 109 | return null; // Bail out 110 | }, 111 | }, 112 | { 113 | name: 'response.body.raw', 114 | description: 'Access the entire response body, as text', 115 | aliases: ['response'], 116 | args: [ 117 | requestArg, 118 | behaviorArg, 119 | ], 120 | async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { 121 | if (!args.values.request) return null; 122 | 123 | const response = await getResponse(ctx, { 124 | requestId: args.values.request, 125 | purpose: args.purpose, 126 | behavior: args.values.behavior ?? null, 127 | }); 128 | if (response == null) return null; 129 | 130 | if (response.bodyPath == null) { 131 | return null; 132 | } 133 | 134 | let body; 135 | try { 136 | body = readFileSync(response.bodyPath, 'utf-8'); 137 | } catch (_) { 138 | return null; 139 | } 140 | 141 | return body; 142 | }, 143 | }, 144 | ], 145 | }; 146 | 147 | function filterJSONPath(body: string, path: string): string { 148 | const parsed = JSON.parse(body); 149 | const items = JSONPath({ path, json: parsed })[0]; 150 | if (items == null) { 151 | return ''; 152 | } 153 | 154 | if ( 155 | Object.prototype.toString.call(items) === '[object Array]' || 156 | Object.prototype.toString.call(items) === '[object Object]' 157 | ) { 158 | return JSON.stringify(items); 159 | } else { 160 | return String(items); 161 | } 162 | } 163 | 164 | function filterXPath(body: string, path: string): string { 165 | const doc = new DOMParser().parseFromString(body, 'text/xml'); 166 | const items = xpath.select(path, doc, false); 167 | 168 | if (Array.isArray(items)) { 169 | return items[0] != null ? String(items[0].firstChild ?? '') : ''; 170 | } else { 171 | // Not sure what cases this happens in (?) 172 | return String(items); 173 | } 174 | } 175 | 176 | async function getResponse(ctx: Context, { requestId, behavior, purpose }: { 177 | requestId: string, 178 | behavior: string | null, 179 | purpose: RenderPurpose, 180 | }): Promise { 181 | if (!requestId) return null; 182 | 183 | const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' }); 184 | if (httpRequest == null) { 185 | return null; 186 | } 187 | 188 | const responses = await ctx.httpResponse.find({ requestId: httpRequest.id, limit: 1 }); 189 | 190 | if (behavior === 'never' && responses.length === 0) { 191 | return null; 192 | } 193 | 194 | let response: HttpResponse | null = responses[0] ?? null; 195 | 196 | // Previews happen a ton, and we don't want to send too many times on "always," so treat 197 | // it as "smart" during preview. 198 | let finalBehavior = (behavior === 'always' && purpose === 'preview') 199 | ? 'smart' 200 | : behavior; 201 | 202 | // Send if no responses and "smart," or "always" 203 | if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') { 204 | // NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...) 205 | const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose }); 206 | response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest }); 207 | } 208 | 209 | return response; 210 | } 211 | -------------------------------------------------------------------------------- /plugins/template-function-uuid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-uuid", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "uuid": "^11.1.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugins/template-function-uuid/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 2 | import { v1, v3, v4, v5, v6, v7 } from 'uuid'; 3 | 4 | export const plugin: PluginDefinition = { 5 | templateFunctions: [ 6 | { 7 | name: 'uuid.v1', 8 | description: 'Generate a UUID V1', 9 | args: [], 10 | async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise { 11 | return v1(); 12 | }, 13 | }, 14 | { 15 | name: 'uuid.v3', 16 | description: 'Generate a UUID V3', 17 | args: [ 18 | { type: 'text', name: 'name', label: 'Name' }, 19 | { 20 | type: 'text', 21 | name: 'namespace', 22 | label: 'Namespace UUID', 23 | description: 'A valid UUID to use as the namespace', 24 | placeholder: '24ced880-3bf4-11f0-8329-cd053d577f0e', 25 | }, 26 | ], 27 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 28 | return v3(String(args.values.name), String(args.values.namespace)); 29 | }, 30 | }, 31 | { 32 | name: 'uuid.v4', 33 | description: 'Generate a UUID V4', 34 | args: [], 35 | async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise { 36 | return v4(); 37 | }, 38 | }, 39 | { 40 | name: 'uuid.v5', 41 | description: 'Generate a UUID V5', 42 | args: [ 43 | { type: 'text', name: 'name', label: 'Name' }, 44 | { type: 'text', name: 'namespace', label: 'Namespace' }, 45 | ], 46 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 47 | return v5(String(args.values.name), String(args.values.namespace)); 48 | }, 49 | }, 50 | { 51 | name: 'uuid.v6', 52 | description: 'Generate a UUID V6', 53 | args: [ 54 | { 55 | type: 'text', 56 | name: 'timestamp', 57 | label: 'Timestamp', 58 | optional: true, 59 | description: 'Can be any format that can be parsed by JavaScript new Date(...)', 60 | placeholder: '2025-05-28T11:15:00Z', 61 | }, 62 | ], 63 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 64 | return v6({ msecs: new Date(String(args.values.timestamp)).getTime() }); 65 | }, 66 | }, 67 | { 68 | name: 'uuid.v7', 69 | description: 'Generate a UUID V7', 70 | args: [], 71 | async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise { 72 | return v7(); 73 | }, 74 | }, 75 | ], 76 | }; 77 | -------------------------------------------------------------------------------- /plugins/template-function-xml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaakapp/template-function-xml", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "yaakcli build ./src/index.ts", 7 | "dev": "yaakcli dev ./src/index.js" 8 | }, 9 | "dependencies": { 10 | "@xmldom/xmldom": "^0.8.10", 11 | "xpath": "^0.0.34" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/template-function-xml/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom'; 2 | import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; 3 | import xpath from 'xpath'; 4 | 5 | export const plugin: PluginDefinition = { 6 | templateFunctions: [{ 7 | name: 'xml.xpath', 8 | description: 'Filter XML-formatted text using XPath syntax', 9 | args: [ 10 | { type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: '' }, 11 | { type: 'text', name: 'query', label: 'Query', placeholder: '//foo' }, 12 | ], 13 | async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise { 14 | try { 15 | const doc = new DOMParser().parseFromString(String(args.values.input), 'text/xml'); 16 | let result = xpath.select(String(args.values.query), doc, false); 17 | if (Array.isArray(result)) { 18 | return String(result.map(c => String(c.firstChild))[0] ?? ''); 19 | } else if (result instanceof Node) { 20 | return String(result.firstChild); 21 | } else { 22 | return String(result); 23 | } 24 | } catch (e) { 25 | return null; 26 | } 27 | }, 28 | }], 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "include": [ 4 | "types", 5 | "plugins" 6 | ], 7 | "compilerOptions": { 8 | "target": "es2021", 9 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 | "useDefineForClassFields": true, 11 | "allowJs": false, 12 | "skipLibCheck": true, 13 | "esModuleInterop": false, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "noUncheckedIndexedAccess": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "react-jsx" 24 | } 25 | } 26 | --------------------------------------------------------------------------------