Compare commits

..

1 Commits

Author SHA1 Message Date
Gregory Schier
64f5d973eb Fix env editor switching 2025-05-23 21:44:34 -07:00
393 changed files with 183808 additions and 20260 deletions

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
.eslintrc.cjs
.prettierrc.cjs
src-web/postcss.config.cjs
src-web/vite.config.ts

49
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,49 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
],
plugins: ['react-refresh'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
},
ignorePatterns: [
'scripts/**/*',
'packages/plugin-runtime/**/*',
'packages/plugin-runtime-types/**/*',
'src-tauri/**/*',
'src-web/tailwind.config.cjs',
'src-web/vite.config.ts',
],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src-web'],
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
'react-refresh/only-export-components': 'error',
'jsx-a11y/no-autofocus': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
},
],
},
};

View File

@@ -3,6 +3,9 @@ on:
push:
tags: [ v* ]
env:
YAAK_PLUGINS_DIR: checkout/plugins
jobs:
build-artifacts:
permissions:
@@ -65,7 +68,9 @@ jobs:
run: cargo install --force trusted-signing-cli --version 0.5.0
- name: Install NPM Dependencies
run: npm ci
run: |
npm ci
npm install @yaakapp/cli
- name: Install Protoc for plugin-runtime
uses: arduino/setup-protoc@v3
@@ -78,6 +83,12 @@ jobs:
- name: Run lint
run: npm run lint
- name: Checkout yaakapp/plugins
uses: actions/checkout@v4
with:
repository: yaakapp/plugins
path: ${{ env.YAAK_PLUGINS_DIR }}
- name: Set version
run: npm run replace-version
env:
@@ -85,6 +96,7 @@ jobs:
- uses: tauri-apps/tauri-action@v0
env:
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}

View File

@@ -34,6 +34,8 @@ Run the `bootstrap` command to do some initial setup:
npm run bootstrap
```
_NOTE: Run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>` to re-build bundled plugins_
## Run the App
After bootstrapping, start the app in development mode:
@@ -42,17 +44,22 @@ After bootstrapping, start the app in development mode:
npm start
```
_NOTE: If working on bundled plugins, run with `YAAK_PLUGINS_DIR=<Path to yaakapp/plugins>`_
## SQLite Migrations
New migrations can be created from the `src-tauri/` directory:
```shell
npm run migration
cd src-tauri
sqlx migrate add migration-name
```
Rerun the app to apply the migrations.
Run the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
## Lezer Grammer Generation

View File

@@ -1,89 +0,0 @@
const { defineConfig, globalIgnores } = require('eslint/config');
const { fixupConfigRules } = require('@eslint/compat');
const reactRefresh = require('eslint-plugin-react-refresh');
const tsParser = require('@typescript-eslint/parser');
const js = require('@eslint/js');
const { FlatCompat } = require('@eslint/eslintrc');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
module.exports = defineConfig([
{
extends: fixupConfigRules(
compat.extends(
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
),
),
plugins: {
'react-refresh': reactRefresh,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['./tsconfig.json'],
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src-web'],
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
'react-refresh/only-export-components': 'error',
'jsx-a11y/no-autofocus': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
},
],
},
},
globalIgnores([
'scripts/**/*',
'packages/plugin-runtime/**/*',
'packages/plugin-runtime-types/**/*',
'src-tauri/**/*',
'src-web/tailwind.config.cjs',
'src-web/vite.config.ts',
]),
globalIgnores([
'**/node_modules/',
'**/dist/',
'**/build/',
'**/.eslintrc.cjs',
'**/.prettierrc.cjs',
'src-web/postcss.config.cjs',
'src-web/vite.config.ts',
]),
]);

5776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,34 +10,8 @@
"packages/plugin-runtime",
"packages/plugin-runtime-types",
"packages/common-lib",
"plugins/auth-basic",
"plugins/auth-bearer",
"plugins/auth-jwt",
"plugins/auth-oauth2",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/filter-jsonpath",
"plugins/filter-xpath",
"plugins/importer-curl",
"plugins/importer-insomnia",
"plugins/importer-openapi",
"plugins/importer-postman",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-encode",
"plugins/template-function-fs",
"plugins/template-function-hash",
"plugins/template-function-json",
"plugins/template-function-prompt",
"plugins/template-function-regex",
"plugins/template-function-request",
"plugins/template-function-response",
"plugins/template-function-uuid",
"plugins/template-function-xml",
"plugins/themes-yaak",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -52,9 +26,7 @@
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
@@ -63,30 +35,24 @@
"replace-version": "node scripts/replace-version.cjs",
"tauri": "tauri",
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
"tauri-before-dev": "workspaces-run --parallel -- npm run --workspaces --if-present dev"
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"dependencies": {
"jotai": "^2.12.2"
},
"devDependencies": {
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@tauri-apps/cli": "2.4.1",
"@tauri-apps/cli": "^2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint": "^8",
"eslint-config-prettier": "^8",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"workspaces-run": "^1.0.2"
"typescript": "^5.8.2"
}
}

View File

@@ -1,28 +0,0 @@
# Yaak Plugin API
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
built using Tauri, Rust, and ReactJS.
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
runtime. This package contains the TypeScript type definitions required to make building
Yaak plugins a breeze.
## Quick Start
The easiest way to get started is by generating a plugin with the Yaak CLI:
```shell
npx @yaakapp/cli generate
```
For more details on creating plugins, check out
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
## Installation
If you prefer starting from scratch, manually install the types package:
```shell
npm install -D @yaakapp/api
```

View File

@@ -1,20 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.6.6",
"keywords": [
"api-client",
"insomnia-alternative",
"bruno-alternative",
"postman-alternative"
],
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak"
},
"bugs": {
"url": "https://feedback.yaak.app"
},
"homepage": "https://yaak.app",
"version": "0.5.3",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,8 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PluginVersion } from "./gen_search.js";
export type PluginNameVersion = { name: string, version: string, };
export type PluginSearchResponse = { plugins: Array<PluginVersion>, };
export type PluginUpdatesResponse = { plugins: Array<PluginNameVersion>, };

View File

@@ -1,15 +1,17 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
@@ -27,7 +29,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonValue }, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
@@ -102,11 +104,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
@@ -133,11 +131,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
@@ -176,11 +170,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
@@ -215,11 +205,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
@@ -246,11 +232,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
@@ -283,11 +265,7 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
@@ -328,26 +306,18 @@ hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, };
export type GetGrpcRequestActionsResponse = { actions: Array<GrpcRequestAction>, pluginRefId: string, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
@@ -360,12 +330,6 @@ export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>;
export type GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
@@ -382,14 +346,10 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, };
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
@@ -414,10 +374,6 @@ required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderGrpcRequestRequest = { grpcRequest: GrpcRequest, purpose: RenderPurpose, };
export type RenderGrpcRequestResponse = { grpcRequest: GrpcRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
@@ -450,32 +406,6 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
export type TemplateRenderResponse = { data: JsonValue, };
export type Theme = {
/**
* How the theme is identified. This should never be changed
*/
id: string,
/**
* The friendly name of the theme to be displayed to the user
*/
label: string,
/**
* Whether the theme will be used for dark or light appearance
*/
dark: boolean,
/**
* The default top-level colors for the theme
*/
base: ThemeComponentColors,
/**
* Optionally override theme for individual UI components for more control
*/
components?: ThemeComponents, };
export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string, surfaceActive?: string, text?: string, textSubtle?: string, textSubtlest?: string, border?: string, borderSubtle?: string, borderFocus?: string, shadow?: string, backdrop?: string, selection?: string, primary?: string, secondary?: string, info?: string, success?: string, notice?: string, warning?: string, danger?: string, };
export type ThemeComponents = { dialog?: ThemeComponentColors, menu?: ThemeComponentColors, toast?: ThemeComponentColors, sidebar?: ThemeComponentColors, responsePane?: ThemeComponentColors, appHeader?: ThemeComponentColors, button?: ThemeComponentColors, banner?: ThemeComponentColors, templateTag?: ThemeComponentColors, urlBar?: ThemeComponentColors, editor?: ThemeComponentColors, input?: ThemeComponentColors, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -1,12 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, sortPriority: number, };
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id?: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
@@ -22,4 +24,4 @@ export type HttpUrlParameter = { enabled?: boolean, name: string, value: string,
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, };
export type PluginVersion = { id: string, version: string, url: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, };

View File

@@ -1,16 +1,11 @@
import type {
FindHttpResponsesRequest,
FindHttpResponsesResponse,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
ListCookieNamesResponse,
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderGrpcRequestRequest,
RenderGrpcRequestResponse,
RenderHttpRequestRequest,
RenderHttpRequestResponse,
SendHttpRequestRequest,
@@ -43,13 +38,6 @@ export interface Context {
},
): Promise<{ close: () => void }>;
};
cookies: {
listNames(): Promise<ListCookieNamesResponse['names']>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
};
grpcRequest: {
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;

View File

@@ -1,6 +0,0 @@
import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type GrpcRequestActionPlugin = GrpcRequestAction & {
onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void;
};

View File

@@ -1,5 +1,5 @@
import { ImportResources } from '../bindings/gen_events';
import { AtLeast, MaybePromise } from '../helpers';
import { AtLeast } from '../helpers';
import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model';
@@ -21,8 +21,5 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = {
name: string;
description?: string;
onImport(
ctx: Context,
args: { text: string },
): MaybePromise<ImportPluginResponse | null | undefined>;
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>;
};

View File

@@ -1,3 +1,8 @@
import { Theme } from '../bindings/gen_events';
import { Index } from "../themes";
import { Context } from "./Context";
export type ThemePlugin = Theme;
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: Context, fileContents: string): Promise<Index>;
};

View File

@@ -1,6 +1,5 @@
import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
@@ -13,10 +12,9 @@ export type { Context } from './Context';
*/
export type PluginDefinition = {
importer?: ImporterPlugin;
themes?: ThemePlugin[];
theme?: ThemePlugin;
filter?: FilterPlugin;
authentication?: AuthenticationPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
grpcRequestActions?: GrpcRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[];
};

View File

@@ -2,17 +2,12 @@
"compilerOptions": {
"module": "node16",
"target": "es6",
"lib": [
"es2021",
"dom"
],
"lib": ["es2021"],
"declaration": true,
"declarationDir": "./lib",
"outDir": "./lib",
"strict": true,
"types": [
"node"
]
"types": ["node"]
},
"files": [
"src/index.ts"

View File

@@ -1,33 +1,30 @@
import {
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
import type {
BootRequest,
BootResponse,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdResponse,
GetKeyValueResponse, GrpcRequestAction,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
ListCookieNamesResponse,
PluginWindowContext,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderHttpRequestResponse,
RenderGrpcRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateFunctionArg,
TemplateRenderResponse,
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
} from '@yaakapp/api';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
// import util from 'node:util';
import { EventChannel } from './EventChannel';
// import { interceptStdout } from './interceptStdout';
import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData {
@@ -54,22 +51,9 @@ export class PluginInstance {
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
this.#mod = {};
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const bootResponse: BootResponse = {
name: this.#pkg.name ?? 'unknown',
version: this.#pkg.version ?? '0.0.1',
};
const fileChangeCallback = async () => {
this.#importModule();
return this.#sendPayload(
windowContextNone,
{ type: 'reload_response', ...bootResponse },
null,
);
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
if (this.#workerData.bootRequest.watch) {
@@ -77,6 +61,12 @@ export class PluginInstance {
watchFile(this.#pathPkg(), fileChangeCallback);
}
this.#mod = {};
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
// TODO: Re-implement this now that we're not using workers
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
this.#importModule();
}
@@ -146,24 +136,6 @@ export class PluginInstance {
return;
}
if (
payload.type === 'get_grpc_request_actions_request' &&
Array.isArray(this.#mod?.grpcRequestActions)
) {
const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'get_grpc_request_actions_response',
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(this.#mod?.httpRequestActions)
@@ -182,15 +154,6 @@ export class PluginInstance {
return;
}
if (payload.type === 'get_themes_request' && Array.isArray(this.#mod?.themes)) {
const replyPayload: InternalEventPayload = {
type: 'get_themes_response',
themes: this.#mod.themes,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(this.#mod?.templateFunctions)
@@ -227,12 +190,13 @@ export class PluginInstance {
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = [];
for (const v of args) {
if (v && 'dynamic' in v) {
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else if (v) {
} else {
resolvedArgs.push(v);
}
}
@@ -293,18 +257,6 @@ export class PluginInstance {
}
}
if (
payload.type === 'call_grpc_request_action_request' &&
Array.isArray(this.#mod.grpcRequestActions)
) {
const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions)
@@ -329,9 +281,15 @@ export class PluginInstance {
this.#importModule();
}
} catch (err) {
const error = `${err}`.replace(/^Error:\s*/g, '');
console.log('Plugin call threw exception', payload.type, '→', error);
this.#sendPayload(windowContext, { type: 'error_response', error }, replyId);
console.log('Plugin call threw exception', payload.type, err);
this.#sendPayload(
windowContext,
{
type: 'error_response',
error: `${err}`,
},
replyId,
);
return;
}
@@ -502,19 +460,6 @@ export class PluginInstance {
return httpResponses;
},
},
grpcRequest: {
render: async (args) => {
const payload = {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
event.windowContext,
payload,
);
return grpcRequest;
},
},
httpRequest: {
getById: async (args) => {
const payload = {
@@ -550,27 +495,6 @@ export class PluginInstance {
return httpRequest;
},
},
cookies: {
getValue: async (args: GetCookieValueRequest) => {
const payload = {
type: 'get_cookie_value_request',
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
event.windowContext,
payload,
);
return value;
},
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
event.windowContext,
payload,
);
return names;
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type
@@ -628,7 +552,7 @@ function genId(len = 5): string {
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonValue | undefined },
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
@@ -639,20 +563,20 @@ function applyFormInputDefaults(
}
}
const watchedFiles: Record<string, Stats | null> = {};
const watchedFiles: Record<string, Stats> = {};
/**
* Watch a file and trigger a callback on change.
* Watch a file and trigger callback on change.
*
* We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes.
* trigger a "change" event when the access date changes
*/
function watchFile(filepath: string, cb: () => void) {
function watchFile(filepath: string, cb: (filepath: string) => void) {
watch(filepath, () => {
const stat = statSync(filepath, { throwIfNoEntry: false });
if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
watchedFiles[filepath] = stat ?? null;
cb();
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
}
watchedFiles[filepath] = stat;
});
}

View File

@@ -53,7 +53,3 @@ async function handleIncoming(msg: string) {
plugin.sendToWorker(pluginEvent);
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

1
plugins/.gitignore vendored
View File

@@ -1 +0,0 @@
*/build

View File

@@ -1,112 +0,0 @@
import type { HttpRequest, PluginDefinition } from '@yaakapp/api';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
httpRequestActions: [
{
label: 'Copy as Curl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
purpose: 'preview',
});
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
},
],
};
export async function convertToCurl(request: Partial<HttpRequest>) {
const xs = ['curl'];
// Add method and URL all on first line
if (request.method) xs.push('-X', request.method);
if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE);
// Add URL params
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
xs.push('--url-query', quote(`${p.name}=${p.value}`));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push('--header', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add form params
if (Array.isArray(request.body?.form)) {
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : '';
xs.push(flag, v);
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.query === 'string') {
const body = {
query: request.body.query || '',
variables: maybeParseJSON(request.body.variables, undefined),
};
xs.push('--data-raw', `${quote(JSON.stringify(body))}`);
xs.push(NEWLINE);
} else if (typeof request.body?.text === 'string') {
xs.push('--data-raw', `${quote(request.body.text)}`);
xs.push(NEWLINE);
}
// Add basic/digest authentication
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
if (request.authenticationType === 'digest') xs.push('--digest');
xs.push(
'--user',
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),
);
xs.push(NEWLINE);
}
// Add bearer authentication
if (request.authenticationType === 'bearer') {
xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON<T>(v: string, fallback: T) {
try {
return JSON.parse(v);
} catch {
return fallback;
}
}

View File

@@ -1,206 +0,0 @@
import { describe, expect, test } from 'vitest';
import { convertToCurl } from '../src';
describe('exporter-curl', () => {
test('Exports GET with params', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
urlParameters: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual(
[`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `),
);
});
test('Exports POST with url form data', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `),
);
});
test('Exports POST with GraphQL data', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'graphql',
body: {
query: '{foo,bar}',
variables: '{"a": "aaa", "b": "bbb"}',
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`].join(` \\\n `),
);
});
test('Exports POST with GraphQL data no variables', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'graphql',
body: {
query: '{foo,bar}',
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}"}'`].join(` \\\n `),
);
});
test('Exports PUT with multipart form', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'PUT',
bodyType: 'multipart/form-data',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
{ name: 'f', file: '/foo/bar.png', contentType: 'image/png' },
],
},
}),
).toEqual(
[
`curl -X PUT 'https://yaak.app'`,
`--form 'a=aaa'`,
`--form 'b=bbb'`,
`--form f=@/foo/bar.png;type=image/png`,
].join(` \\\n `),
);
});
test('Exports JSON body', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar's"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw '{"foo":"bar\\'s"}'`,
].join(` \\\n `),
);
});
test('Exports multi-line JSON body', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar",\n"baz":"qux"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw '{"foo":"bar",\n"baz":"qux"}'`,
].join(` \\\n `),
);
});
test('Exports headers', async () => {
expect(
await convertToCurl({
headers: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
});
test('Basic auth', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `));
});
test('Broken basic auth', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
});
test('Digest auth', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `));
});
test('Bearer auth', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'tok',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
});
test('Broken bearer auth', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
});
});

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/action-copy-grpcurl",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,134 +0,0 @@
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
import path from 'node:path';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
grpcRequestActions: [
{
label: 'Copy as gRPCurl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.grpcRequest.render({
grpcRequest: args.grpcRequest,
purpose: 'preview',
});
const data = await convert(rendered_request, args.protoFiles);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
},
],
};
export async function convert(request: Partial<GrpcRequest>, allProtoFiles: string[]) {
const xs = ['grpcurl'];
if (request.url?.startsWith('http://')) {
xs.push('-plaintext');
}
const protoIncludes = allProtoFiles.filter((f) => !f.endsWith('.proto'));
const protoFiles = allProtoFiles.filter((f) => f.endsWith('.proto'));
const inferredIncludes = new Set<string>();
for (const f of protoFiles) {
const protoDir = findParentProtoDir(f);
if (protoDir) {
inferredIncludes.add(protoDir);
} else {
inferredIncludes.add(path.join(f, '..'));
inferredIncludes.add(path.join(f, '..', '..'));
}
}
for (const f of protoIncludes) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of inferredIncludes.values()) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of protoFiles) {
xs.push('-proto', quote(f));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.metadata ?? []).filter(onlyEnabled)) {
xs.push('-H', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add basic authentication
if (request.authenticationType === 'basic') {
const user = request.authentication?.username ?? '';
const pass = request.authentication?.password ?? '';
const encoded = btoa(`${user}:${pass}`);
xs.push('-H', quote(`Authorization: Basic ${encoded}`));
xs.push(NEWLINE);
} else if (request.authenticationType === 'bearer') {
// Add bearer authentication
xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Add form params
if (request.message) {
xs.push('-d', `${quote(JSON.stringify(JSON.parse(request.message)))}`);
xs.push(NEWLINE);
}
// Add the server address
if (request.url) {
const server = request.url.replace(/^https?:\/\//, ''); // remove protocol
xs.push(server);
}
// Add service + method
if (request.service && request.method) {
xs.push(`${request.service}/${request.method}`);
}
xs.push(NEWLINE);
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function findParentProtoDir(startPath: string): string | null {
let dir = path.resolve(startPath);
while (true) {
if (path.basename(dir) === 'proto') {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) {
return null; // Reached root
}
dir = parent;
}
}

View File

@@ -1,110 +0,0 @@
import { describe, expect, test } from 'vitest';
import { convert } from '../src';
describe('exporter-curl', () => {
test('Simple example', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
},
[],
),
).toEqual([`grpcurl yaak.app`].join(` \\\n `));
});
test('Basic metadata', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
metadata: [
{ name: 'aaa', value: 'AAA' },
{ enabled: true, name: 'bbb', value: 'BBB' },
{ enabled: false, name: 'disabled', value: 'ddd' },
],
},
[],
),
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, `yaak.app`].join(` \\\n `));
});
test('Single proto file', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/baz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, same dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/foo/bar/aaa.proto', '/foo/bar/bbb.proto']),
).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/aaa.proto'`,
`-proto '/foo/bar/bbb.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, different dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb/ccc.proto', '/xxx/yyy/zzz.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/aaa'`,
`-import-path '/xxx/yyy'`,
`-import-path '/xxx'`,
`-proto '/aaa/bbb/ccc.proto'`,
`-proto '/xxx/yyy/zzz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Single include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `yaak.app`].join(` \\\n `),
);
});
test('Multiple include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, `yaak.app`].join(` \\\n `),
);
});
test('Mixed proto and dirs', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy', '/foo/bar.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/xxx/yyy'`,
`-import-path '/foo'`,
`-import-path '/'`,
`-proto '/foo/bar.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Sends data', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
message: JSON.stringify({ foo: 'bar', baz: 1.0 }, null, 2),
},
['/foo.proto'],
),
).toEqual(
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`,
`yaak.app`,
].join(` \\\n `),
);
});
});

View File

@@ -1,26 +0,0 @@
import { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
authentication: {
name: 'basic',
label: 'Basic Auth',
shortLabel: 'Basic',
args: [{
type: 'text',
name: 'username',
label: 'Username',
optional: true,
}, {
type: 'text',
name: 'password',
label: 'Password',
optional: true,
password: true,
}],
async onApply(_ctx, { values }) {
const { username, password } = values;
const value = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
return { setHeaders: [{ name: 'Authorization', value }] };
},
},
};

View File

@@ -1,21 +0,0 @@
import { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
authentication: {
name: 'bearer',
label: 'Bearer Token',
shortLabel: 'Bearer',
args: [{
type: 'text',
name: 'token',
label: 'Token',
optional: true,
password: true,
}],
async onApply(_ctx, { values }) {
const { token } = values;
const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: 'Authorization', value }] };
},
},
};

View File

@@ -1,68 +0,0 @@
import { PluginDefinition } from '@yaakapp/api';
import jwt from 'jsonwebtoken';
const algorithms = [
'HS256',
'HS384',
'HS512',
'RS256',
'RS384',
'RS512',
'PS256',
'PS384',
'PS512',
'ES256',
'ES384',
'ES512',
'none',
] as const;
const defaultAlgorithm = algorithms[0];
export const plugin: PluginDefinition = {
authentication: {
name: 'jwt',
label: 'JWT Bearer',
shortLabel: 'JWT',
args: [
{
type: 'select',
name: 'algorithm',
label: 'Algorithm',
hideLabel: true,
defaultValue: defaultAlgorithm,
options: algorithms.map(value => ({ label: value === 'none' ? 'None' : value, value })),
},
{
type: 'text',
name: 'secret',
label: 'Secret or Private Key',
password: true,
optional: true,
multiLine: true,
},
{
type: 'checkbox',
name: 'secretBase64',
label: 'Secret is base64 encoded',
},
{
type: 'editor',
name: 'payload',
label: 'Payload',
language: 'json',
defaultValue: '{\n "foo": "bar"\n}',
placeholder: '{ }',
},
],
async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
const token = jwt.sign(`${payload}`, secret, { algorithm: algorithm as any });
const value = `Bearer ${token}`;
return { setHeaders: [{ name: 'Authorization', value }] };
}
,
},
}
;

View File

@@ -1,78 +0,0 @@
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import type { AccessTokenRawResponse } from './store';
export async function fetchAccessToken(
ctx: Context,
{
accessTokenUrl,
scope,
audience,
params,
grantType,
credentialsInBody,
clientId,
clientSecret,
}: {
clientId: string;
clientSecret: string;
grantType: string;
accessTokenUrl: string;
scope: string | null;
audience: string | null;
credentialsInBody: boolean;
params: HttpUrlParameter[];
},
): Promise<AccessTokenRawResponse> {
console.log('[oauth2] Getting access token', accessTokenUrl);
const httpRequest: Partial<HttpRequest> = {
method: 'POST',
url: accessTokenUrl,
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [{ name: 'grant_type', value: grantType }, ...params],
},
headers: [
{ name: 'User-Agent', value: 'yaak' },
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
],
};
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
if (audience) httpRequest.body!.form.push({ name: 'audience', value: audience });
if (credentialsInBody) {
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
} else {
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
httpRequest.headers!.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
console.log('[oauth2] Got access token response', resp.status);
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
if (resp.status < 200 || resp.status >= 300) {
throw new Error(
'Failed to fetch access token with status=' + resp.status + ' and body=' + body,
);
}
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error('Failed to fetch access token with ' + response.error);
}
return response;
}

View File

@@ -1,19 +0,0 @@
import type { Context } from '@yaakapp/api';
import type { AccessToken } from './store';
import { getToken } from './store';
export async function getAccessTokenIfNotExpired(
ctx: Context,
contextId: string,
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
if (token == null || isTokenExpired(token)) {
return null;
}
return token;
}
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}

View File

@@ -1,112 +0,0 @@
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { isTokenExpired } from './getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse } from './store';
import { deleteToken, getToken, storeToken } from './store';
export async function getOrRefreshAccessToken(
ctx: Context,
contextId: string,
{
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
forceRefresh?: boolean;
},
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
if (token == null) {
return null;
}
const isExpired = isTokenExpired(token);
// Return the current access token if it's still valid
if (!isExpired && !forceRefresh) {
return token;
}
// Token is expired, but there's no refresh token :(
if (!token.response.refresh_token) {
return null;
}
// Access token is expired, so get a new one
const httpRequest: Partial<HttpRequest> = {
method: 'POST',
url: accessTokenUrl,
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [
{ name: 'grant_type', value: 'refresh_token' },
{ name: 'refresh_token', value: token.response.refresh_token },
],
},
headers: [
{ name: 'User-Agent', value: 'yaak' },
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
],
};
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
if (credentialsInBody) {
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
} else {
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
httpRequest.headers!.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
// and returning null;
console.log('[oauth2] Unauthorized refresh_token request');
await deleteToken(ctx, contextId);
return null;
}
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
console.log('[oauth2] Got refresh token response', resp.status);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(
'Failed to refresh access token with status=' + resp.status + ' and body=' + body,
);
}
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error(
`Failed to fetch access token with ${response.error} -> ${response.error_description}`,
);
}
const newResponse: AccessTokenRawResponse = {
...response,
// Assign a new one or keep the old one,
refresh_token: response.refresh_token ?? token.response.refresh_token,
};
return storeToken(ctx, contextId, newResponse);
}

View File

@@ -1,151 +0,0 @@
import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken } from '../store';
import { getDataDirKey, storeToken } from '../store';
export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain';
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
export async function getAuthorizationCode(
ctx: Context,
contextId: string,
{
authorizationUrl: authorizationUrlRaw,
accessTokenUrl,
clientId,
clientSecret,
redirectUri,
scope,
state,
audience,
credentialsInBody,
pkce,
tokenName,
}: {
authorizationUrl: string;
accessTokenUrl: string;
clientId: string;
clientSecret: string;
redirectUri: string | null;
scope: string | null;
state: string | null;
audience: string | null;
credentialsInBody: boolean;
pkce: {
challengeMethod: string;
codeVerifier: string;
} | null;
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody,
});
if (token != null) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (pkce) {
authorizationUrl.searchParams.set(
'code_challenge',
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
);
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
}
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
// eslint-disable-next-line no-async-promise-executor
const code = await new Promise<string>(async (resolve, reject) => {
let foundCode = false;
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
dataDirKey,
async onClose() {
if (!foundCode) {
reject(new Error('Authorization window closed'));
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
if (url.searchParams.has('error')) {
close();
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
}
const code = url.searchParams.get('code');
if (!code) {
console.log('[oauth2] Code not found');
return; // Could be one of many redirects in a chain, so skip it
}
// Close the window here, because we don't need it anymore!
foundCode = true;
close();
resolve(code);
},
});
});
console.log('[oauth2] Code found');
const response = await fetchAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
return storeToken(ctx, contextId, response, tokenName);
}
export function genPkceCodeVerifier() {
return encodeForPkce(randomBytes(32));
}
function pkceCodeChallenge(verifier: string, method: string) {
if (method === 'plain') {
return verifier;
}
const hash = encodeForPkce(createHash('sha256').update(verifier).digest());
return hash
.replace(/=/g, '') // Remove padding '='
.replace(/\+/g, '-') // Replace '+' with '-'
.replace(/\//g, '_'); // Replace '/' with '_'
}
function encodeForPkce(bytes: Buffer) {
return bytes
.toString('base64')
.replace(/=/g, '') // Remove padding '='
.replace(/\+/g, '-') // Replace '+' with '-'
.replace(/\//g, '_'); // Replace '/' with '_'
}

View File

@@ -1,42 +0,0 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import { getToken, storeToken } from '../store';
export async function getClientCredentials(
ctx: Context,
contextId: string,
{
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
}: {
accessTokenUrl: string;
clientId: string;
clientSecret: string;
scope: string | null;
audience: string | null;
credentialsInBody: boolean;
},
) {
const token = await getToken(ctx, contextId);
if (token && !isTokenExpired(token)) {
return token;
}
const response = await fetchAccessToken(ctx, {
grantType: 'client_credentials',
accessTokenUrl,
audience,
clientId,
clientSecret,
scope,
credentialsInBody,
params: [],
});
return storeToken(ctx, contextId, response);
}

View File

@@ -1,89 +0,0 @@
import type { Context } from '@yaakapp/api';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getToken, storeToken } from '../store';
export async function getImplicit(
ctx: Context,
contextId: string,
{
authorizationUrl: authorizationUrlRaw,
responseType,
clientId,
redirectUri,
scope,
state,
audience,
tokenName,
}: {
authorizationUrl: string;
responseType: string;
clientId: string;
redirectUri: string | null;
scope: string | null;
state: string | null;
audience: string | null;
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
const token = await getToken(ctx, contextId);
if (token != null && !isTokenExpired(token)) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
// eslint-disable-next-line no-async-promise-executor
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
let foundAccessToken = false;
const authorizationUrlStr = authorizationUrl.toString();
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
async onClose() {
if (!foundAccessToken) {
reject(new Error('Authorization window closed'));
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has('error')) {
return reject(Error(`Failed to authorize: ${url.searchParams.get('error')}`));
}
const hash = url.hash.slice(1);
const params = new URLSearchParams(hash);
const accessToken = params.get(tokenName);
if (!accessToken) {
return;
}
foundAccessToken = true;
// Close the window here, because we don't need it anymore
close();
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try {
resolve(storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
},
});
});
return newToken;
}

View File

@@ -1,56 +0,0 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken} from '../store';
import { storeToken } from '../store';
export async function getPassword(
ctx: Context,
contextId: string,
{
accessTokenUrl,
clientId,
clientSecret,
username,
password,
credentialsInBody,
audience,
scope,
}: {
accessTokenUrl: string;
clientId: string;
clientSecret: string;
username: string;
password: string;
scope: string | null;
audience: string | null;
credentialsInBody: boolean;
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody,
});
if (token != null) {
return token;
}
const response = await fetchAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
grantType: 'password',
credentialsInBody,
params: [
{ name: 'username', value: username },
{ name: 'password', value: password },
],
});
return storeToken(ctx, contextId, response);
}

View File

@@ -1,406 +0,0 @@
import type {
Context,
FormInputSelectOption,
GetHttpAuthenticationConfigRequest,
JsonPrimitive,
PluginDefinition,
} from '@yaakapp/api';
import {
genPkceCodeVerifier,
DEFAULT_PKCE_METHOD,
getAuthorizationCode,
PKCE_PLAIN,
PKCE_SHA256,
} from './grants/authorizationCode';
import { getClientCredentials } from './grants/clientCredentials';
import { getImplicit } from './grants/implicit';
import { getPassword } from './grants/password';
import type { AccessToken } from './store';
import { deleteToken, getToken, resetDataDirKey } from './store';
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
const grantTypes: FormInputSelectOption[] = [
{ label: 'Authorization Code', value: 'authorization_code' },
{ label: 'Implicit', value: 'implicit' },
{ label: 'Resource Owner Password Credential', value: 'password' },
{ label: 'Client Credentials', value: 'client_credentials' },
];
const defaultGrantType = grantTypes[0]!.value;
function hiddenIfNot(
grantTypes: GrantType[],
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
) {
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
const hasGrantType = grantTypes.find((t) => t === String(values.grantType ?? defaultGrantType));
const hasOtherBools = other.every((t) => t(values));
const show = hasGrantType && hasOtherBools;
return { hidden: !show };
};
}
const authorizationUrls = [
'https://github.com/login/oauth/authorize',
'https://account.box.com/api/oauth2/authorize',
'https://accounts.google.com/o/oauth2/v2/auth',
'https://api.imgur.com/oauth2/authorize',
'https://bitly.com/oauth/authorize',
'https://gitlab.example.com/oauth/authorize',
'https://medium.com/m/oauth/authorize',
'https://public-api.wordpress.com/oauth2/authorize',
'https://slack.com/oauth/authorize',
'https://todoist.com/oauth/authorize',
'https://www.dropbox.com/oauth2/authorize',
'https://www.linkedin.com/oauth/v2/authorization',
'https://MY_SHOP.myshopify.com/admin/oauth/access_token',
'https://appcenter.intuit.com/app/connect/oauth2/authorize',
];
const accessTokenUrls = [
'https://github.com/login/oauth/access_token',
'https://api-ssl.bitly.com/oauth/access_token',
'https://api.box.com/oauth2/token',
'https://api.dropboxapi.com/oauth2/token',
'https://api.imgur.com/oauth2/token',
'https://api.medium.com/v1/tokens',
'https://gitlab.example.com/oauth/token',
'https://public-api.wordpress.com/oauth2/token',
'https://slack.com/api/oauth.access',
'https://todoist.com/oauth/access_token',
'https://www.googleapis.com/oauth2/v4/token',
'https://www.linkedin.com/oauth/v2/accessToken',
'https://MY_SHOP.myshopify.com/admin/oauth/authorize',
'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer',
];
export const plugin: PluginDefinition = {
authentication: {
name: 'oauth2',
label: 'OAuth 2.0',
shortLabel: 'OAuth 2',
actions: [
{
label: 'Copy Current Token',
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
} else {
await ctx.clipboard.copyText(token.response.access_token);
await ctx.toast.show({
message: 'Token copied to clipboard',
icon: 'copy',
color: 'success',
});
}
},
},
{
label: 'Delete Token',
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
await ctx.toast.show({ message: 'Token deleted', color: 'success' });
} else {
await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
}
},
},
{
label: 'Clear Window Session',
async onSelect(ctx, { contextId }) {
await resetDataDirKey(ctx, contextId);
},
},
{
label: 'Toggle Debug Logs',
async onSelect(ctx) {
const enableLogs = !(await ctx.store.get('enable_logs'));
await ctx.store.set('enable_logs', enableLogs);
await ctx.toast.show({
message: `Debug logs ${enableLogs ? 'enabled' : 'disabled'}`,
color: 'info',
});
},
},
],
args: [
{
type: 'select',
name: 'grantType',
label: 'Grant Type',
hideLabel: true,
defaultValue: defaultGrantType,
options: grantTypes,
},
// Always-present fields
{
type: 'text',
name: 'clientId',
label: 'Client ID',
optional: true,
},
{
type: 'text',
name: 'clientSecret',
label: 'Client Secret',
optional: true,
password: true,
dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']),
},
{
type: 'text',
name: 'authorizationUrl',
optional: true,
label: 'Authorization URL',
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
placeholder: authorizationUrls[0],
completionOptions: authorizationUrls.map((url) => ({ label: url, value: url })),
},
{
type: 'text',
name: 'accessTokenUrl',
optional: true,
label: 'Access Token URL',
placeholder: accessTokenUrls[0],
dynamic: hiddenIfNot(['authorization_code', 'password', 'client_credentials']),
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url })),
},
{
type: 'text',
name: 'redirectUri',
label: 'Redirect URI',
optional: true,
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
},
{
type: 'text',
name: 'state',
label: 'State',
optional: true,
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
},
{
type: 'text',
name: 'audience',
label: 'Audience',
optional: true,
},
{
type: 'select',
name: 'tokenName',
label: 'Token for authorization',
description:
'Select which token to send in the "Authorization: Bearer" header. Most APIs expect ' +
'access_token, but some (like OpenID Connect) require id_token.',
defaultValue: 'access_token',
options: [
{ label: 'access_token', value: 'access_token' },
{ label: 'id_token', value: 'id_token' },
],
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
},
{
type: 'checkbox',
name: 'usePkce',
label: 'Use PKCE',
dynamic: hiddenIfNot(['authorization_code']),
},
{
type: 'select',
name: 'pkceChallengeMethod',
label: 'Code Challenge Method',
options: [
{ label: 'SHA-256', value: PKCE_SHA256 },
{ label: 'Plain', value: PKCE_PLAIN },
],
defaultValue: DEFAULT_PKCE_METHOD,
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
},
{
type: 'text',
name: 'pkceCodeChallenge',
label: 'Code Verifier',
placeholder: 'Automatically generated when not set',
optional: true,
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
},
{
type: 'text',
name: 'username',
label: 'Username',
optional: true,
dynamic: hiddenIfNot(['password']),
},
{
type: 'text',
name: 'password',
label: 'Password',
password: true,
optional: true,
dynamic: hiddenIfNot(['password']),
},
{
type: 'select',
name: 'responseType',
label: 'Response Type',
defaultValue: 'token',
options: [
{ label: 'Access Token', value: 'token' },
{ label: 'ID Token', value: 'id_token' },
{ label: 'ID and Access Token', value: 'id_token token' },
],
dynamic: hiddenIfNot(['implicit']),
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
{
type: 'text',
name: 'headerPrefix',
label: 'Header Prefix',
optional: true,
defaultValue: 'Bearer',
},
{
type: 'select',
name: 'credentials',
label: 'Send Credentials',
defaultValue: 'body',
options: [
{ label: 'In Request Body', value: 'body' },
{ label: 'As Basic Authentication', value: 'basic' },
],
},
],
},
{
type: 'accordion',
label: 'Access Token Response',
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
return { hidden: true };
}
return {
label: 'Access Token Response',
inputs: [
{
type: 'editor',
defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true,
readOnly: true,
language: 'json',
},
],
};
},
},
],
async onApply(ctx, { values, contextId }) {
const headerPrefix = stringArg(values, 'headerPrefix');
const grantType = stringArg(values, 'grantType') as GrantType;
const credentialsInBody = values.credentials === 'body';
const tokenName = values.tokenName === 'id_token' ? 'id_token' : 'access_token';
let token: AccessToken;
if (grantType === 'authorization_code') {
const authorizationUrl = stringArg(values, 'authorizationUrl');
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
token = await getAuthorizationCode(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'),
redirectUri: stringArgOrNull(values, 'redirectUri'),
scope: stringArgOrNull(values, 'scope'),
audience: stringArgOrNull(values, 'audience'),
state: stringArgOrNull(values, 'state'),
credentialsInBody,
pkce: values.usePkce
? {
challengeMethod: stringArg(values, 'pkceChallengeMethod') || DEFAULT_PKCE_METHOD,
codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(),
}
: null,
tokenName: tokenName,
});
} else if (grantType === 'implicit') {
const authorizationUrl = stringArg(values, 'authorizationUrl');
token = await getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
clientId: stringArg(values, 'clientId'),
redirectUri: stringArgOrNull(values, 'redirectUri'),
responseType: stringArg(values, 'responseType'),
scope: stringArgOrNull(values, 'scope'),
audience: stringArgOrNull(values, 'audience'),
state: stringArgOrNull(values, 'state'),
tokenName: tokenName,
});
} else if (grantType === 'client_credentials') {
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
token = await getClientCredentials(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'),
scope: stringArgOrNull(values, 'scope'),
audience: stringArgOrNull(values, 'audience'),
credentialsInBody,
});
} else if (grantType === 'password') {
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
token = await getPassword(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'),
username: stringArg(values, 'username'),
password: stringArg(values, 'password'),
scope: stringArgOrNull(values, 'scope'),
audience: stringArgOrNull(values, 'audience'),
credentialsInBody,
});
} else {
throw new Error('Invalid grant type ' + grantType);
}
const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim();
return {
setHeaders: [
{
name: 'Authorization',
value: headerValue,
},
],
};
},
},
};
function stringArgOrNull(
values: Record<string, JsonPrimitive | undefined>,
name: string,
): string | null {
const arg = values[name];
if (arg == null || arg == '') return null;
return `${arg}`;
}
function stringArg(values: Record<string, JsonPrimitive | undefined>, name: string): string {
const arg = stringArgOrNull(values, name);
if (!arg) return '';
return arg;
}

View File

@@ -1,62 +0,0 @@
import { Context } from '@yaakapp/api';
export async function storeToken(
ctx: Context,
contextId: string,
response: AccessTokenRawResponse,
tokenName: 'access_token' | 'id_token' = 'access_token',
) {
if (!response[tokenName]) {
throw new Error(`${tokenName} not found in response ${Object.keys(response).join(', ')}`);
}
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1000 : null;
const token: AccessToken = {
response,
expiresAt,
};
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token);
return token;
}
export async function getToken(ctx: Context, contextId: string) {
return ctx.store.get<AccessToken>(tokenStoreKey(contextId));
}
export async function deleteToken(ctx: Context, contextId: string) {
return ctx.store.delete(tokenStoreKey(contextId));
}
export async function resetDataDirKey(ctx: Context, contextId: string) {
const key = new Date().toISOString();
return ctx.store.set<string>(dataDirStoreKey(contextId), key);
}
export async function getDataDirKey(ctx: Context, contextId: string) {
const key = (await ctx.store.get<string>(dataDirStoreKey(contextId))) ?? 'default';
return `${contextId}::${key}`;
}
function tokenStoreKey(contextId: string) {
return ['token', contextId].join('::');
}
function dataDirStoreKey(contextId: string) {
return ['data_dir', contextId].join('::');
}
export interface AccessToken {
response: AccessTokenRawResponse;
expiresAt: number | null;
}
export interface AccessTokenRawResponse {
access_token: string;
id_token?: string;
token_type?: string;
expires_in?: number;
refresh_token?: string;
error?: string;
error_description?: string;
scope?: string;
}

View File

@@ -1,14 +0,0 @@
import { PluginDefinition } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
export const plugin: PluginDefinition = {
filter: {
name: 'JSONPath',
description: 'Filter JSONPath',
onFilter(_ctx, args) {
const parsed = JSON.parse(args.payload);
const filtered = JSONPath({ path: args.filter, json: parsed });
return { filtered: JSON.stringify(filtered, null, 2) };
},
},
};

View File

@@ -1,21 +0,0 @@
import { DOMParser } from '@xmldom/xmldom';
import { PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath';
export const plugin: PluginDefinition = {
filter: {
name: 'XPath',
description: 'Filter XPath',
onFilter(_ctx, args) {
const doc = new DOMParser().parseFromString(args.payload, 'text/xml');
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { filtered: result.map(r => String(r)).join('\n') };
} else {
// Not sure what cases this happens in (?)
return { filtered: String(result) };
}
},
},
};

View File

@@ -1,441 +0,0 @@
import { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
import { ControlOperator, parse, ParseEntry } from 'shell-quote';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii'];
const SUPPORTED_FLAGS = [
['cookie', 'b'],
['d', 'data'], // Add url encoded data
['data-ascii'],
['data-binary'],
['data-raw'],
['data-urlencode'],
['digest'], // Apply auth as digest
['form', 'F'], // Add multipart data
['get', 'G'], // Put the post data in the URL
['header', 'H'],
['request', 'X'], // Request method
['url'], // Specify the URL explicitly
['url-query'],
['user', 'u'], // Authentication
DATA_FLAGS,
].flatMap((v) => v);
const BOOLEAN_FLAGS = ['G', 'get', 'digest'];
type FlagValue = string | boolean;
type FlagsByName = Record<string, FlagValue[]>;
export const plugin: PluginDefinition = {
importer: {
name: 'cURL',
description: 'Import cURL commands',
onImport(_ctx: Context, args: { text: string }) {
return convertCurl(args.text) as any;
},
},
};
export function convertCurl(rawData: string) {
if (!rawData.match(/^\s*curl /)) {
return null;
}
const commands: ParseEntry[][] = [];
// Replace non-escaped newlines with semicolons to make parsing easier
// NOTE: This is really slow in debug build but fast in release mode
const normalizedData = rawData.replace(/\ncurl/g, '; curl');
let currentCommand: ParseEntry[] = [];
const parsed = parse(normalizedData);
// Break up `-XPOST` into `-X POST`
const normalizedParseEntries = parsed.flatMap((entry) => {
if (
typeof entry === 'string' &&
entry.startsWith('-') &&
!entry.startsWith('--') &&
entry.length > 2
) {
return [entry.slice(0, 2), entry.slice(2)];
}
return entry;
});
for (const parseEntry of normalizedParseEntries) {
if (typeof parseEntry === 'string') {
if (parseEntry.startsWith('$')) {
currentCommand.push(parseEntry.slice(1));
} else {
currentCommand.push(parseEntry);
}
continue;
}
if ('comment' in parseEntry) {
continue;
}
const { op } = parseEntry as { op: 'glob'; pattern: string } | { op: ControlOperator };
// `;` separates commands
if (op === ';') {
commands.push(currentCommand);
currentCommand = [];
continue;
}
if (op?.startsWith('$')) {
// Handle the case where literal like -H $'Header: \'Some Quoted Thing\''
const str = op.slice(2, op.length - 1).replace(/\\'/g, '\'');
currentCommand.push(str);
continue;
}
if (op === 'glob') {
currentCommand.push((parseEntry as { op: 'glob'; pattern: string }).pattern);
}
}
commands.push(currentCommand);
const workspace: ExportResources['workspaces'][0] = {
model: 'workspace',
id: generateId('workspace'),
name: 'Curl Import',
};
const requests: ExportResources['httpRequests'] = commands
.filter((command) => command[0] === 'curl')
.map((v) => importCommand(v, workspace.id));
return {
resources: {
httpRequests: requests,
workspaces: [workspace],
},
};
}
function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
// ~~~~~~~~~~~~~~~~~~~~~ //
// Collect all the flags //
// ~~~~~~~~~~~~~~~~~~~~~ //
const flagsByName: FlagsByName = {};
const singletons: ParseEntry[] = [];
// Start at 1 so we can skip the ^curl part
for (let i = 1; i < parseEntries.length; i++) {
let parseEntry = parseEntries[i];
if (typeof parseEntry === 'string') {
parseEntry = parseEntry.trim();
}
if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) {
const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-';
let name = parseEntry.replace(/^-{1,2}/, '');
if (!SUPPORTED_FLAGS.includes(name)) {
continue;
}
let value;
const nextEntry = parseEntries[i + 1];
const hasValue = !BOOLEAN_FLAGS.includes(name);
if (isSingleDash && name.length > 1) {
// Handle squished arguments like -XPOST
value = name.slice(1);
name = name.slice(0, 1);
} else if (typeof nextEntry === 'string' && hasValue && !nextEntry.startsWith('-')) {
// Next arg is not a flag, so assign it as the value
value = nextEntry;
i++; // Skip next one
} else {
value = true;
}
flagsByName[name] = flagsByName[name] || [];
flagsByName[name]!.push(value);
} else if (parseEntry) {
singletons.push(parseEntry);
}
}
// ~~~~~~~~~~~~~~~~~ //
// Build the request //
// ~~~~~~~~~~~~~~~~~ //
// Url and Parameters
let urlParameters: HttpUrlParameter[];
let url: string;
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
const [baseUrl, search] = splitOnce(urlArg, '?');
urlParameters =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true };
}) ?? [];
url = baseUrl ?? urlArg;
// Query params
for (const p of flagsByName['url-query'] ?? []) {
if (typeof p !== 'string') {
continue;
}
const [name, value] = p.split('=');
urlParameters.push({
name: name ?? '',
value: value ?? '',
enabled: true,
});
}
// Authentication
const [username, password] = getPairValue(flagsByName, '', ['u', 'user']).split(/:(.*)$/);
const isDigest = getPairValue(flagsByName, false, ['digest']);
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
const authentication = username
? {
username: username.trim(),
password: (password ?? '').trim(),
}
: {};
// Headers
const headers = [
...((flagsByName['header'] as string[] | undefined) || []),
...((flagsByName['H'] as string[] | undefined) || []),
].map((header) => {
const [name, value] = header.split(/:(.*)$/);
// remove final colon from header name if present
if (!value) {
return {
name: (name ?? '').trim().replace(/;$/, ''),
value: '',
enabled: true,
};
}
return {
name: (name ?? '').trim(),
value: value.trim(),
enabled: true,
};
});
// Cookies
const cookieHeaderValue = [
...((flagsByName['cookie'] as string[] | undefined) || []),
...((flagsByName['b'] as string[] | undefined) || []),
]
.map((str) => {
const name = str.split('=', 1)[0];
const value = str.replace(`${name}=`, '');
return `${name}=${value}`;
})
.join('; ');
// Convert cookie value to header
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie');
if (cookieHeaderValue && existingCookieHeader) {
// Has existing cookie header, so let's update it
existingCookieHeader.value += `; ${cookieHeaderValue}`;
} else if (cookieHeaderValue) {
// No existing cookie header, so let's make a new one
headers.push({
name: 'Cookie',
value: cookieHeaderValue,
enabled: true,
});
}
// Body (Text or Blob)
const dataParameters = pairsToDataParameters(flagsByName);
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0] : null;
// Body (Multipart Form Data)
const formDataParams = [
...((flagsByName['form'] as string[] | undefined) || []),
...((flagsByName['F'] as string[] | undefined) || []),
].map((str) => {
const parts = str.split('=');
const name = parts[0] ?? '';
const value = parts[1] ?? '';
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
name,
enabled: true,
};
if (value.indexOf('@') === 0) {
item['file'] = value.slice(1);
} else {
item['value'] = value;
}
return item;
});
// Body
let body = {};
let bodyType: string | null = null;
const bodyAsGET = getPairValue(flagsByName, false, ['G', 'get']);
if (dataParameters.length > 0 && bodyAsGET) {
urlParameters.push(...dataParameters);
} else if (
dataParameters.length > 0 &&
(mimeType == null || mimeType === 'application/x-www-form-urlencoded')
) {
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
body = {
form: dataParameters.map((parameter) => ({
...parameter,
name: decodeURIComponent(parameter.name || ''),
value: decodeURIComponent(parameter.value || ''),
})),
};
headers.push({
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
enabled: true,
});
} else if (dataParameters.length > 0) {
bodyType =
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
? mimeType
: 'other';
body = {
text: dataParameters
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
.join('&'),
};
} else if (formDataParams.length) {
bodyType = mimeType ?? 'multipart/form-data';
body = {
form: formDataParams,
};
if (mimeType == null) {
headers.push({
name: 'Content-Type',
value: 'multipart/form-data',
enabled: true,
});
}
}
// Method
let method = getPairValue(flagsByName, '', ['X', 'request']).toUpperCase();
if (method === '' && body) {
method = 'text' in body || 'form' in body ? 'POST' : 'GET';
}
const request: ExportResources['httpRequests'][0] = {
id: generateId('http_request'),
model: 'http_request',
workspaceId,
name: '',
urlParameters,
url,
method,
headers,
authentication,
authenticationType,
body,
bodyType,
folderId: null,
sortPriority: 0,
};
return request;
}
interface DataParameter {
name: string;
value: string;
contentType?: string;
filePath?: string;
enabled?: boolean;
}
function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
let dataParameters: DataParameter[] = [];
for (const flagName of DATA_FLAGS) {
const pairs = keyedPairs[flagName];
if (!pairs || pairs.length === 0) {
continue;
}
for (const p of pairs) {
if (typeof p !== 'string') continue;
let params = p.split("&");
for (const param of params) {
const [name, value] = param.split('=');
if (param.startsWith('@')) {
// Yaak doesn't support files in url-encoded data, so
dataParameters.push({
name: name ?? '',
value: '',
filePath: param.slice(1),
enabled: true,
});
} else {
dataParameters.push({
name: name ?? '',
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '',
enabled: true,
});
}
}
}
}
return dataParameters;
}
const getPairValue = <T extends string | boolean>(
pairsByName: FlagsByName,
defaultValue: T,
names: string[],
) => {
for (const name of names) {
if (pairsByName[name] && pairsByName[name]!.length) {
return pairsByName[name]![0] as T;
}
}
return defaultValue;
};
function splitOnce(str: string, sep: string): string[] {
const index = str.indexOf(sep);
if (index > -1) {
return [str.slice(0, index), str.slice(index + 1)];
}
return [str];
}
const idCount: Partial<Record<string, number>> = {};
function generateId(model: string): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View File

@@ -1,400 +0,0 @@
import { HttpRequest, Workspace } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { convertCurl } from '../src';
describe('importer-curl', () => {
test('Imports basic GET', () => {
expect(convertCurl('curl https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Explicit URL', () => {
expect(convertCurl('curl --url https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Missing URL', () => {
expect(convertCurl('curl -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
}),
],
},
});
});
test('URL between', () => {
expect(convertCurl('curl -v https://yaak.app -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Random flags', () => {
expect(convertCurl('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
}),
],
},
});
});
test('Imports --request method', () => {
expect(convertCurl('curl --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports -XPOST method', () => {
expect(convertCurl('curl -XPOST --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
}),
],
},
});
});
test('Imports multiple requests', () => {
expect(
convertCurl('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({ url: 'https://yaak.app' }),
baseRequest({ url: 'example.com' }),
baseRequest({ url: 'foo.com' }),
],
},
});
});
test('Imports form data', () => {
expect(
convertCurl('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
headers: [
{
name: 'Content-Type',
value: 'multipart/form-data',
enabled: true,
},
],
bodyType: 'multipart/form-data',
body: {
form: [
{ enabled: true, name: 'a', value: 'aaa' },
{ enabled: true, name: 'b', value: 'bbb' },
{ enabled: true, name: 'f', file: 'filepath' },
],
},
}),
],
},
});
});
test('Imports data params as form url-encoded', () => {
expect(convertCurl('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
bodyType: 'application/x-www-form-urlencoded',
headers: [
{
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
enabled: true,
},
],
body: {
form: [
{ name: 'a', value: '', enabled: true },
{ name: 'b', value: '', enabled: true },
{ name: 'c', value: 'ccc', enabled: true },
],
},
}),
],
},
});
});
test('Imports combined data params as form url-encoded', () => {
expect(convertCurl(`curl -d 'a=aaa&b=bbb&c' https://yaak.app`)).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
bodyType: 'application/x-www-form-urlencoded',
headers: [
{
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
enabled: true,
},
],
body: {
form: [
{ name: 'a', value: 'aaa', enabled: true },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: '', enabled: true },
],
},
}),
],
},
});
});
test('Imports data params as text', () => {
expect(
convertCurl('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }],
bodyType: 'text/plain',
body: { text: 'a&b&c=ccc' },
}),
],
},
});
});
test('Imports post data into URL', () => {
expect(
convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'GET',
url: 'https://api.stripe.com/v1/payment_links',
urlParameters: [{
enabled: true,
name: 'limit',
value: '3',
}],
}),
],
},
});
});
test('Imports multi-line JSON', () => {
expect(
convertCurl(`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'POST',
url: 'https://yaak.app',
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
bodyType: 'application/json',
body: { text: '{\n "foo":"bar"\n}' },
}),
],
},
});
});
test('Imports multiple headers', () => {
expect(
convertCurl('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
headers: [
{ name: 'Name', value: '', enabled: true },
{ name: 'Foo', value: 'bar', enabled: true },
{ name: 'AAA', value: 'bbb', enabled: true },
{ name: '', value: 'ccc', enabled: true },
],
}),
],
},
});
});
test('Imports basic auth', () => {
expect(convertCurl('curl --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
}),
],
},
});
});
test('Imports digest auth', () => {
expect(convertCurl('curl --digest --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
username: 'user',
password: 'pass',
},
}),
],
},
});
});
test('Imports cookie as header', () => {
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }],
}),
],
},
});
});
test('Imports query params', () => {
expect(convertCurl('curl "https://yaak.app" --url-query foo=bar --url-query baz=qux')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
urlParameters: [
{ name: 'foo', value: 'bar', enabled: true },
{ name: 'baz', value: 'qux', enabled: true },
],
}),
],
},
});
});
test('Imports query params from the URL', () => {
expect(convertCurl('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
urlParameters: [
{ name: 'foo', value: 'bar', enabled: true },
{ name: 'baz', value: 'a a', enabled: true },
],
}),
],
},
});
});
});
const idCount: Partial<Record<string, number>> = {};
function baseRequest(mergeWith: Partial<HttpRequest>) {
idCount.http_request = (idCount.http_request ?? -1) + 1;
return {
id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`,
model: 'http_request',
authentication: {},
authenticationType: null,
body: {},
bodyType: null,
folderId: null,
headers: [],
method: 'GET',
name: '',
sortPriority: 0,
url: '',
urlParameters: [],
workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
...mergeWith,
};
}
function baseWorkspace(mergeWith: Partial<Workspace> = {}) {
idCount.workspace = (idCount.workspace ?? -1) + 1;
return {
id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
model: 'workspace',
name: 'Curl Import',
...mergeWith,
};
}

View File

@@ -1,34 +0,0 @@
export function convertSyntax(variable: string): string {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}
export function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isJSString(obj: any) {
return Object.prototype.toString.call(obj) === '[object String]';
}
export function convertId(id: string): string {
if (id.startsWith('GENERATE_ID::')) {
return id;
}
return `GENERATE_ID::${id}`;
}
export function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
}

View File

@@ -1,35 +0,0 @@
import { Context, PluginDefinition } from '@yaakapp/api';
import YAML from 'yaml';
import { deleteUndefinedAttrs, isJSObject } from './common';
import { convertInsomniaV4 } from './v4';
import { convertInsomniaV5 } from './v5';
export const plugin: PluginDefinition = {
importer: {
name: 'Insomnia',
description: 'Import Insomnia workspaces',
async onImport(_ctx: Context, args: { text: string }) {
return convertInsomnia(args.text);
},
},
};
export function convertInsomnia(contents: string) {
let parsed: any;
try {
parsed = JSON.parse(contents);
} catch (e) {
}
try {
parsed = parsed ?? YAML.parse(contents);
} catch (e) {
}
if (!isJSObject(parsed)) return null;
const result = convertInsomniaV5(parsed) ?? convertInsomniaV4(parsed);
return deleteUndefinedAttrs(result);
}

View File

@@ -1,206 +0,0 @@
import { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV4(parsed: Record<string, any>) {
if (!Array.isArray(parsed.resources)) return null;
const resources: PartialImportResources = {
environments: [],
folders: [],
grpcRequests: [],
httpRequests: [],
websocketRequests: [],
workspaces: [],
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(r => isJSObject(r) && r._type === 'workspace');
for (const w of workspacesToImport) {
resources.workspaces.push({
id: convertId(w._id),
createdAt: w.created ? new Date(w.created).toISOString().replace('Z', '') : undefined,
updatedAt: w.updated ? new Date(w.updated).toISOString().replace('Z', '') : undefined,
model: 'workspace',
name: w.name,
description: w.description || undefined,
});
const environmentsToImport = parsed.resources.filter(
(r: any) => isJSObject(r) && r._type === 'environment',
);
resources.environments.push(
...environmentsToImport.map((r: any) => importEnvironment(r, w._id)),
);
const nextFolder = (parentId: string) => {
const children = parsed.resources.filter((r: any) => r.parentId === parentId);
for (const child of children) {
if (!isJSObject(child)) continue;
if (child._type === 'request_group') {
resources.folders.push(importFolder(child, w._id));
nextFolder(child._id);
} else if (child._type === 'request') {
resources.httpRequests.push(
importHttpRequest(child, w._id),
);
} else if (child._type === 'grpc_request') {
resources.grpcRequests.push(
importGrpcRequest(child, w._id),
);
}
}
};
// Import folders
nextFolder(w._id);
}
// Filter out any `null` values
resources.httpRequests = resources.httpRequests.filter(Boolean);
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
}
function importHttpRequest(
r: any,
workspaceId: string,
): PartialImportResources['httpRequests'][0] {
let bodyType: string | null = null;
let body = {};
if (r.body.mimeType === 'application/octet-stream') {
bodyType = 'binary';
body = { filePath: r.body.fileName ?? '' };
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
bodyType = 'application/x-www-form-urlencoded';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
};
} else if (r.body?.mimeType === 'multipart/form-data') {
bodyType = 'multipart/form-data';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
file: p.fileName ?? null,
})),
};
} else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') };
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') };
}
let authenticationType: string | null = null;
let authentication = {};
if (r.authentication.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
};
} else if (r.authentication.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
};
}
return {
id: convertId(r.meta?.id ?? r._id),
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'http_request',
sortPriority: r.metaSortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
body,
bodyType,
authentication,
authenticationType,
method: r.method,
headers: (r.headers ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== ''),
};
}
function importGrpcRequest(
r: any,
workspaceId: string,
): PartialImportResources['grpcRequests'][0] {
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
const service = parts[0] ?? null;
const method = parts[1] ?? null;
return {
id: convertId(r.meta?.id ?? r._id),
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
updatedAt: r.modified ? new Date(r.modified).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'grpc_request',
sortPriority: r.metaSortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
service,
method,
message: r.body?.text ?? '',
metadata: (r.metadata ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== ''),
};
}
function importFolder(f: any, workspaceId: string): PartialImportResources['folders'][0] {
return {
id: convertId(f._id),
createdAt: f.created ? new Date(f.created).toISOString().replace('Z', '') : undefined,
updatedAt: f.modified ? new Date(f.modified).toISOString().replace('Z', '') : undefined,
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
workspaceId: convertId(workspaceId),
description: f.description || undefined,
model: 'folder',
name: f.name,
};
}
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
return {
id: convertId(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
// @ts-ignore
sortPriority: e.metaSortKey, // Will be added to Yaak later
base: isParent ?? e.parentId === workspaceId,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}

View File

@@ -1,265 +0,0 @@
import { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV5(parsed: Record<string, any>) {
if (!Array.isArray(parsed.collection)) return null;
const resources: PartialImportResources = {
environments: [],
folders: [],
grpcRequests: [],
httpRequests: [],
websocketRequests: [],
workspaces: [],
};
// Import workspaces
const meta: Record<string, any> = parsed.meta ?? {};
resources.workspaces.push({
id: convertId(meta.id ?? 'collection'),
createdAt: meta.created ? new Date(meta.created).toISOString().replace('Z', '') : undefined,
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace('Z', '') : undefined,
model: 'workspace',
name: parsed.name,
description: meta.description || undefined,
});
resources.environments.push(
importEnvironment(parsed.environments, meta.id, true),
...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)),
);
const nextFolder = (children: any[], parentId: string) => {
for (const child of children ?? []) {
if (!isJSObject(child)) continue;
if (Array.isArray(child.children)) {
resources.folders.push(importFolder(child, meta.id, parentId));
nextFolder(child.children, child.meta.id);
} else if (child.method) {
resources.httpRequests.push(
importHttpRequest(child, meta.id, parentId),
);
} else if (child.protoFileId) {
resources.grpcRequests.push(
importGrpcRequest(child, meta.id, parentId),
);
} else if (child.url) {
resources.websocketRequests.push(
importWebsocketRequest(child, meta.id, parentId),
);
}
}
};
// Import folders
nextFolder(parsed.collection ?? [], meta.id);
// Filter out any `null` values
resources.httpRequests = resources.httpRequests.filter(Boolean);
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
}
function importHttpRequest(
r: any,
workspaceId: string,
parentId: string,
): PartialImportResources['httpRequests'][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
const updated = r.meta?.modified ?? r.updated;
const sortKey = r.meta?.sortKey ?? r.sortKey;
let bodyType: string | null = null;
let body = {};
if (r.body?.mimeType === 'application/octet-stream') {
bodyType = 'binary';
body = { filePath: r.body.fileName ?? '' };
} else if (r.body?.mimeType === 'application/x-www-form-urlencoded') {
bodyType = 'application/x-www-form-urlencoded';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
};
} else if (r.body?.mimeType === 'multipart/form-data') {
bodyType = 'multipart/form-data';
body = {
form: (r.body.params ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
file: p.fileName ?? null,
})),
};
} else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') };
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') };
}
return {
id: convertId(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
model: 'http_request',
name: r.name,
description: r.meta?.description || undefined,
url: convertSyntax(r.url),
body,
bodyType,
method: r.method,
...importHeaders(r),
...importAuthentication(r),
};
}
function importGrpcRequest(
r: any,
workspaceId: string,
parentId: string,
): PartialImportResources['grpcRequests'][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
const updated = r.meta?.modified ?? r.updated;
const sortKey = r.meta?.sortKey ?? r.sortKey;
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
const service = parts[0] ?? null;
const method = parts[1] ?? null;
return {
model: 'grpc_request',
id: convertId(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
service,
method,
message: r.body?.text ?? '',
metadata: (r.metadata ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== ''),
};
}
function importWebsocketRequest(
r: any,
workspaceId: string,
parentId: string,
): PartialImportResources['websocketRequests'][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
const updated = r.meta?.modified ?? r.updated;
const sortKey = r.meta?.sortKey ?? r.sortKey;
return {
model: 'websocket_request',
id: convertId(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
message: r.body?.text ?? '',
...importHeaders(r),
...importAuthentication(r),
};
}
function importHeaders(r: any) {
const headers = (r.headers ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
value: h.value ?? '',
}))
.filter(({ name, value }: any) => name !== '' || value !== '');
return { headers } as const;
}
function importAuthentication(r: any) {
let authenticationType: string | null = null;
let authentication = {};
if (r.authentication?.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
};
} else if (r.authentication?.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
};
}
return { authenticationType, authentication } as const;
}
function importFolder(f: any, workspaceId: string, parentId: string): PartialImportResources['folders'][0] {
const id = f.meta?.id ?? f._id;
const created = f.meta?.created ?? f.created;
const updated = f.meta?.modified ?? f.updated;
const sortKey = f.meta?.sortKey ?? f.sortKey;
return {
model: 'folder',
id: convertId(id),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
workspaceId: convertId(workspaceId),
description: f.description || undefined,
name: f.name,
};
}
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
const id = e.meta?.id ?? e._id;
const created = e.meta?.created ?? e.created;
const updated = e.meta?.modified ?? e.updated;
const sortKey = e.meta?.sortKey ?? e.sortKey;
return {
id: convertId(id),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
public: !e.isPrivate,
// @ts-ignore
sortPriority: sortKey, // Will be added to Yaak later
base: isParent ?? e.parentId === workspaceId,
model: 'environment',
name: e.name,
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}

View File

@@ -1,187 +0,0 @@
{
"_type": "export",
"__export_format": 4,
"__export_date": "2025-01-13T15:19:18.330Z",
"__export_source": "insomnia.desktop.app:v10.3.0",
"resources": [
{
"_id": "req_84cd9ae4bd034dd8bb730e856a665cbb",
"parentId": "fld_859d1df78261463480b6a3a1419517e3",
"modified": 1736781473176,
"created": 1736781406672,
"url": "{{ _.BASE_URL }}/foo/:id",
"name": "New Request",
"description": "My description of the request",
"method": "GET",
"body": {
"mimeType": "multipart/form-data",
"params": [
{
"id": "pair_7c86036ae8ef499dbbc0b43d0800c5a3",
"name": "form",
"value": "data",
"description": "",
"disabled": false
}
]
},
"parameters": [
{
"id": "pair_b22f6ff611cd4250a6e405ca7b713d09",
"name": "query",
"value": "qqq",
"description": "",
"disabled": false
}
],
"headers": [
{
"name": "Content-Type",
"value": "multipart/form-data",
"id": "pair_4af845963bd14256b98716617971eecd"
},
{
"name": "User-Agent",
"value": "insomnia/10.3.0",
"id": "pair_535ffd00ce48462cb1b7258832ade65a"
},
{
"id": "pair_ab4b870278e943cba6babf5a73e213e3",
"name": "X-Header",
"value": "xxxx",
"description": "",
"disabled": false
}
],
"authentication": {
"type": "basic",
"useISO88591": false,
"disabled": false,
"username": "user",
"password": "pass"
},
"metaSortKey": -1736781406672,
"isPrivate": false,
"pathParameters": [
{
"name": "id",
"value": "iii"
}
],
"settingStoreCookies": true,
"settingSendCookies": true,
"settingDisableRenderRequestBody": false,
"settingEncodeUrl": true,
"settingRebuildPath": true,
"settingFollowRedirects": "global",
"_type": "request"
},
{
"_id": "fld_859d1df78261463480b6a3a1419517e3",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781404718,
"created": 1736781404718,
"name": "Top Level",
"description": "",
"environment": {},
"environmentPropertyOrder": null,
"metaSortKey": -1736781404718,
"environmentType": "kv",
"_type": "request_group"
},
{
"_id": "wrk_d4d92f7c0ee947b89159243506687019",
"parentId": null,
"modified": 1736781343765,
"created": 1736781343765,
"name": "Dummy",
"description": "",
"scope": "collection",
"_type": "workspace"
},
{
"_id": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781355209,
"created": 1736781343767,
"name": "Base Environment",
"data": {
"BASE_VAR": "hello"
},
"dataPropertyOrder": null,
"color": null,
"isPrivate": false,
"metaSortKey": 1736781343767,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_61c1be66d42241b5a28306d2cd92d3e3",
"name": "BASE_VAR",
"value": "hello",
"type": "str",
"enabled": true
}
],
"_type": "environment"
},
{
"_id": "jar_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781343768,
"created": 1736781343768,
"name": "Default Jar",
"cookies": [],
"_type": "cookie_jar"
},
{
"_id": "env_799ae3d723ef44af91b4817e5d057e6d",
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"modified": 1736781394705,
"created": 1736781358515,
"name": "Production",
"data": {
"BASE_URL": "https://api.yaak.app"
},
"dataPropertyOrder": null,
"color": "#f22c2c",
"isPrivate": false,
"metaSortKey": 1736781358515,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
"name": "BASE_URL",
"value": "https://api.yaak.app",
"type": "str",
"enabled": true
}
],
"_type": "environment"
},
{
"_id": "env_030fbfdbb274426ebd78e2e6518f8553",
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"modified": 1736781391078,
"created": 1736781374707,
"name": "Staging",
"data": {
"BASE_URL": "https://api.staging.yaak.app"
},
"dataPropertyOrder": null,
"color": "#206fac",
"isPrivate": false,
"metaSortKey": 1736781358565,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
"name": "BASE_URL",
"value": "https://api.staging.yaak.app",
"type": "str",
"enabled": true
}
],
"_type": "environment"
}
]
}

View File

@@ -1,126 +0,0 @@
{
"resources": {
"environments": [
{
"createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209",
"sortPriority": 1736781343767,
"base": true,
"id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"model": "environment",
"name": "Base Environment",
"variables": [
{
"enabled": true,
"name": "BASE_VAR",
"value": "hello"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
},
{
"createdAt": "2025-01-13T15:15:58.515",
"updatedAt": "2025-01-13T15:16:34.705",
"sortPriority": 1736781358515,
"base": false,
"id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d",
"model": "environment",
"name": "Production",
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
},
{
"createdAt": "2025-01-13T15:16:14.707",
"updatedAt": "2025-01-13T15:16:31.078",
"sortPriority": 1736781358565,
"base": false,
"id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553",
"model": "environment",
"name": "Staging",
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.staging.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"folders": [
{
"createdAt": "2025-01-13T15:16:44.718",
"updatedAt": "2025-01-13T15:16:44.718",
"folderId": null,
"id": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
"model": "folder",
"name": "Top Level",
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"grpcRequests": [],
"httpRequests": [
{
"authentication": {
"password": "pass",
"username": "user"
},
"authenticationType": "basic",
"body": {
"form": [
{
"enabled": true,
"file": null,
"name": "form",
"value": "data"
}
]
},
"bodyType": "multipart/form-data",
"createdAt": "2025-01-13T15:16:46.672",
"sortPriority": -1736781406672,
"updatedAt": "2025-01-13T15:17:53.176",
"description": "My description of the request",
"folderId": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
"headers": [
{
"enabled": true,
"name": "Content-Type",
"value": "multipart/form-data"
},
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/10.3.0"
},
{
"enabled": true,
"name": "X-Header",
"value": "xxxx"
}
],
"id": "GENERATE_ID::req_84cd9ae4bd034dd8bb730e856a665cbb",
"method": "GET",
"model": "http_request",
"name": "New Request",
"url": "${[BASE_URL ]}/foo/:id",
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"websocketRequests": [],
"workspaces": [
{
"createdAt": "2025-01-13T15:15:43.765",
"id": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019",
"model": "workspace",
"name": "Dummy"
}
]
}
}

View File

@@ -1,72 +0,0 @@
type: collection.insomnia.rest/5.0
name: Debugging
meta:
id: wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c
created: 1747197924902
modified: 1747197924902
collection:
- name: My Folder
meta:
id: fld_296933ea4ea84783a775d199997e9be7
created: 1747414092298
modified: 1747414142427
sortKey: -1747414092298
children:
- url: https://httpbin.org/post
name: New Request
meta:
id: req_9a80320365ac4509ade406359dbc6a71
created: 1747197928502
modified: 1747414129313
isPrivate: false
sortKey: -1747414129276
method: GET
headers:
- name: User-Agent
value: insomnia/11.1.0
id: pair_6ae87d1620a9494f8e5b29cd9f92d087
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
headers:
- id: pair_f2b330e3914f4c11b209318aef94325c
name: foo
value: bar
disabled: false
- name: New Request
meta:
id: req_e3f8cdbd58784a539dd4c1e127d73451
created: 1747414160497
modified: 1747414160497
isPrivate: false
sortKey: -1747414160498
method: GET
headers:
- name: User-Agent
value: insomnia/11.1.0
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_e46dc73e8ccda30ca132153e8f11183bd08119ce
created: 1747197924904
modified: 1747197924904
environments:
name: Base Environment
meta:
id: env_e46dc73e8ccda30ca132153e8f11183bd08119ce
created: 1747197924903
modified: 1747197924903
isPrivate: false

View File

@@ -1,87 +0,0 @@
{
"resources": {
"environments": [
{
"base": true,
"createdAt": "2025-05-14T04:45:24.903",
"id": "GENERATE_ID::env_e46dc73e8ccda30ca132153e8f11183bd08119ce",
"model": "environment",
"name": "Base Environment",
"public": true,
"updatedAt": "2025-05-14T04:45:24.903",
"variables": [],
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}
],
"folders": [
{
"createdAt": "2025-05-16T16:48:12.298",
"folderId": null,
"id": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
"model": "folder",
"name": "My Folder",
"sortPriority": -1747414092298,
"updatedAt": "2025-05-16T16:49:02.427",
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}
],
"grpcRequests": [],
"httpRequests": [
{
"authentication": {},
"authenticationType": null,
"body": {},
"bodyType": null,
"createdAt": "2025-05-14T04:45:28.502",
"folderId": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
"headers": [
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/11.1.0"
}
],
"id": "GENERATE_ID::req_9a80320365ac4509ade406359dbc6a71",
"method": "GET",
"model": "http_request",
"name": "New Request",
"sortPriority": -1747414129276,
"updatedAt": "2025-05-16T16:48:49.313",
"url": "https://httpbin.org/post",
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
},
{
"authentication": {},
"authenticationType": null,
"body": {},
"bodyType": null,
"createdAt": "2025-05-16T16:49:20.497",
"folderId": null,
"headers": [
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/11.1.0"
}
],
"id": "GENERATE_ID::req_e3f8cdbd58784a539dd4c1e127d73451",
"method": "GET",
"model": "http_request",
"name": "New Request",
"sortPriority": -1747414160498,
"updatedAt": "2025-05-16T16:49:20.497",
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}
],
"websocketRequests": [],
"workspaces": [
{
"createdAt": "2025-05-14T04:45:24.902",
"id": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c",
"model": "workspace",
"name": "Debugging",
"updatedAt": "2025-05-14T04:45:24.902"
}
]
}
}

View File

@@ -1,142 +0,0 @@
type: collection.insomnia.rest/5.0
name: Dummy
meta:
id: wrk_c1eacfa750a04f3ea9985ef28043fa53
created: 1746799305927
modified: 1746843054272
description: This is the description
collection:
- name: Top Level
meta:
id: fld_42eb2e2bb22b4cedacbd3d057634e80c
created: 1736781404718
modified: 1736781404718
sortKey: -1736781404718
children:
- url: "{{ _.BASE_URL }}/foo/:id"
name: New Request
meta:
id: req_d72fff2a6b104b91a2ebe9de9edd2785
created: 1736781406672
modified: 1736781473176
isPrivate: false
description: My description of the request
sortKey: -1736781406672
method: GET
body:
mimeType: multipart/form-data
params:
- id: pair_7c86036ae8ef499dbbc0b43d0800c5a3
name: form
value: data
disabled: false
parameters:
- id: pair_b22f6ff611cd4250a6e405ca7b713d09
name: query
value: qqq
disabled: false
headers:
- name: Content-Type
value: multipart/form-data
id: pair_4af845963bd14256b98716617971eecd
- name: User-Agent
value: insomnia/10.3.0
id: pair_535ffd00ce48462cb1b7258832ade65a
- id: pair_ab4b870278e943cba6babf5a73e213e3
name: X-Header
value: xxxx
disabled: false
authentication:
type: basic
useISO88591: false
disabled: false
username: user
password: pass
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
pathParameters:
- name: id
value: iii
- url: grpcb.in:9000
name: New Request
meta:
id: greq_06d659324df94504a4d64632be7106b3
created: 1746799344864
modified: 1746799544082
isPrivate: false
sortKey: -1746799344864
body:
text: |-
{
"greeting": "Greg"
}
protoFileId: pf_9d45b0dfaccc4bcc9d930746716786c5
protoMethodName: /hello.HelloService/SayHello
reflectionApi:
enabled: false
url: https://buf.build
module: buf.build/connectrpc/eliza
- url: wss://echo.websocket.org
name: New WebSocket Request
meta:
id: ws-req_5d1a4c7c79494743962e5176f6add270
created: 1746799553909
modified: 1746887120958
sortKey: -1746799553909
settings:
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
authentication:
type: basic
useISO88591: false
disabled: false
username: user
password: password
headers:
- name: User-Agent
value: insomnia/11.1.0
cookieJar:
name: Default Jar
meta:
id: jar_663d5741b072441aa2709a6113371510
created: 1736781343768
modified: 1736781343768
environments:
name: Base Environment
meta:
id: env_20945044d3c8497ca8b717bef750987e
created: 1736781343767
modified: 1736781355209
isPrivate: false
data:
BASE_VAR: hello
subEnvironments:
- name: Production
meta:
id: env_6f7728bb7fc04d558d668e954d756ea2
created: 1736781358515
modified: 1736781394705
isPrivate: false
sortKey: 1736781358515
data:
BASE_URL: https://api.yaak.app
color: "#f22c2c"
- name: Staging
meta:
id: env_976a8b6eb5d44fb6a20150f65c32d243
created: 1736781374707
modified: 1736781391078
isPrivate: false
sortKey: 1736781358565
data:
BASE_URL: https://api.staging.yaak.app
color: "#206fac"

View File

@@ -1,172 +0,0 @@
{
"resources": {
"environments": [
{
"createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209",
"base": true,
"public": true,
"id": "GENERATE_ID::env_20945044d3c8497ca8b717bef750987e",
"model": "environment",
"name": "Base Environment",
"variables": [
{
"enabled": true,
"name": "BASE_VAR",
"value": "hello"
}
],
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
},
{
"createdAt": "2025-01-13T15:15:58.515",
"updatedAt": "2025-01-13T15:16:34.705",
"base": false,
"public": true,
"id": "GENERATE_ID::env_6f7728bb7fc04d558d668e954d756ea2",
"model": "environment",
"name": "Production",
"sortPriority": 1736781358515,
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
},
{
"createdAt": "2025-01-13T15:16:14.707",
"updatedAt": "2025-01-13T15:16:31.078",
"base": false,
"public": true,
"id": "GENERATE_ID::env_976a8b6eb5d44fb6a20150f65c32d243",
"model": "environment",
"name": "Staging",
"sortPriority": 1736781358565,
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.staging.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
],
"folders": [
{
"createdAt": "2025-01-13T15:16:44.718",
"updatedAt": "2025-01-13T15:16:44.718",
"folderId": null,
"id": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c",
"model": "folder",
"name": "Top Level",
"sortPriority": -1736781404718,
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
],
"grpcRequests": [
{
"model": "grpc_request",
"createdAt": "2025-05-09T14:02:24.864",
"folderId": null,
"id": "GENERATE_ID::greq_06d659324df94504a4d64632be7106b3",
"message": "{\n\t\"greeting\": \"Greg\"\n}",
"metadata": [],
"method": "SayHello",
"name": "New Request",
"service": "hello.HelloService",
"sortPriority": -1746799344864,
"updatedAt": "2025-05-09T14:05:44.082",
"url": "grpcb.in:9000",
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
],
"httpRequests": [
{
"authentication": {
"password": "pass",
"username": "user"
},
"authenticationType": "basic",
"body": {
"form": [
{
"enabled": true,
"file": null,
"name": "form",
"value": "data"
}
]
},
"bodyType": "multipart/form-data",
"createdAt": "2025-01-13T15:16:46.672",
"updatedAt": "2025-01-13T15:17:53.176",
"description": "My description of the request",
"folderId": "GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c",
"headers": [
{
"enabled": true,
"name": "Content-Type",
"value": "multipart/form-data"
},
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/10.3.0"
},
{
"enabled": true,
"name": "X-Header",
"value": "xxxx"
}
],
"id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785",
"method": "GET",
"model": "http_request",
"name": "New Request",
"sortPriority": -1736781406672,
"url": "${[BASE_URL ]}/foo/:id",
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
],
"websocketRequests": [
{
"id": "GENERATE_ID::ws-req_5d1a4c7c79494743962e5176f6add270",
"createdAt": "2025-05-09T14:05:53.909",
"updatedAt": "2025-05-10T14:25:20.958",
"message": "",
"model": "websocket_request",
"name": "New WebSocket Request",
"sortPriority": -1746799553909,
"authenticationType": "basic",
"authentication": {
"password": "password",
"username": "user"
},
"folderId": null,
"headers": [
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/11.1.0"
}
],
"url": "wss://echo.websocket.org",
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
],
"workspaces": [
{
"createdAt": "2025-05-09T14:01:45.927",
"updatedAt": "2025-05-10T02:10:54.272",
"description": "This is the description",
"id": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53",
"model": "workspace",
"name": "Dummy"
}
]
}
}

View File

@@ -1,32 +0,0 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import YAML from 'yaml';
import { convertInsomnia } from '../src';
describe('importer-yaak', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
if (fixture.includes('.output')) {
continue;
}
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace(/.input\..*/, '.output.json')), 'utf-8');
const result = convertInsomnia(contents);
// console.log(JSON.stringify(result, null, 2))
expect(result).toEqual(parseJsonOrYaml(expected));
});
}
});
function parseJsonOrYaml(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return YAML.parse(text);
}
}

View File

@@ -1,35 +0,0 @@
import { convertPostman } from '@yaak/importer-postman/src';
import type { Context, PluginDefinition } from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
import { convert } from 'openapi-to-postmanv2';
export const plugin: PluginDefinition = {
importer: {
name: 'OpenAPI',
description: 'Import OpenAPI collections',
onImport(_ctx: Context, args: { text: string }) {
return convertOpenApi(args.text);
},
},
};
export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
let postmanCollection;
try {
postmanCollection = await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
convert({ type: 'string', data: contents }, {}, (err, result: any) => {
if (err != null) reject(err);
if (Array.isArray(result.output) && result.output.length > 0) {
resolve(result.output[0].data);
}
});
});
} catch {
// Probably not an OpenAPI file, so skip it
return undefined;
}
return convertPostman(JSON.stringify(postmanCollection));
}

View File

@@ -1,819 +0,0 @@
openapi: 3.0.2
servers:
- url: /v3
info:
description: |-
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
version: 1.0.20-SNAPSHOT
title: Swagger Petstore - OpenAPI 3.0
termsOfService: 'http://swagger.io/terms/'
contact:
email: apiteam@swagger.io
license:
name: Apache 2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: 'http://swagger.io'
- name: store
description: Access to Petstore orders
externalDocs:
description: Find out more about our store
url: 'http://swagger.io'
- name: user
description: Operations about user
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: Add a new pet to the store
operationId: addPet
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
description: Create a new pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Pet'
put:
tags:
- pet
summary: Update an existing pet
description: Update an existing pet by Id
operationId: updatePet
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
description: Update an existent pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Pet'
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: false
explode: true
schema:
type: string
enum:
- available
- pending
- sold
default: available
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid status value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
/pet/findByTags:
get:
tags:
- pet
summary: Finds Pets by tags
description: >-
Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: false
explode: true
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid tag value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
- petstore_auth:
- 'write:pets'
- 'read:pets'
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
- name: name
in: query
description: Name of pet that needs to be updated
schema:
type: string
- name: status
in: query
description: Status of pet that needs to be updated
schema:
type: string
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
parameters:
- name: api_key
in: header
description: ''
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
- name: additionalMetadata
in: query
description: Additional Metadata
required: false
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
/store/inventory:
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
x-swagger-router-controller: OrderController
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/store/order:
post:
tags:
- store
summary: Place an order for a pet
description: Place a new order in the store
operationId: placeOrder
x-swagger-router-controller: OrderController
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'405':
description: Invalid input
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Order'
'/store/order/{orderId}':
get:
tags:
- store
summary: Find purchase order by ID
x-swagger-router-controller: OrderController
description: >-
For valid response try integer IDs with value <= 5 or > 10. Other values
will generate exceptions.
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of order that needs to be fetched
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
x-swagger-router-controller: OrderController
description: >-
For valid response try integer IDs with value < 1000. Anything above
1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
/user:
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
responses:
default:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/User'
description: Created user object
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: 'Creates list of users with given input array'
x-swagger-router-controller: UserController
operationId: createUsersWithListInput
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
default:
description: successful operation
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: false
schema:
type: string
- name: password
in: query
description: The password for login in clear text
required: false
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
'400':
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
parameters: []
responses:
default:
description: successful operation
'/user/{username}':
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing. '
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid username supplied
'404':
description: User not found
put:
tags:
- user
summary: Update user
x-swagger-router-controller: UserController
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that needs to be updated
required: true
schema:
type: string
responses:
default:
description: successful operation
requestBody:
description: Update an existent user in the store
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/User'
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
components:
schemas:
Order:
x-swagger-router-model: io.swagger.petstore.model.Order
properties:
id:
type: integer
format: int64
example: 10
petId:
type: integer
format: int64
example: 198772
quantity:
type: integer
format: int32
example: 7
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
example: approved
complete:
type: boolean
xml:
name: order
type: object
Customer:
properties:
id:
type: integer
format: int64
example: 100000
username:
type: string
example: fehguy
address:
type: array
items:
$ref: '#/components/schemas/Address'
xml:
wrapped: true
name: addresses
xml:
name: customer
type: object
Address:
properties:
street:
type: string
example: 437 Lytton
city:
type: string
example: Palo Alto
state:
type: string
example: CA
zip:
type: string
example: 94301
xml:
name: address
type: object
Category:
x-swagger-router-model: io.swagger.petstore.model.Category
properties:
id:
type: integer
format: int64
example: 1
name:
type: string
example: Dogs
xml:
name: category
type: object
User:
x-swagger-router-model: io.swagger.petstore.model.User
properties:
id:
type: integer
format: int64
example: 10
username:
type: string
example: theUser
firstName:
type: string
example: John
lastName:
type: string
example: James
email:
type: string
example: john@email.com
password:
type: string
example: 12345
phone:
type: string
example: 12345
userStatus:
type: integer
format: int32
example: 1
description: User Status
xml:
name: user
type: object
Tag:
x-swagger-router-model: io.swagger.petstore.model.Tag
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: tag
type: object
Pet:
x-swagger-router-model: io.swagger.petstore.model.Pet
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
category:
$ref: '#/components/schemas/Category'
photoUrls:
type: array
xml:
wrapped: true
items:
type: string
xml:
name: photoUrl
tags:
type: array
xml:
wrapped: true
items:
$ref: '#/components/schemas/Tag'
xml:
name: tag
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: pet
type: object
ApiResponse:
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
xml:
name: '##default'
type: object
requestBodies:
Pet:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
UserArray:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
description: List of user object
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: 'https://petstore.swagger.io/oauth/authorize'
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
api_key:
type: apiKey
name: api_key
in: header

View File

@@ -1,29 +0,0 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { convertOpenApi } from '../src';
describe('importer-openapi', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
test('Skips invalid file', async () => {
const imported = await convertOpenApi('{}');
expect(imported).toBeUndefined();
});
for (const fixture of fixtures) {
test('Imports ' + fixture, async () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const imported = await convertOpenApi(contents);
expect(imported?.resources.workspaces).toEqual([
expect.objectContaining({
name: 'Swagger Petstore - OpenAPI 3.0',
description: expect.stringContaining('This is a sample Pet Store Server'),
}),
]);
expect(imported?.resources.httpRequests.length).toBe(19);
expect(imported?.resources.folders.length).toBe(7);
});
}
});

View File

@@ -1,403 +0,0 @@
import type {
Context,
Environment,
Folder,
HttpRequest,
HttpRequestHeader,
HttpUrlParameter,
PartialImportResources,
PluginDefinition,
Workspace,
} from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
const VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const plugin: PluginDefinition = {
importer: {
name: 'Postman',
description: 'Import postman collections',
onImport(_ctx: Context, args: { text: string }) {
return convertPostman(args.text);
},
},
};
export function convertPostman(contents: string): ImportPluginResponse | undefined {
const root = parseJSONToRecord(contents);
if (root == null) return;
const info = toRecord(root.info);
const isValidSchema = VALID_SCHEMAS.includes(
typeof info.schema === 'string' ? info.schema : 'n/a',
);
if (!isValidSchema || !Array.isArray(root.item)) {
return;
}
const globalAuth = importAuth(root.auth);
const exportResources: ExportResources = {
workspaces: [],
environments: [],
httpRequests: [],
folders: [],
};
const rawDescription = info.description;
const description =
typeof rawDescription === 'object' && rawDescription !== null && 'content' in rawDescription
? String(rawDescription.content)
: String(rawDescription);
const workspace: ExportResources['workspaces'][0] = {
model: 'workspace',
id: generateId('workspace'),
name: info.name ? String(info.name) : 'Postman Import',
description,
};
exportResources.workspaces.push(workspace);
// Create the base environment
const environment: ExportResources['environments'][0] = {
model: 'environment',
id: generateId('environment'),
name: 'Global Variables',
workspaceId: workspace.id,
variables:
toArray<{ key: string; value: string }>(root.variable).map((v) => ({
name: v.key,
value: v.value,
})) ?? [],
};
exportResources.environments.push(environment);
const importItem = (v: Record<string, unknown>, folderId: string | null = null) => {
if (typeof v.name === 'string' && Array.isArray(v.item)) {
const folder: ExportResources['folders'][0] = {
model: 'folder',
workspaceId: workspace.id,
id: generateId('folder'),
name: v.name,
folderId,
};
exportResources.folders.push(folder);
for (const child of v.item) {
importItem(child, folder.id);
}
} else if (typeof v.name === 'string' && 'request' in v) {
const r = toRecord(v.request);
const bodyPatch = importBody(r.body);
const requestAuthPath = importAuth(r.auth);
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
const headers: HttpRequestHeader[] = toArray<{
key: string;
value: string;
disabled?: boolean;
}>(r.header).map((h) => {
return {
name: h.key,
value: h.value,
enabled: !h.disabled,
};
});
// Add body headers only if they don't already exist
for (const bodyPatchHeader of bodyPatch.headers) {
const existingHeader = headers.find(
(h) => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase(),
);
if (existingHeader) {
continue;
}
headers.push(bodyPatchHeader);
}
const { url, urlParameters } = convertUrl(r.url);
const request: ExportResources['httpRequests'][0] = {
model: 'http_request',
id: generateId('http_request'),
workspaceId: workspace.id,
folderId,
name: v.name,
description: v.description ? String(v.description) : undefined,
method: typeof r.method === 'string' ? r.method : 'GET',
url,
urlParameters,
body: bodyPatch.body,
bodyType: bodyPatch.bodyType,
authentication: authPatch.authentication,
authenticationType: authPatch.authenticationType,
headers,
};
exportResources.httpRequests.push(request);
} else {
console.log('Unknown item', v, folderId);
}
};
for (const item of root.item) {
importItem(item);
}
const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
}
function convertUrl(rawUrl: string | unknown): Pick<HttpRequest, 'url' | 'urlParameters'> {
if (typeof rawUrl === 'string') {
return { url: rawUrl, urlParameters: [] };
}
const url = toRecord(rawUrl);
let v = '';
if ('protocol' in url && typeof url.protocol === 'string') {
v += `${url.protocol}://`;
}
if ('host' in url) {
v += `${Array.isArray(url.host) ? url.host.join('.') : url.host}`;
}
if ('port' in url && typeof url.port === 'string') {
v += `:${url.port}`;
}
if ('path' in url && Array.isArray(url.path) && url.path.length > 0) {
v += `/${Array.isArray(url.path) ? url.path.join('/') : url.path}`;
}
const params: HttpUrlParameter[] = [];
if ('query' in url && Array.isArray(url.query) && url.query.length > 0) {
for (const query of url.query) {
params.push({
name: query.key ?? '',
value: query.value ?? '',
enabled: !query.disabled,
});
}
}
if ('variable' in url && Array.isArray(url.variable) && url.variable.length > 0) {
for (const v of url.variable) {
params.push({
name: ':' + (v.key ?? ''),
value: v.value ?? '',
enabled: !v.disabled,
});
}
}
if ('hash' in url && typeof url.hash === 'string') {
v += `#${url.hash}`;
}
// TODO: Implement url.variables (path variables)
return { url: v, urlParameters: params };
}
function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
const auth = toRecord<{ username?: string; password?: string; token?: string }>(rawAuth);
if ('basic' in auth) {
return {
authenticationType: 'basic',
authentication: {
username: auth.basic.username || '',
password: auth.basic.password || '',
},
};
} else if ('bearer' in auth) {
return {
authenticationType: 'bearer',
authentication: {
token: auth.bearer.token || '',
},
};
} else {
return { authenticationType: null, authentication: {} };
}
}
function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
const body = toRecord(rawBody) as {
mode: string;
graphql: { query?: string; variables?: string };
urlencoded?: { key?: string; value?: string; disabled?: boolean }[];
formdata?: {
key?: string;
value?: string;
disabled?: boolean;
contentType?: string;
src?: string;
}[];
raw?: string;
options?: { raw?: { language?: string } };
file?: { src?: string };
};
if (body.mode === 'graphql') {
return {
headers: [
{
name: 'Content-Type',
value: 'application/json',
enabled: true,
},
],
bodyType: 'graphql',
body: {
text: JSON.stringify(
{
query: body.graphql?.query || '',
variables: parseJSONToRecord(body.graphql?.variables || '{}'),
},
null,
2,
),
},
};
} else if (body.mode === 'urlencoded') {
return {
headers: [
{
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
enabled: true,
},
],
bodyType: 'application/x-www-form-urlencoded',
body: {
form: toArray<NonNullable<typeof body.urlencoded>[0]>(body.urlencoded).map((f) => ({
enabled: !f.disabled,
name: f.key ?? '',
value: f.value ?? '',
})),
},
};
} else if (body.mode === 'formdata') {
return {
headers: [
{
name: 'Content-Type',
value: 'multipart/form-data',
enabled: true,
},
],
bodyType: 'multipart/form-data',
body: {
form: toArray<NonNullable<typeof body.formdata>[0]>(body.formdata).map((f) =>
f.src != null
? {
enabled: !f.disabled,
contentType: f.contentType ?? null,
name: f.key ?? '',
file: f.src ?? '',
}
: {
enabled: !f.disabled,
name: f.key ?? '',
value: f.value ?? '',
},
),
},
};
} else if (body.mode === 'raw') {
return {
headers: [
{
name: 'Content-Type',
value: body.options?.raw?.language === 'json' ? 'application/json' : '',
enabled: true,
},
],
bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other',
body: {
text: body.raw ?? '',
},
};
} else if (body.mode === 'file') {
return {
headers: [],
bodyType: 'binary',
body: {
filePath: body.file?.src,
},
};
} else {
return { headers: [], bodyType: null, body: {} };
}
}
function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
try {
return toRecord(JSON.parse(jsonStr));
} catch {
return null;
}
}
function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, T>;
}
return {};
}
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [];
}
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
}
function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
}
const idCount: Partial<Record<string, number>> = {};
function generateId(model: string): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}

View File

@@ -1,38 +0,0 @@
{
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "Top Folder",
"item": [
{
"name": "Nested Folder",
"item": [
{
"name": "Request 1",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 2",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 3",
"request": {
"method": "GET"
}
}
]
}

View File

@@ -1,83 +0,0 @@
{
"resources": {
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_0",
"name": "New Collection"
}
],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment",
"name": "Global Variables",
"variables": [],
"workspaceId": "GENERATE_ID::WORKSPACE_0"
}
],
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_0",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": "GENERATE_ID::FOLDER_1",
"name": "Request 1",
"method": "GET",
"url": "",
"urlParameters": [],
"body": {},
"bodyType": null,
"authentication": {},
"authenticationType": null,
"headers": []
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_1",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": "GENERATE_ID::FOLDER_0",
"name": "Request 2",
"method": "GET",
"url": "",
"urlParameters": [],
"body": {},
"bodyType": null,
"authentication": {},
"authenticationType": null,
"headers": []
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_2",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "Request 3",
"method": "GET",
"url": "",
"urlParameters": [],
"body": {},
"bodyType": null,
"authentication": {},
"authenticationType": null,
"headers": []
}
],
"folders": [
{
"model": "folder",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"id": "GENERATE_ID::FOLDER_0",
"name": "Top Folder",
"folderId": null
},
{
"model": "folder",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"id": "GENERATE_ID::FOLDER_1",
"name": "Nested Folder",
"folderId": "GENERATE_ID::FOLDER_0"
}
]
}
}

View File

@@ -1,136 +0,0 @@
{
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "Form URL",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "baeare",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
"key": "X-foo",
"value": "bar",
"description": "description"
},
{
"key": "Disabled",
"value": "tnroant",
"description": "ntisorantosra",
"disabled": true
}
],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "Key",
"contentType": "Custom/COntent",
"description": "DEscription",
"type": "file",
"src": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11PM.png"
}
]
},
"url": {
"raw": "example.com/:foo/:bar?q=qqq&",
"host": [
"example",
"com"
],
"path": [
":foo",
":bar"
],
"query": [
{
"key": "disabled",
"value": "secondvalue",
"description": "this is disabled",
"disabled": true
},
{
"key": "q",
"value": "qqq",
"description": "hello"
},
{
"key": "",
"value": null
}
],
"variable": [
{
"key": "foo",
"value": "fff",
"description": "Description"
},
{
"key": "bar",
"value": "bbb",
"description": "bbb description"
}
]
}
},
"response": []
}
],
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "globalpass",
"type": "string"
},
{
"key": "username",
"value": "globaluser",
"type": "string"
}
]
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"packages": {},
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"packages": {},
"exec": [
""
]
}
}
],
"variable": [
{
"key": "COLLECTION VARIABLE",
"value": "collection variable",
"type": "string"
}
]
}

View File

@@ -1,96 +0,0 @@
{
"resources": {
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_1",
"name": "New Collection"
}
],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_1",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"model": "environment",
"name": "Global Variables",
"variables": [
{
"name": "COLLECTION VARIABLE",
"value": "collection variable"
}
]
}
],
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_3",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"folderId": null,
"name": "Form URL",
"method": "POST",
"url": "example.com/:foo/:bar",
"urlParameters": [
{
"name": "disabled",
"value": "secondvalue",
"enabled": false
},
{
"name": "q",
"value": "qqq",
"enabled": true
},
{
"name": "",
"value": "",
"enabled": true
},
{
"name": ":foo",
"value": "fff",
"enabled": true
},
{
"name": ":bar",
"value": "bbb",
"enabled": true
}
],
"body": {
"form": [
{
"enabled": true,
"contentType": "Custom/COntent",
"name": "Key",
"file": "/Users/gschier/Desktop/Screenshot 2024-05-31 at 12.05.11PM.png"
}
]
},
"bodyType": "multipart/form-data",
"authentication": {
"token": ""
},
"authenticationType": "bearer",
"headers": [
{
"name": "X-foo",
"value": "bar",
"enabled": true
},
{
"name": "Disabled",
"value": "tnroant",
"enabled": false
},
{
"name": "Content-Type",
"value": "multipart/form-data",
"enabled": true
}
]
}
],
"folders": []
}
}

View File

@@ -1,23 +0,0 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { convertPostman } from '../src';
describe('importer-postman', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
if (fixture.includes('.output')) {
continue;
}
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostman(contents);
// console.log(JSON.stringify(result, null, 2))
expect(result).toEqual(JSON.parse(expected));
});
}
});

View File

@@ -1,74 +0,0 @@
import { Environment, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
importer: {
name: 'Yaak',
description: 'Yaak official format',
onImport(_ctx, args) {
return migrateImport(args.text) as any;
},
},
};
export function migrateImport(contents: string) {
let parsed;
try {
parsed = JSON.parse(contents);
} catch (err) {
return undefined;
}
if (!isJSObject(parsed)) {
return undefined;
}
const isYaakExport = 'yaakSchema' in parsed;
if (!isYaakExport) {
return;
}
// Migrate v1 to v2 -- changes requests to httpRequests
if ('requests' in parsed.resources) {
parsed.resources.httpRequests = parsed.resources.requests;
delete parsed.resources['requests'];
}
// Migrate v2 to v3
for (const workspace of parsed.resources.workspaces ?? []) {
if ('variables' in workspace) {
// Create the base environment
const baseEnvironment: Partial<Environment> = {
id: `GENERATE_ID::base_env_${workspace['id']}`,
name: 'Global Variables',
variables: workspace.variables,
workspaceId: workspace.id,
};
parsed.resources.environments = parsed.resources.environments ?? [];
parsed.resources.environments.push(baseEnvironment);
// Delete variables key from the workspace
delete workspace.variables;
// Add environmentId to relevant environments
for (const environment of parsed.resources.environments) {
if (environment.workspaceId === workspace.id && environment.id !== baseEnvironment.id) {
environment.environmentId = baseEnvironment.id;
}
}
}
}
// Migrate v3 to v4
for (const environment of parsed.resources.environments ?? []) {
if ('environmentId' in environment) {
environment.base = environment.environmentId == null;
delete environment.environmentId;
}
}
return { resources: parsed.resources }; // Should already be in the correct format
}
function isJSObject(obj: any) {
return Object.prototype.toString.call(obj) === '[object Object]';
}

View File

@@ -1,70 +0,0 @@
import { describe, expect, test } from 'vitest';
import { migrateImport } from '../src';
describe('importer-yaak', () => {
test('Skips invalid imports', () => {
expect(migrateImport('not JSON')).toBeUndefined();
expect(migrateImport('[]')).toBeUndefined();
expect(migrateImport(JSON.stringify({ resources: {} }))).toBeUndefined();
});
test('converts schema 1 to 2', () => {
const imported = migrateImport(
JSON.stringify({
yaakSchema: 1,
resources: {
requests: [],
},
}),
);
expect(imported).toEqual(
expect.objectContaining({
resources: {
httpRequests: [],
},
}),
);
});
test('converts schema 2 to 3', () => {
const imported = migrateImport(
JSON.stringify({
yaakSchema: 2,
resources: {
environments: [{
id: 'e_1',
workspaceId: 'w_1',
name: 'Production',
variables: [{ name: 'E1', value: 'E1!' }],
}],
workspaces: [{
id: 'w_1',
variables: [{ name: 'W1', value: 'W1!' }],
}],
},
}),
);
expect(imported).toEqual(
expect.objectContaining({
resources: {
workspaces: [{
id: 'w_1',
}],
environments: [{
id: 'e_1',
base: false,
workspaceId: 'w_1',
name: 'Production',
variables: [{ name: 'E1', value: 'E1!' }],
}, {
id: 'GENERATE_ID::base_env_w_1',
workspaceId: 'w_1',
name: 'Global Variables',
variables: [{ name: 'W1', value: 'W1!' }],
}],
},
}),
);
});
});

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-cookie",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,20 +0,0 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'cookie.value',
description: 'Read the value of a cookie in the jar, by name',
args: [
{
type: 'text',
name: 'cookie_name',
label: 'Cookie Name',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
},
},
],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-encode",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,42 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'base64.encode',
description: 'Encode a value to base64',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return Buffer.from(String(args.values.value ?? '')).toString('base64');
},
},
{
name: 'base64.decode',
description: 'Decode a value from base64',
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return Buffer.from(String(args.values.value ?? ''), 'base64').toString('utf-8');
},
},
{
name: 'url.encode',
description: 'Encode a value for use in a URL (percent-encoding)',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return encodeURIComponent(String(args.values.value ?? ''));
},
},
{
name: 'url.decode',
description: 'Decode a percent-encoded URL value',
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try {
return decodeURIComponent(String(args.values.value ?? ''));
} catch {
return '';
}
},
},
],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-fs",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,21 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import fs from 'node:fs';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'fs.readFile',
description: 'Read the contents of a file as utf-8',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null;
try {
return fs.promises.readFile(String(args.values.path ?? ''), 'utf-8');
} catch {
return null;
}
},
},
],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-hash",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,86 +0,0 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import { createHash, createHmac } from 'node:crypto';
const algorithms = ['md5', 'sha1', 'sha256', 'sha512'] as const;
const encodings = ['base64', 'hex'] as const;
type TemplateFunctionPlugin = NonNullable<PluginDefinition['templateFunctions']>[number];
const hashFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({
name: `hash.${algorithm}`,
description: 'Hash a value to its hexidecimal representation',
args: [
{
type: 'text',
name: 'input',
label: 'Input',
placeholder: 'input text',
multiLine: true,
},
{
type: 'select',
name: 'encoding',
label: 'Encoding',
defaultValue: 'base64',
options: encodings.map(encoding => ({
label: capitalize(encoding),
value: encoding,
})),
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const input = String(args.values.input);
const encoding = String(args.values.encoding) as typeof encodings[number];
return createHash(algorithm)
.update(input, 'utf-8')
.digest(encoding);
},
}));
const hmacFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({
name: `hmac.${algorithm}`,
description: 'Compute the HMAC of a value',
args: [
{
type: 'text',
name: 'input',
label: 'Input',
placeholder: 'input text',
multiLine: true,
},
{
type: 'text',
name: 'key',
label: 'Key',
password: true,
},
{
type: 'select',
name: 'encoding',
label: 'Encoding',
defaultValue: 'base64',
options: encodings.map(encoding => ({
value: encoding,
label: capitalize(encoding),
})),
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const input = String(args.values.input);
const key = String(args.values.key);
const encoding = String(args.values.encoding) as typeof encodings[number];
return createHmac(algorithm, key, {})
.update(input)
.digest(encoding);
},
}));
export const plugin: PluginDefinition = {
templateFunctions: [...hashFunctions, ...hmacFunctions],
};
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@@ -1,16 +0,0 @@
{
"name": "@yaak/template-function-json",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

@@ -1,81 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'json.jsonpath',
description: 'Filter JSON-formatted text using JSONPath syntax',
args: [
{
type: 'text',
name: 'input',
label: 'Input',
multiLine: true,
placeholder: '{ "foo": "bar" }',
},
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
{ type: 'checkbox', name: 'formatted', label: 'Format Output' },
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try {
const parsed = JSON.parse(String(args.values.input));
const query = String(args.values.query ?? '$').trim();
let filtered = JSONPath({ path: query, json: parsed });
if (Array.isArray(filtered)) {
filtered = filtered[0];
}
if (typeof filtered === 'string') {
return filtered;
}
if (args.values.formatted) {
return JSON.stringify(filtered, null, 2);
} else {
return JSON.stringify(filtered);
}
} catch {
return null;
}
},
},
{
name: 'json.escape',
description: 'Escape a JSON string, useful when using the output in JSON values',
args: [
{
type: 'text',
name: 'input',
label: 'Input',
multiLine: true,
placeholder: 'Hello "World"',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const input = String(args.values.input ?? '');
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
},
},
{
name: 'json.minify',
description: 'Remove unnecessary whitespace from a valid JSON string.',
args: [
{
type: 'editor',
language: 'json',
name: 'input',
label: 'Input',
placeholder: '{ "foo": "bar" }',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const input = String(args.values.input ?? '');
try {
return JSON.stringify(JSON.parse(input));
} catch {
return input;
}
},
},
],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-prompt",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,25 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [{
name: 'prompt.text',
description: 'Prompt the user for input when sending a request',
args: [
{ type: 'text', name: 'title', label: 'Title' },
{ type: 'text', name: 'label', label: 'Label', optional: true },
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
{ type: 'text', name: 'placeholder', label: 'Placeholder', optional: true },
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (args.purpose !== 'send') return null;
return await ctx.prompt.text({
id: `prompt-${args.values.label}`,
label: String(args.values.title ?? ''),
title: String(args.values.title ?? ''),
defaultValue: String(args.values.defaultValue),
placeholder: String(args.values.placeholder),
});
},
}],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-regex",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,32 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'regex.match',
description: 'Extract',
args: [
{
type: 'text',
name: 'regex',
label: 'Regular Expression',
placeholder: '^\\w+=(?<value>\\w*)$',
defaultValue: '^(.*)$',
description:
'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.',
},
{ type: 'text', name: 'input', label: 'Input Text', multiLine: true },
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.regex || !args.values.input) return '';
const input = String(args.values.input);
const regex = new RegExp(String(args.values.regex));
const match = input.match(regex);
return match?.groups
? (Object.values(match.groups)[0] ?? '')
: (match?.[1] ?? match?.[0] ?? '');
},
},
],
};

View File

@@ -1,10 +0,0 @@
{
"name": "@yaak/template-function-request",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -1,57 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'request.body',
args: [
{
name: 'requestId',
label: 'Http Request',
type: 'http_request',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null;
return String(
await ctx.templates.render({
data: httpRequest.body?.text ?? '',
purpose: args.purpose,
}),
);
},
},
{
name: 'request.header',
args: [
{
name: 'requestId',
label: 'Http Request',
type: 'http_request',
},
{
name: 'header',
label: 'Header Name',
type: 'text',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const headerName = String(args.values.header ?? '');
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null;
const header = httpRequest.headers.find(
(h) => h.name.toLowerCase() === headerName.toLowerCase(),
);
return String(
await ctx.templates.render({
data: header?.value ?? '',
purpose: args.purpose,
}),
);
},
},
],
};

View File

@@ -1,212 +0,0 @@
import { DOMParser } from '@xmldom/xmldom';
import type {
CallTemplateFunctionArgs,
Context,
FormInput,
HttpResponse,
PluginDefinition,
RenderPurpose,
} from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
import { readFileSync } from 'node:fs';
import xpath from 'xpath';
const behaviorArg: FormInput = {
type: 'select',
name: 'behavior',
label: 'Sending Behavior',
defaultValue: 'smart',
options: [
{ label: 'When no responses', value: 'smart' },
{ label: 'Always', value: 'always' },
],
};
const requestArg: FormInput = {
type: 'http_request',
name: 'request',
label: 'Request',
};
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'response.header',
description: 'Read the value of a response header, by name',
args: [
requestArg,
{
type: 'text',
name: 'header',
label: 'Header Name',
placeholder: 'Content-Type',
},
behaviorArg,
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.header) return null;
const response = await getResponse(ctx, {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
const header = response.headers.find(
(h) => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(),
);
return header?.value ?? null;
},
},
{
name: 'response.body.path',
description: 'Access a field of the response body using JsonPath or XPath',
aliases: ['response'],
args: [
requestArg,
{
type: 'text',
name: 'path',
label: 'JSONPath or XPath',
placeholder: '$.books[0].id or /books[0]/id',
},
behaviorArg,
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.path) return null;
const response = await getResponse(ctx, {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
if (response.bodyPath == null) {
return null;
}
let body;
try {
body = readFileSync(response.bodyPath, 'utf-8');
} catch {
return null;
}
try {
return filterJSONPath(body, String(args.values.path || ''));
} catch {
// Probably not JSON, try XPath
}
try {
return filterXPath(body, String(args.values.path || ''));
} catch {
// Probably not XML
}
return null; // Bail out
},
},
{
name: 'response.body.raw',
description: 'Access the entire response body, as text',
aliases: ['response'],
args: [requestArg, behaviorArg],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null;
const response = await getResponse(ctx, {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
if (response.bodyPath == null) {
return null;
}
let body;
try {
body = readFileSync(response.bodyPath, 'utf-8');
} catch {
return null;
}
return body;
},
},
],
};
function filterJSONPath(body: string, path: string): string {
const parsed = JSON.parse(body);
const items = JSONPath({ path, json: parsed })[0];
if (items == null) {
return '';
}
if (
Object.prototype.toString.call(items) === '[object Array]' ||
Object.prototype.toString.call(items) === '[object Object]'
) {
return JSON.stringify(items);
} else {
return String(items);
}
}
function filterXPath(body: string, path: string): string {
const doc = new DOMParser().parseFromString(body, 'text/xml');
const items = xpath.select(path, doc, false);
if (Array.isArray(items)) {
return items[0] != null ? String(items[0].firstChild ?? '') : '';
} else {
// Not sure what cases this happens in (?)
return String(items);
}
}
async function getResponse(
ctx: Context,
{
requestId,
behavior,
purpose,
}: {
requestId: string;
behavior: string | null;
purpose: RenderPurpose;
},
): Promise<HttpResponse | null> {
if (!requestId) return null;
const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' });
if (httpRequest == null) {
return null;
}
const responses = await ctx.httpResponse.find({ requestId: httpRequest.id, limit: 1 });
if (behavior === 'never' && responses.length === 0) {
return null;
}
let response: HttpResponse | null = responses[0] ?? null;
// Previews happen a ton, and we don't want to send too many times on "always," so treat
// it as "smart" during preview.
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
// Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') {
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
}
return response;
}

View File

@@ -1,13 +0,0 @@
{
"name": "@yaak/template-function-uuid",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"uuid": "^11.1.0"
}
}

View File

@@ -1,76 +0,0 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import { v1, v3, v4, v5, v6, v7 } from 'uuid';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'uuid.v1',
description: 'Generate a UUID V1',
args: [],
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
return v1();
},
},
{
name: 'uuid.v3',
description: 'Generate a UUID V3',
args: [
{ type: 'text', name: 'name', label: 'Name' },
{
type: 'text',
name: 'namespace',
label: 'Namespace UUID',
description: 'A valid UUID to use as the namespace',
placeholder: '24ced880-3bf4-11f0-8329-cd053d577f0e',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return v3(String(args.values.name), String(args.values.namespace));
},
},
{
name: 'uuid.v4',
description: 'Generate a UUID V4',
args: [],
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
return v4();
},
},
{
name: 'uuid.v5',
description: 'Generate a UUID V5',
args: [
{ type: 'text', name: 'name', label: 'Name' },
{ type: 'text', name: 'namespace', label: 'Namespace' },
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return v5(String(args.values.name), String(args.values.namespace));
},
},
{
name: 'uuid.v6',
description: 'Generate a UUID V6',
args: [
{
type: 'text',
name: 'timestamp',
label: 'Timestamp',
optional: true,
description: 'Can be any format that can be parsed by JavaScript new Date(...)',
placeholder: '2025-05-28T11:15:00Z',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return v6({ msecs: new Date(String(args.values.timestamp)).getTime() });
},
},
{
name: 'uuid.v7',
description: 'Generate a UUID V7',
args: [],
async onRender(_ctx: Context, _args: CallTemplateFunctionArgs): Promise<string | null> {
return v7();
},
},
],
};

View File

@@ -1,14 +0,0 @@
{
"name": "@yaak/template-function-xml",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"xpath": "^0.0.34"
}
}

View File

@@ -1,37 +0,0 @@
import { DOMParser } from '@xmldom/xmldom';
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'xml.xpath',
description: 'Filter XML-formatted text using XPath syntax',
args: [
{
type: 'text',
name: 'input',
label: 'Input',
multiLine: true,
placeholder: '<foo></foo>',
},
{ type: 'text', name: 'query', label: 'Query', placeholder: '//foo' },
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try {
const doc = new DOMParser().parseFromString(String(args.values.input), 'text/xml');
const result = xpath.select(String(args.values.query), doc, false);
if (Array.isArray(result)) {
return String(result.map((c) => String(c.firstChild))[0] ?? '');
} else if (result instanceof Node) {
return String(result.firstChild);
} else {
return String(result);
}
} catch {
return null;
}
},
},
],
};

View File

@@ -1,599 +0,0 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
themes: [
{
id: 'catppuccin-frappe',
label: 'Catppuccin Frappé',
dark: true,
base: {
surface: 'hsl(231,19%,20%)',
text: 'hsl(227,70%,87%)',
textSubtle: 'hsl(228,29%,73%)',
textSubtlest: 'hsl(227,17%,58%)',
primary: 'hsl(277,59%,76%)',
secondary: 'hsl(228,39%,80%)',
info: 'hsl(222,74%,74%)',
success: 'hsl(96,44%,68%)',
notice: 'hsl(40,62%,73%)',
warning: 'hsl(20,79%,70%)',
danger: 'hsl(359,68%,71%)',
},
components: {
dialog: {
surface: 'hsl(240,21%,12%)',
},
sidebar: {
surface: 'hsl(229,19%,23%)',
border: 'hsl(229,19%,27%)',
},
appHeader: {
surface: 'hsl(229,20%,17%)',
border: 'hsl(229,20%,25%)',
},
responsePane: {
surface: 'hsl(229,19%,23%)',
border: 'hsl(229,19%,27%)',
},
button: {
primary: 'hsl(277,59%,68%)',
secondary: 'hsl(228,39%,72%)',
info: 'hsl(222,74%,67%)',
success: 'hsl(96,44%,61%)',
notice: 'hsl(40,62%,66%)',
warning: 'hsl(20,79%,63%)',
danger: 'hsl(359,68%,64%)',
},
},
},
{
id: 'catppuccin-macchiato',
label: 'Catppuccin Macchiato',
dark: true,
base: {
surface: 'hsl(233,23%,15%)',
text: 'hsl(227,68%,88%)',
textSubtle: 'hsl(227,27%,72%)',
textSubtlest: 'hsl(228,15%,57%)',
primary: 'hsl(267,83%,80%)',
secondary: 'hsl(228,39%,80%)',
info: 'hsl(220,83%,75%)',
success: 'hsl(105,48%,72%)',
notice: 'hsl(40,70%,78%)',
warning: 'hsl(21,86%,73%)',
danger: 'hsl(351,74%,73%)',
},
components: {
dialog: {
surface: 'hsl(240,21%,12%)',
},
sidebar: {
surface: 'hsl(232,23%,18%)',
border: 'hsl(231,23%,22%)',
},
appHeader: {
surface: 'hsl(236,23%,12%)',
border: 'hsl(236,23%,21%)',
},
responsePane: {
surface: 'hsl(232,23%,18%)',
border: 'hsl(231,23%,22%)',
},
button: {
primary: 'hsl(267,82%,72%)',
secondary: 'hsl(228,39%,72%)',
info: 'hsl(220,83%,68%)',
success: 'hsl(105,48%,65%)',
notice: 'hsl(40,70%,70%)',
warning: 'hsl(21,86%,66%)',
danger: 'hsl(351,74%,66%)',
},
},
},
{
id: 'catppuccin-mocha',
label: 'Catppuccin Mocha',
dark: true,
base: {
surface: 'hsl(240,21%,12%)',
text: 'hsl(226,64%,88%)',
textSubtle: 'hsl(228,24%,72%)',
textSubtlest: 'hsl(230,13%,55%)',
primary: 'hsl(267,83%,80%)',
secondary: 'hsl(227,35%,80%)',
info: 'hsl(217,92%,76%)',
success: 'hsl(115,54%,76%)',
notice: 'hsl(41,86%,83%)',
warning: 'hsl(23,92%,75%)',
danger: 'hsl(343,81%,75%)',
},
components: {
dialog: {
surface: 'hsl(240,21%,12%)',
},
sidebar: {
surface: 'hsl(240,21%,15%)',
border: 'hsl(240,21%,19%)',
},
appHeader: {
surface: 'hsl(240,23%,9%)',
border: 'hsl(240,22%,18%)',
},
responsePane: {
surface: 'hsl(240,21%,15%)',
border: 'hsl(240,21%,19%)',
},
button: {
primary: 'hsl(267,67%,65%)',
secondary: 'hsl(227,28%,64%)',
info: 'hsl(217,74%,61%)',
success: 'hsl(115,43%,61%)',
notice: 'hsl(41,69%,66%)',
warning: 'hsl(23,74%,60%)',
danger: 'hsl(343,65%,60%)',
},
},
},
{
id: 'catppuccin-latte',
label: 'Catppuccin Latte',
dark: false,
base: {
surface: 'hsl(220,23%,95%)',
text: 'hsl(234,16%,35%)',
textSubtle: 'hsl(233,10%,47%)',
textSubtlest: 'hsl(231,10%,59%)',
primary: 'hsl(266,85%,58%)',
secondary: 'hsl(233,10%,47%)',
info: 'hsl(231,97%,72%)',
success: 'hsl(183,74%,35%)',
notice: 'hsl(35,77%,49%)',
warning: 'hsl(22,99%,52%)',
danger: 'hsl(355,76%,59%)',
},
components: {
sidebar: {
surface: 'hsl(220,22%,92%)',
border: 'hsl(220,22%,87%)',
},
appHeader: {
surface: 'hsl(220,21%,89%)',
border: 'hsl(220,22%,87%)',
},
},
},
{
id: 'dracula',
label: 'Dracula',
dark: true,
base: {
surface: 'hsl(231,15%,18%)',
surfaceHighlight: 'hsl(230,15%,24%)',
text: 'hsl(60,30%,96%)',
textSubtle: 'hsl(232,14%,65%)',
textSubtlest: 'hsl(232,14%,50%)',
primary: 'hsl(265,89%,78%)',
secondary: 'hsl(225,27%,51%)',
info: 'hsl(191,97%,77%)',
success: 'hsl(135,94%,65%)',
notice: 'hsl(65,92%,76%)',
warning: 'hsl(31,100%,71%)',
danger: 'hsl(0,100%,67%)',
},
components: {
sidebar: {
backdrop: 'hsl(230,15%,24%)',
},
appHeader: {
backdrop: 'hsl(235,14%,15%)',
},
},
},
{
id: 'github-dark',
label: 'GitHub',
dark: true,
base: {
surface: 'hsl(213,30%,7%)',
surfaceHighlight: 'hsl(213,16%,13%)',
text: 'hsl(212,27%,89%)',
textSubtle: 'hsl(212,9%,57%)',
textSubtlest: 'hsl(217,8%,45%)',
border: 'hsl(215,21%,11%)',
primary: 'hsl(262,78%,74%)',
secondary: 'hsl(217,8%,50%)',
info: 'hsl(215,84%,64%)',
success: 'hsl(129,48%,52%)',
notice: 'hsl(39,71%,58%)',
warning: 'hsl(22,83%,60%)',
danger: 'hsl(3,83%,65%)',
},
components: {
button: {
primary: 'hsl(262,79%,71%)',
secondary: 'hsl(217,8%,45%)',
info: 'hsl(215,84%,60%)',
success: 'hsl(129,48%,47%)',
notice: 'hsl(39,71%,53%)',
warning: 'hsl(22,83%,56%)',
danger: 'hsl(3,83%,61%)',
},
},
},
{
id: 'github-light',
label: 'GitHub',
dark: false,
base: {
surface: 'hsl(0,0%,100%)',
surfaceHighlight: 'hsl(210,29%,94%)',
text: 'hsl(213,13%,14%)',
textSubtle: 'hsl(212,9%,43%)',
textSubtlest: 'hsl(203,8%,55%)',
border: 'hsl(210,15%,92%)',
borderSubtle: 'hsl(210,15%,92%)',
primary: 'hsl(261,69%,59%)',
secondary: 'hsl(212,8%,47%)',
info: 'hsl(212,92%,48%)',
success: 'hsl(137,66%,32%)',
notice: 'hsl(40,100%,40%)',
warning: 'hsl(24,100%,44%)',
danger: 'hsl(356,71%,48%)',
},
},
{
id: 'gruvbox',
label: 'Gruvbox',
dark: true,
base: {
surface: 'hsl(0,0%,16%)',
surfaceHighlight: 'hsl(20,3%,19%)',
text: 'hsl(53,74%,91%)',
textSubtle: 'hsl(39,24%,66%)',
textSubtlest: 'hsl(30,12%,51%)',
primary: 'hsl(344,47%,68%)',
secondary: 'hsl(157,16%,58%)',
info: 'hsl(104,35%,62%)',
success: 'hsl(61,66%,44%)',
notice: 'hsl(42,95%,58%)',
warning: 'hsl(27,99%,55%)',
danger: 'hsl(6,96%,59%)',
},
},
{
id: 'hotdog-stand',
label: 'Hotdog Stand',
dark: true,
base: {
surface: 'hsl(0,100%,50%)',
surfaceHighlight: 'hsl(0,0%,0%)',
text: 'hsl(0,0%,100%)',
textSubtle: 'hsl(0,0%,100%)',
textSubtlest: 'hsl(60,100%,50%)',
border: 'hsl(0,0%,0%)',
primary: 'hsl(60,100%,50%)',
secondary: 'hsl(60,100%,50%)',
info: 'hsl(60,100%,50%)',
success: 'hsl(60,100%,50%)',
notice: 'hsl(60,100%,50%)',
warning: 'hsl(60,100%,50%)',
danger: 'hsl(60,100%,50%)',
},
components: {
appHeader: {
surface: 'hsl(0,0%,0%)',
text: 'hsl(0,0%,100%)',
textSubtle: 'hsl(60,100%,50%)',
textSubtlest: 'hsl(0,100%,50%)',
},
menu: {
surface: 'hsl(0,0%,0%)',
border: 'hsl(0,100%,50%)',
surfaceHighlight: 'hsl(0,100%,50%)',
text: 'hsl(0,0%,100%)',
textSubtle: 'hsl(60,100%,50%)',
textSubtlest: 'hsl(60,100%,50%)',
},
button: {
surface: 'hsl(0,0%,0%)',
text: 'hsl(0,0%,100%)',
primary: 'hsl(0,0%,0%)',
secondary: 'hsl(0,0%,100%)',
info: 'hsl(0,0%,0%)',
success: 'hsl(60,100%,50%)',
notice: 'hsl(60,100%,50%)',
warning: 'hsl(0,0%,0%)',
danger: 'hsl(0,100%,50%)',
},
editor: {
primary: 'hsl(0,0%,100%)',
secondary: 'hsl(0,0%,100%)',
info: 'hsl(0,0%,100%)',
success: 'hsl(0,0%,100%)',
notice: 'hsl(60,100%,50%)',
warning: 'hsl(0,0%,100%)',
danger: 'hsl(0,0%,100%)',
},
},
},
{
id: 'monokai-pro',
label: 'Monokai Pro',
dark: true,
base: {
surface: 'hsl(285,5%,17%)',
text: 'hsl(60,25%,98%)',
textSubtle: 'hsl(0,1%,75%)',
textSubtlest: 'hsl(300,0%,57%)',
primary: 'hsl(250,77%,78%)',
secondary: 'hsl(0,1%,75%)',
info: 'hsl(186,71%,69%)',
success: 'hsl(90,59%,66%)',
notice: 'hsl(45,100%,70%)',
warning: 'hsl(20,96%,70%)',
danger: 'hsl(345,100%,69%)',
},
components: {
appHeader: {
surface: 'hsl(300,5%,13%)',
text: 'hsl(0,1%,75%)',
textSubtle: 'hsl(300,0%,57%)',
textSubtlest: 'hsl(300,1%,44%)',
},
button: {
primary: 'hsl(250,77%,70%)',
secondary: 'hsl(0,1%,68%)',
info: 'hsl(186,71%,62%)',
success: 'hsl(90,59%,59%)',
notice: 'hsl(45,100%,63%)',
warning: 'hsl(20,96%,63%)',
danger: 'hsl(345,100%,62%)',
},
},
},
{
id: 'monokai-pro-classic',
label: 'Monokai Pro Classic',
dark: true,
base: {
surface: 'hsl(70,8%,15%)',
text: 'hsl(69,100%,97%)',
textSubtle: 'hsl(65,9%,73%)',
textSubtlest: 'hsl(66,4%,55%)',
primary: 'hsl(261,100%,75%)',
secondary: 'hsl(202,8%,72%)',
info: 'hsl(190,81%,67%)',
success: 'hsl(80,76%,53%)',
notice: 'hsl(54,70%,68%)',
warning: 'hsl(32,98%,56%)',
danger: 'hsl(338,95%,56%)',
},
components: {
appHeader: {
surface: 'hsl(72,9%,11%)',
text: 'hsl(202,8%,72%)',
textSubtle: 'hsl(213,4%,48%)',
textSubtlest: 'hsl(223,6%,44%)',
},
button: {
primary: 'hsl(261,100%,68%)',
secondary: 'hsl(202,8%,65%)',
info: 'hsl(190,81%,60%)',
success: 'hsl(80,76%,48%)',
notice: 'hsl(54,71%,61%)',
warning: 'hsl(32,98%,50%)',
danger: 'hsl(338,95%,50%)',
},
},
},
{
id: 'monokai-pro-machine',
label: 'Monokai Pro Machine',
dark: true,
base: {
surface: 'hsl(200,16%,18%)',
text: 'hsl(173,24%,93%)',
textSubtle: 'hsl(185,6%,57%)',
textSubtlest: 'hsl(189,6%,45%)',
primary: 'hsl(258,86%,80%)',
secondary: 'hsl(175,9%,75%)',
info: 'hsl(194,81%,72%)',
success: 'hsl(98,67%,69%)',
notice: 'hsl(52,100%,72%)',
warning: 'hsl(28,100%,72%)',
danger: 'hsl(353,100%,71%)',
},
components: {
appHeader: {
surface: 'hsl(196,16%,14%)',
text: 'hsl(202,8%,72%)',
textSubtle: 'hsl(213,4%,48%)',
textSubtlest: 'hsl(223,6%,44%)',
},
button: {
primary: 'hsl(258,86%,72%)',
secondary: 'hsl(175,9%,68%)',
info: 'hsl(194,80%,65%)',
success: 'hsl(98,67%,62%)',
notice: 'hsl(52,100%,65%)',
warning: 'hsl(28,100%,65%)',
danger: 'hsl(353,100%,64%)',
},
},
},
{
id: 'monokai-pro-octagon',
label: 'Monokai Pro Octagon',
dark: true,
base: {
surface: 'hsl(233,18%,19%)',
text: 'hsl(173,24%,93%)',
textSubtle: 'hsl(202,8%,72%)',
textSubtlest: 'hsl(213,4%,48%)',
primary: 'hsl(292,30%,70%)',
secondary: 'hsl(202,8%,72%)',
info: 'hsl(155,37%,72%)',
success: 'hsl(75,60%,61%)',
notice: 'hsl(44,100%,71%)',
warning: 'hsl(23,100%,68%)',
danger: 'hsl(352,100%,70%)',
},
components: {
appHeader: {
surface: 'hsl(235,18%,14%)',
text: 'hsl(202,8%,72%)',
textSubtle: 'hsl(213,4%,48%)',
textSubtlest: 'hsl(223,6%,44%)',
},
button: {
primary: 'hsl(292,26%,63%)',
secondary: 'hsl(201,7%,65%)',
info: 'hsl(155,33%,65%)',
success: 'hsl(75,54%,55%)',
notice: 'hsl(44,90%,64%)',
warning: 'hsl(23,90%,61%)',
danger: 'hsl(352,90%,63%)',
},
},
},
{
id: 'monokai-pro-ristretto',
label: 'Monokai Pro Ristretto',
dark: true,
base: {
surface: 'hsl(0,9%,16%)',
text: 'hsl(351,100%,97%)',
textSubtle: 'hsl(355,9%,74%)',
textSubtlest: 'hsl(354,4%,56%)',
primary: 'hsl(239,63%,79%)',
secondary: 'hsl(355,9%,74%)',
info: 'hsl(170,53%,69%)',
success: 'hsl(88,57%,66%)',
notice: 'hsl(41,92%,70%)',
warning: 'hsl(13,85%,70%)',
danger: 'hsl(349,97%,70%)',
},
components: {
appHeader: {
surface: 'hsl(0,8%,12%)',
text: 'hsl(355,9%,74%)',
textSubtle: 'hsl(354,4%,56%)',
textSubtlest: 'hsl(353,4%,43%)',
},
button: {
primary: 'hsl(239,63%,71%)',
secondary: 'hsl(355,9%,67%)',
info: 'hsl(170,53%,62%)',
success: 'hsl(88,57%,59%)',
notice: 'hsl(41,92%,63%)',
warning: 'hsl(13,86%,63%)',
danger: 'hsl(349,97%,63%)',
},
},
},
{
id: 'monokai-pro-spectrum',
label: 'Monokai Pro Spectrum',
dark: true,
base: {
surface: 'hsl(0,0%,13%)',
text: 'hsl(266,100%,97%)',
textSubtle: 'hsl(264,7%,73%)',
textSubtlest: 'hsl(266,3%,55%)',
primary: 'hsl(247,61%,72%)',
secondary: 'hsl(264,7%,73%)',
info: 'hsl(188,74%,63%)',
success: 'hsl(133,54%,66%)',
notice: 'hsl(51,96%,69%)',
warning: 'hsl(23,98%,66%)',
danger: 'hsl(343,96%,68%)',
},
components: {
appHeader: {
surface: 'hsl(0,0%,10%)',
text: 'hsl(264,7%,73%)',
textSubtle: 'hsl(266,3%,55%)',
textSubtlest: 'hsl(264,2%,41%)',
},
button: {
primary: 'hsl(247,61%,65%)',
secondary: 'hsl(264,7%,66%)',
info: 'hsl(188,74%,57%)',
success: 'hsl(133,54%,59%)',
notice: 'hsl(51,96%,62%)',
warning: 'hsl(23,98%,59%)',
danger: 'hsl(343,96%,61%)',
},
},
},
{
id: 'moonlight',
label: 'Moonlight',
dark: true,
base: {
surface: 'hsl(234,23%,17%)',
text: 'hsl(225,71%,90%)',
textSubtle: 'hsl(230,28%,62%)',
textSubtlest: 'hsl(232,26%,43%)',
primary: 'hsl(262,100%,82%)',
secondary: 'hsl(232,18%,65%)',
info: 'hsl(217,100%,74%)',
success: 'hsl(174,66%,54%)',
notice: 'hsl(35,100%,73%)',
warning: 'hsl(17,100%,71%)',
danger: 'hsl(356,100%,73%)',
},
components: {
appHeader: {
surface: 'hsl(233,23%,15%)',
},
sidebar: {
surface: 'hsl(233,23%,15%)',
},
},
},
{
id: 'nord',
label: 'Nord',
dark: true,
base: {
surface: 'hsl(220,16%,22%)',
surfaceHighlight: 'hsl(220,14%,28%)',
text: 'hsl(220,28%,93%)',
textSubtle: 'hsl(220,26%,90%)',
textSubtlest: 'hsl(220,24%,86%)',
primary: 'hsl(193,38%,68%)',
secondary: 'hsl(210,34%,63%)',
info: 'hsl(174,25%,69%)',
success: 'hsl(89,26%,66%)',
notice: 'hsl(40,66%,73%)',
warning: 'hsl(17,48%,64%)',
danger: 'hsl(353,43%,56%)',
},
components: {
sidebar: {
backdrop: 'hsl(220,16%,22%)',
},
appHeader: {
backdrop: 'hsl(220,14%,28%)',
},
},
},
{
id: 'relaxing',
label: 'Relaxing',
dark: true,
base: {
surface: 'hsl(267,33%,17%)',
text: 'hsl(275,49%,92%)',
primary: 'hsl(267,84%,81%)',
secondary: 'hsl(227,35%,80%)',
info: 'hsl(217,92%,76%)',
success: 'hsl(115,54%,76%)',
notice: 'hsl(41,86%,83%)',
warning: 'hsl(23,92%,75%)',
danger: 'hsl(343,81%,75%)',
},
},
],
};

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package yaak.plugins.runtime;
service PluginRuntime {
rpc EventStream (stream EventStreamEvent) returns (stream EventStreamEvent);
}
message EventStreamEvent {
string event = 1;
}

View File

@@ -1,46 +0,0 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const slugify = require('slugify');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
async function createMigration() {
try {
const migrationName = await new Promise((resolve) => {
rl.question('Enter migration name: ', resolve);
});
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
fs.writeFileSync(filePath, '-- Add migration SQL here\n');
console.log(`Created migration file: ${fileName}`);
} catch (error) {
console.error('Error creating migration:', error);
} finally {
rl.close();
}
}
createMigration().catch(console.error);

View File

@@ -1,16 +1,23 @@
const { readdirSync, cpSync } = require('node:fs');
const path = require('node:path');
const { execSync } = require('node:child_process');
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
if (!pluginsDir) {
console.log('Skipping bundled plugins build because YAAK_PLUGINS_DIR is not set');
return;
}
const pluginsDir = path.join(__dirname, '..', 'plugins');
console.log('Installing Yaak plugins dependencies', pluginsDir);
execSync('npm ci', { cwd: pluginsDir });
console.log('Building Yaak plugins', pluginsDir);
execSync('npm run build', { cwd: pluginsDir });
console.log('Copying Yaak plugins to', pluginsDir);
for (const name of readdirSync(pluginsDir)) {
const dir = path.join(pluginsDir, name);
const pluginsRoot = path.join(pluginsDir, 'plugins');
for (const name of readdirSync(pluginsRoot)) {
const dir = path.join(pluginsRoot, name);
if (name.startsWith('.')) continue;
console.log('Building plugin', dir);
execSync('npm run build', { cwd: dir });
const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name);
console.log(`Copying ${name} to ${destDir}`);
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));

View File

@@ -3,6 +3,7 @@
target/
vendored/*
!vendored/plugins
gen/*

3566
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
[workspace]
members = [
"yaak-crypto",
"yaak-fonts",
"yaak-git",
"yaak-grpc",
"yaak-http",
@@ -34,44 +33,43 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
tauri-build = { version = "2.1.1", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
chrono = { workspace = true, features = ["serde"] }
cookie = "0.18.1"
chrono = { version = "0.4.31", features = ["serde"] }
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
http = { version = "1.2.0", default-features = false }
log = "0.4.27"
md5 = "0.8.0"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest_cookie_store = "0.8.0"
rustls = { version = "0.23.25", default-features = false, features = ["custom-provider", "ring"] }
rustls-platform-verifier = "0.5.1"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = "2.4.0"
tauri-plugin-log = { version = "2.6.0", features = ["colored"] }
tauri-plugin-opener = "2.4.0"
tauri-plugin-os = "2.3.0"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
tauri-plugin-opener = "2.2.6"
tauri-plugin-os = "2.2.1"
tauri-plugin-shell = { workspace = true }
tauri-plugin-deep-link = "2.4.0"
tauri-plugin-single-instance = { version = "2.3.0", features = ["deep-link"] }
tauri-plugin-updater = "2.9.0"
tauri-plugin-window-state = "2.3.0"
tauri-plugin-single-instance = "2.2.2"
tauri-plugin-updater = "2.6.1"
tauri-plugin-window-state = "2.2.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio-stream = "0.1.17"
uuid = "1.12.1"
yaak-common = { workspace = true }
yaak-crypto = { workspace = true }
yaak-fonts = { workspace = true }
yaak-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" }
yaak-http = { workspace = true }
@@ -85,27 +83,20 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
chrono = "0.4.41"
hex = "0.4.3"
reqwest = "0.12.20"
reqwest = "0.12.15"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.6.2"
tauri-plugin = "2.3.0"
tauri-plugin-dialog = "2.3.0"
tauri-plugin-shell = "2.3.0"
tokio = "1.45.1"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
thiserror = "2.0.12"
ts-rs = "11.0.1"
rustls = { version = "0.23.27", default-features = false }
rustls-platform-verifier = "0.6.0"
sha2 = "0.10.9"
ts-rs = "10.1.0"
yaak-common = { path = "yaak-common" }
yaak-crypto = { path = "yaak-crypto" }
yaak-fonts = { path = "yaak-fonts" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-plugins = { path = "yaak-plugins" }
yaak-sse = { path = "yaak-sse" }
yaak-sync = { path = "yaak-sync" }
yaak-templates = { path = "yaak-templates" }
yaak-crypto = { path = "yaak-crypto" }

View File

@@ -52,12 +52,10 @@
"opener:allow-reveal-item-in-dir",
"shell:allow-open",
"yaak-crypto:default",
"yaak-fonts:default",
"yaak-git:default",
"yaak-license:default",
"yaak-mac-window:default",
"yaak-models:default",
"yaak-plugins:default",
"yaak-sync:default",
"yaak-ws:default"
]

View File

@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Enable for NodeJS execution -->
<!-- Enable for v8 execution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>

Some files were not shown because too many files have changed in this diff Show More