Compare commits

..

1 Commits

Author SHA1 Message Date
Gregory Schier
64f5d973eb Fix env editor switching 2025-05-23 21:44:34 -07:00
629 changed files with 190935 additions and 35534 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:
@@ -62,25 +65,29 @@ jobs:
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli
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
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Some things (eg. WASM package) requires building before lint will work
- name: Run bootstrap
run: npm run bootstrap
- name: Run JS build
run: npm run build
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test
- 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
@@ -89,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 }}
@@ -113,5 +121,5 @@ jobs:
releaseName: 'Release __VERSION__'
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
releaseDraft: true
prerelease: true
args: '${{ matrix.args }} --config ./src-tauri/tauri.release.conf.json'
prerelease: false
args: ${{ matrix.args }}

View File

@@ -1,44 +0,0 @@
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 30 15 * * 0-6
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md'
maximum: 1999
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: 'sponsors-base'
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md'
minimum: 2000
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: 'sponsors-premium'
# ⚠️ Note: You can use any deployment step here to automatically push the README
# changes back to your branch.
- name: Commit Changes
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: main
force: false
folder: '.'

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,70 +1,34 @@
<p align="center">
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/src-tauri/icons/icon.png">
</a>
</p>
# Yaak API Client
<h1 align="center">
💫 Yaak ➟ Desktop API Client 💫
</h1>
<p align="center">
A fast, privacy-first API client for REST, GraphQL, SSE, WebSocket, and gRPC built with Tauri, Rust, and React.
</p>
<p align="center">
Development is funded by community-purchased <a href="https://yaak.app/pricing">licenses</a>. You can also <a href="https://github.com/sponsors/gschier">become a sponsor</a> to have your logo appear below. 💖
</p>
<br>
<p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="80px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
<p align="center">
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
</p>
![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
### 🌐 Work with any API
- Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl.
- Send requests via REST, GraphQL, gRPC, WebSocket, or Server-Sent Events.
- Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain.
### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI.
Yaak is a desktop API client for interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC
APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
![366149288-f18e963f-0b68-4ecb-b8b8-cb71aa9aec02](https://github.com/user-attachments/assets/ca83b7ad-5708-411b-8faf-e36b365841a4)
## Contribution Policy
Yaak is open source but only accepting contributions for bug fixes. To get started,
Yaak is open source, but only accepting contributions for bug fixes. To get started,
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
## Feature Overview
- 🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
- 📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
- 🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
- 🔎 Filter response bodies using JSONPath or XPath queries.<br/>
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
- 📂 Organize requests into workspaces and nested folders.<br/>
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
- 🛡️ Secure arbitrary text values with end-to-end encryption<br/>
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
- 🎨 Choose from many of the included themes, or make your own.<br/>
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
- 📜 View response history for each request.<br/>
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
- 🛜 Configure a proxy to access firewall-blocked APIs
## Useful Resources
- [Feedback and Bug Reports](https://feedback.yaak.app)
- [Documentation](https://feedback.yaak.app/help)
- [Yaak vs Postman](https://yaak.app/alternatives/postman)
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)
- [Yaak vs Postman](https://yaak.app/blog/postman-alternative)

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',
]),
]);

12065
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,41 +7,10 @@
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"workspaces": [
"packages/common-lib",
"packages/plugin-runtime",
"packages/plugin-runtime-types",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/auth-apikey",
"plugins/auth-aws",
"plugins/auth-basic",
"plugins/auth-bearer",
"plugins/auth-jwt",
"plugins/auth-oauth2",
"plugins/filter-jsonpath",
"plugins/filter-xpath",
"plugins/importer-curl",
"plugins/importer-insomnia",
"plugins/importer-openapi",
"plugins/importer-postman",
"plugins/importer-postman-environment",
"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-timestamp",
"plugins/template-function-uuid",
"plugins/template-function-xml",
"plugins/themes-yaak",
"src-tauri",
"packages/common-lib",
"src-tauri/yaak-crypto",
"src-tauri/yaak-fonts",
"src-tauri/yaak-git",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
@@ -56,14 +25,8 @@
"scripts": {
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri.development.conf.json",
"migration": "node scripts/create-migration.cjs",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"test": "npm run --workspaces --if-present test",
"icons": "run-p icons:*",
"icons:dev": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
"icons:release": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release",
"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",
@@ -72,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.9.1",
"@tauri-apps/cli": "^2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.2.7",
"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.7.0",
"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": [
@@ -31,7 +17,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@types/node": "^24.0.13"
"@types/node": "^22.5.4"
},
"devDependencies": {
"cpy-cli": "^5.0.0"

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,12 +1,16 @@
// 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 CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
@@ -19,22 +23,17 @@ export type CallHttpAuthenticationResponse = {
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders?: Array<HttpHeader>,
/**
* Query parameters to add to the request. Existing params will be replaced, while
* new params will be added.
*/
setQueryParameters?: Array<HttpHeader>, };
setHeaders: Array<HttpHeader>, };
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, };
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
@@ -68,7 +67,7 @@ extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, error?: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
@@ -105,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 = {
/**
@@ -136,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 = {
/**
@@ -179,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 = {
/**
@@ -218,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 = {
/**
@@ -249,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, };
@@ -286,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, };
@@ -331,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, };
@@ -361,17 +328,7 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { 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 GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
@@ -389,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" } | { "type": "reload_response" } & ReloadResponse | { "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_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "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": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "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": "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.
@@ -421,12 +374,6 @@ required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type ReloadResponse = { silent: boolean, };
export type RenderGrpcRequestRequest = { grpcRequest: GrpcRequest, purpose: RenderPurpose, };
export type RenderGrpcRequestResponse = { grpcRequest: GrpcRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
@@ -441,7 +388,7 @@ export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
@@ -459,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, parentModel: string, parentId: string | null, 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,24 +1,19 @@
import type {
FindHttpResponsesRequest,
FindHttpResponsesResponse,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
ListCookieNamesResponse,
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderGrpcRequestRequest,
RenderGrpcRequestResponse,
RenderHttpRequestRequest,
RenderHttpRequestResponse,
SendHttpRequestRequest,
SendHttpRequestResponse,
ShowToastRequest,
TemplateRenderRequest,
TemplateRenderResponse,
} from '../bindings/gen_events.ts';
import { JsonValue } from '../bindings/serde_json/JsonValue';
export interface Context {
clipboard: {
@@ -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']>;
@@ -59,9 +47,6 @@ export interface Context {
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
};
templates: {
render<T extends JsonValue>(args: TemplateRenderRequest & { data: T }): Promise<T>;
};
plugin: {
reload(): void;
render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
};
}

View File

@@ -1,11 +1,12 @@
import { FilterResponse } from '../bindings/gen_events';
import type { Context } from './Context';
type FilterPluginResponse = { filtered: string };
export type FilterPlugin = {
name: string;
description?: string;
onFilter(
ctx: Context,
args: { payload: string; filter: string; mimeType: string },
): Promise<FilterResponse> | FilterResponse;
): Promise<FilterPluginResponse> | FilterPluginResponse;
};

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,21 +1,12 @@
import {
CallTemplateFunctionArgs,
FormInput,
GetHttpAuthenticationConfigRequest,
TemplateFunction,
TemplateFunctionArg,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type DynamicTemplateFunctionArg = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
} from "../bindings/gen_events";
import { Context } from "./Context";
export type TemplateFunctionPlugin = TemplateFunction & {
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
onRender(
ctx: Context,
args: CallTemplateFunctionArgs,
): Promise<string | null>;
};

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,26 +1,20 @@
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';
import type { ThemePlugin } from './ThemePlugin';
import type { Context } from './Context';
export type { Context };
export type { Context } from './Context';
/**
* The global structure of a Yaak plugin
*/
export type PluginDefinition = {
init?: (ctx: Context) => void | Promise<void>;
dispose?: () => void | Promise<void>;
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

@@ -21,7 +21,7 @@ export class PluginHandle {
this.#instance.postMessage(event);
}
async terminate() {
await this.#instance.terminate();
terminate() {
this.#instance.terminate();
}
}

View File

@@ -1,33 +1,30 @@
import {
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
import type {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
GrpcRequestAction,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
ListCookieNamesResponse,
PluginWindowContext,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderGrpcRequestResponse,
RenderHttpRequestResponse,
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 = {} as any;
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const fileChangeCallback = async () => {
await this.#mod?.dispose?.();
this.#importModule();
await this.#mod?.init?.(this.#newCtx({ type: 'none' }));
return this.#sendPayload(
windowContextNone,
{
type: 'reload_response',
silent: false,
},
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();
}
@@ -84,20 +74,23 @@ export class PluginInstance {
this.#appToPluginEvents.emit(event);
}
async terminate() {
await this.#mod?.dispose?.();
terminate() {
this.#unimportModule();
}
async #onMessage(event: InternalEvent) {
const ctx = this.#newCtx(event.windowContext);
const ctx = this.#newCtx(event);
const { windowContext, payload, id: replyId } = event;
try {
if (payload.type === 'boot_request') {
await this.#mod?.init?.(ctx);
this.#sendPayload(windowContext, { type: 'boot_response' }, replyId);
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
const payload: InternalEventPayload = {
type: 'boot_response',
name: this.#pkg.name ?? 'unknown',
version: this.#pkg.version ?? '0.0.1',
};
this.#sendPayload(windowContext, payload, replyId);
return;
}
@@ -105,7 +98,6 @@ export class PluginInstance {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
await this.terminate();
this.#sendPayload(windowContext, payload, replyId);
return;
}
@@ -136,23 +128,9 @@ export class PluginInstance {
payload: payload.content,
mimeType: payload.type,
});
this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId);
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,
type: 'filter_response',
content: reply.filtered,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
@@ -176,65 +154,21 @@ 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_function_summary_request' &&
payload.type === 'get_template_functions_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
(templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
},
);
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const replyPayload: InternalEventPayload = {
type: 'get_template_function_summary_response',
type: 'get_template_functions_response',
pluginRefId: this.#workerData.pluginRefId,
functions,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(windowContext, replyId);
return;
}
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
// @ts-ignore
delete templateFunction.onRender;
const resolvedArgs: TemplateFunctionArg[] = [];
for (const arg of templateFunction.args) {
if (arg && 'dynamic' in arg) {
const dynamicAttrs = await arg.dynamic(ctx, payload);
const { dynamic, ...other } = arg;
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
} else if (arg) {
resolvedArgs.push(arg);
}
templateFunction.args = resolvedArgs;
}
const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response',
pluginRefId: this.#workerData.pluginRefId,
function: templateFunction,
functions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
@@ -256,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);
}
}
@@ -285,11 +220,12 @@ export class PluginInstance {
const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
this.#sendPayload(
windowContext,
{
type: 'call_http_authentication_response',
...(await auth.onApply(ctx, payload)),
setHeaders: result.setHeaders,
},
replyId,
);
@@ -321,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)
@@ -340,34 +264,32 @@ export class PluginInstance {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') {
applyFormInputDefaults(fn.args, payload.args.values);
try {
const result = await fn.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
} catch (err) {
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: null,
error: `${err}`.replace(/^Error:\s*/g, ''),
},
replyId,
);
}
const result = await fn.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
return;
}
}
if (payload.type === 'reload_request') {
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;
}
@@ -475,11 +397,11 @@ export class PluginInstance {
this.#sendEvent(eventToSend);
}
#newCtx(windowContext: PluginWindowContext): Context {
#newCtx(event: InternalEvent): Context {
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(windowContext, {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'copy_text_request',
text,
});
@@ -487,10 +409,8 @@ export class PluginInstance {
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(windowContext, {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'show_toast_request',
// Handle default here because null/undefined both convert to None in Rust translation
timeout: args.timeout === undefined ? 5000 : args.timeout,
...args,
});
},
@@ -506,21 +426,21 @@ export class PluginInstance {
onClose?.();
}
};
this.#sendAndListenForEvents(windowContext, payload, onEvent);
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
this.#sendPayload(windowContext, closePayload, null);
this.#sendPayload(event.windowContext, closePayload, null);
},
};
},
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(windowContext, {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
type: 'prompt_text_request',
...args,
});
@@ -534,25 +454,12 @@ export class PluginInstance {
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
windowContext,
event.windowContext,
payload,
);
return httpResponses;
},
},
grpcRequest: {
render: async (args) => {
const payload = {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
windowContext,
payload,
);
return grpcRequest;
},
},
httpRequest: {
getById: async (args) => {
const payload = {
@@ -560,7 +467,7 @@ export class PluginInstance {
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
windowContext,
event.windowContext,
payload,
);
return httpRequest;
@@ -571,7 +478,7 @@ export class PluginInstance {
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
windowContext,
event.windowContext,
payload,
);
return httpResponse;
@@ -582,33 +489,12 @@ export class PluginInstance {
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
windowContext,
event.windowContext,
payload,
);
return httpRequest;
},
},
cookies: {
getValue: async (args: GetCookieValueRequest) => {
const payload = {
type: 'get_cookie_value_request',
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
windowContext,
payload,
);
return value;
},
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
windowContext,
payload,
);
return names;
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type
@@ -617,17 +503,17 @@ export class PluginInstance {
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
windowContext,
event.windowContext,
payload,
);
return result.data as any;
return result.data;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
windowContext,
event.windowContext,
payload,
);
return result.value ? (JSON.parse(result.value) as T) : undefined;
@@ -639,22 +525,17 @@ export class PluginInstance {
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(windowContext, payload);
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
windowContext,
event.windowContext,
payload,
);
return result.deleted;
},
},
plugin: {
reload: () => {
this.#sendPayload({ type: 'none' }, { type: 'reload_response', silent: true }, null);
},
},
};
}
}
@@ -671,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) {
@@ -682,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

@@ -46,14 +46,10 @@ async function handleIncoming(msg: string) {
}
if (pluginEvent.payload.type === 'terminate_request') {
await plugin.terminate();
plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent);
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

View File

@@ -1,8 +1,6 @@
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import { TemplateFunction } from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({

1
plugins/.gitignore vendored
View File

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

View File

@@ -1,68 +0,0 @@
# Copy as cUrl
A request action plugin for Yaak that converts HTTP requests into [curl](https://curl.se)
commands, making it easy to share, debug, and execute requests outside Yaak.
![Screenshot of context menu](screenshot.png)
## Overview
This plugin adds a 'Copy as Curl' action to HTTP requests, converting any request into its
equivalent curl command. This is useful for debugging, sharing requests with team members,
and executing requests in terminal environments where `curl` is available.
## How It Works
The plugin analyzes the given HTTP request and generates a properly formatted curl command
that includes:
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Request URL with query parameters
- Headers (including authentication headers)
- Request body (for POST, PUT, PATCH requests)
- Authentication credentials
## Usage
1. Configure an HTTP request as usual in Yaak
2. Right-click on the request in the sidebar
3. Select 'Copy as Curl'
4. The command is copied to your clipboard
5. Share or execute the command
## Generated Curl Examples
### Simple GET Request
```bash
curl -X GET 'https://api.example.com/users' \
--header 'Accept: application/json'
```
### POST Request with JSON Data
```bash
curl -X POST 'https://api.example.com/users' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"name": "John Doe",
"email": "john@example.com"
}'
```
### Request with Multi-part Form Data
```bash
curl -X POST 'yaak.app' \
--header 'Content-Type: multipart/form-data' \
--form 'hello=world' \
--form file=@/path/to/file.json
```
### Request with Authentication
```bash
curl -X GET 'https://api.example.com/protected' \
--user 'username:password'
```

View File

@@ -1,18 +0,0 @@
{
"name": "@yaak/action-copy-curl",
"displayName": "Copy as Curl",
"description": "Copy request as a curl command",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-copy-curl"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

View File

@@ -1,125 +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);
// Build final URL with parameters (compatible with old curl)
let finalUrl = request.url || '';
const urlParams = (request.urlParameters ?? []).filter(onlyEnabled);
if (urlParams.length > 0) {
// Build url
const [base, hash] = finalUrl.split('#');
const separator = base!.includes('?') ? '&' : '?';
const queryString = urlParams
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join('&');
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
}
xs.push(quote(finalUrl));
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
const type = request.bodyType ?? 'none';
if (
(type === 'multipart/form-data' || type === 'application/x-www-form-urlencoded') &&
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 (type === 'graphql' && typeof request.body?.query === 'string') {
const body = {
query: request.body.query || '',
variables: maybeParseJSON(request.body.variables, undefined),
};
xs.push('--data', quote(JSON.stringify(body)));
xs.push(NEWLINE);
} else if (type !== 'none' && typeof request.body?.text === 'string') {
xs.push('--data', 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') {
const value =
`${request.authentication?.prefix ?? 'Bearer'} ${request.authentication?.token ?? ''}`.trim();
xs.push('--header', quote(`Authorization: ${value}`));
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,260 +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?a=aaa&b=bbb'`].join(` \\n `));
});
test('Exports GET with params and hash', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app/path#section',
urlParameters: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].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 '{"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 '{"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 '{"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 '{"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('Bearer auth with custom prefix', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'abc123',
prefix: 'Token',
},
}),
).toEqual(
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(` \\\n `),
);
});
test('Bearer auth with empty prefix', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'xyz789',
prefix: '',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].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 `));
});
test('Stale body data', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
bodyType: 'none',
body: {
text: 'ignore me',
},
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
});
});

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,76 +0,0 @@
# Copy as gRPCurl
An HTTP request action plugin that converts gRPC requests
into [gRPCurl](https://github.com/fullstorydev/grpcurl) commands, enabling easy sharing,
debugging, and execution of gRPC calls outside Yaak.
![Screenshot of context menu](screenshot.png)
## Overview
This plugin adds a "Copy as gRPCurl" action to gRPC requests, converting any gRPC request
into its equivalent executable command. This is useful for debugging gRPC services,
sharing requests with team members, or executing gRPC calls in terminal environments where
`grpcurl` is available.
## How It Works
The plugin analyzes your gRPC request configuration and generates a properly formatted
`grpcurl` command that includes:
- gRPC service and method names
- Server address and port
- Request message data (JSON format)
- Metadata (headers)
- Authentication credentials
- Protocol buffer definitions
## Usage
1. Configure a gRPC request as usual in Yaak
2. Right-click on the request sidebar item
3. Select "Copy as gRPCurl" from the available actions
4. The command is copied to your clipboard
5. Share or execute the command
## Generated gRPCurl Examples
### Simple Unary Call
```bash
grpcurl -plaintext \
-d '{"name": "John Doe"}' \
localhost:9090 \
user.UserService/GetUser
```
### Call with Metadata
```bash
grpcurl -plaintext \
-H "authorization: Bearer my-token" \
-H "x-api-version: v1" \
-d '{"user_id": "12345"}' \
api.example.com:443 \
user.UserService/GetUserProfile
```
### Call with TLS
```bash
grpcurl \
-d '{"query": "search term"}' \
secure-api.example.com:443 \
search.SearchService/Search
```
### Call with Proto Files
```bash
grpcurl -import-path /path/to/protos \
-proto /other/path/to/user.proto \
-d '{"email": "user@example.com"}' \
localhost:9090 \
user.UserService/CreateUser
```

View File

@@ -1,18 +0,0 @@
{
"name": "@yaak/action-copy-grpcurl",
"displayName": "Copy as gRPCurl",
"description": "Copy gRPC request as a grpcurl command",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-copy-grpcurl"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

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.posix.join(f, '..'));
inferredIncludes.add(path.posix.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);
xs.push(NEWLINE);
}
// 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,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,17 +0,0 @@
{
"name": "@yaak/auth-apikey",
"displayName": "API Key Authentication",
"description": "Authenticate requests using an API key",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-apikey"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

View File

@@ -1,53 +0,0 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
authentication: {
name: 'apikey',
label: 'API Key',
shortLabel: 'API Key',
args: [
{
type: 'select',
name: 'location',
label: 'Behavior',
defaultValue: 'header',
options: [
{ label: 'Insert Header', value: 'header' },
{ label: 'Append Query Parameter', value: 'query' },
],
},
{
type: 'text',
name: 'key',
label: 'Key',
dynamic: (_ctx, { values }) => {
return values.location === 'query' ? {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
} : {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
},
},
{
type: 'text',
name: 'value',
label: 'API Key',
optional: true,
password: true,
},
],
async onApply(_ctx, { values }) {
const key = String(values.key ?? '');
const value = String(values.value ?? '');
const location = String(values.location);
if (location === 'query') {
return { setQueryParameters: [{ name: key, value }] };
} else {
return { setHeaders: [{ name: key, value }] };
}
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,49 +0,0 @@
# AWS Signature Version 4 Auth
A plugin for authenticating AWS-compatible requests using the
[AWS Signature Version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
This enables secure, signed requests to AWS services (or any S3-compatible APIs like
Cloudflare R2).
![Screenshot of AWS SigV4 UI](screenshot.png)
## Overview
This plugin provides AWS Signature authentication for API requests in Yaak. SigV4 is used
by nearly all AWS APIs to verify the authenticity and integrity of requests using
cryptographic signatures.
With this plugin, you can securely sign requests to AWS services such as S3, STS, Lambda,
API Gateway, DynamoDB, and more. You can also authenticate against S3-compatible services
like **Cloudflare R2**, **MinIO**, or **Wasabi**.
## How AWS Signature Version 4 Works
SigV4 signs requests by creating a hash of key request components (method, URL, headers,
and optionally the payload) using your AWS credentials. The resulting HMAC signature is
added in the `Authorization` header along with credential scope metadata.
Example header:
```
Authorization: AWS4-HMAC-SHA256 Credential=AKIA…/20251011/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abcdef123456…
```
Each request must include a timestamp (`X-Amz-Date`) and may include a session token if
using temporary credentials.
## Configuration
The plugin presents the following fields:
- **Access Key ID** Your AWS access key identifier
- **Secret Access Key** The secret associated with the access key
- **Session Token** *(optional)* Used for temporary or assumed-role credentials (treated as secret)
- **Region** AWS region (e.g., `us-east-1`)
- **Service** AWS service identifier (e.g., `sts`, `s3`, `execute-api`)
## Usage
1. Configure a request, folder, or workspace to use **AWS SigV4 Authentication**
2. Enter your AWS credentials and target service/region
3. The plugin will automatically sign outgoing requests with valid SigV4 headers

View File

@@ -1,23 +0,0 @@
{
"name": "@yaak/auth-aws",
"displayName": "AWS SigV4",
"description": "Authenticate requests using AWS SigV4 signing",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-aws"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"aws4": "^1.13.2"
},
"devDependencies": {
"@types/aws4": "^1.11.6"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

View File

@@ -1,96 +0,0 @@
import type { CallHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
import type { PluginDefinition } from '@yaakapp/api';
import aws4 from 'aws4';
import type { Request } from 'aws4';
import { URL } from 'node:url';
export const plugin: PluginDefinition = {
authentication: {
name: 'auth-aws-sig-v4',
label: 'AWS Signature',
shortLabel: 'AWS v4',
args: [
{ name: 'accessKeyId', label: 'Access Key ID', type: 'text', password: true },
{
name: 'secretAccessKey',
label: 'Secret Access Key',
type: 'text',
password: true,
},
{
name: 'service',
label: 'Service Name',
type: 'text',
defaultValue: 'sts',
placeholder: 'sts',
description: 'The service that is receiving the request (sts, s3, sqs, ...)',
},
{
name: 'region',
label: 'Region',
type: 'text',
placeholder: 'us-east-1',
description: 'The region that is receiving the request (defaults to us-east-1)',
optional: true,
},
{
name: 'sessionToken',
label: 'Session Token',
type: 'text',
password: true,
optional: true,
description: 'Only required if you are using temporary credentials',
},
],
onApply(_ctx, { values, ...args }): CallHttpAuthenticationResponse {
const accessKeyId = String(values.accessKeyId || '');
const secretAccessKey = String(values.secretAccessKey || '');
const sessionToken = String(values.sessionToken || '') || undefined;
const url = new URL(args.url);
const headers: NonNullable<Request['headers']> = {};
for (const headerName of ['content-type', 'host', 'x-amz-date', 'x-amz-security-token']) {
const v = args.headers.find((h) => h.name.toLowerCase() === headerName);
if (v != null) {
headers[headerName] = v.value;
}
}
if (args.method !== 'GET') {
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
}
const signature = aws4.sign(
{
host: url.host,
method: args.method,
path: url.pathname + (url.search || ''),
service: String(values.service || 'sts'),
region: values.region ? String(values.region) : undefined,
headers,
},
{
accessKeyId,
secretAccessKey,
sessionToken,
},
);
// After signing, aws4 will set:
// - opts.headers["Authorization"]
// - opts.headers["X-Amz-Date"]
// - optionally content sha256 header etc
if (signature.headers == null) {
return {};
}
return {
setHeaders: Object.entries(signature.headers)
.filter(([name]) => name !== 'content-type') // Don't add this because we already have it
.map(([name, value]) => ({ name, value: String(value || '') })),
};
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,44 +0,0 @@
# Basic Authentication
A simple Basic Authentication plugin that implements HTTP Basic Auth according
to [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617), enabling secure
authentication with username and password credentials.
![Screenshot of basic auth UI](screenshot.png)
## Overview
This plugin provides HTTP Basic Authentication support for API requests in Yaak. Basic
Auth is one of the most widely supported authentication methods, making it ideal for APIs
that require simple username/password authentication without the complexity of OAuth
flows.
## How Basic Authentication Works
Basic Authentication encodes your username and password credentials using Base64 encoding
and sends them in the `Authorization` header with each request. The format is:
```
Authorization: Basic <base64-encoded-credentials>
```
Where `<base64-encoded-credentials>` is the Base64 encoding of `username:password`.
## Configuration
The plugin presents two fields:
- **Username**: Username or user identifier
- **Password**: Password or authentication token
## Usage
1. Configure the request, folder, or workspace to use Basic Authentication
2. Enter your username and password in the authentication configuration
3. The plugin will automatically add the proper `Authorization` header to your requests
## Troubleshooting
- **401 Unauthorized**: Verify your username and password are correct
- **403 Forbidden**: Check if your account has the necessary permissions
- **Connection Issues**: Ensure you're using HTTPS for secure transmission

View File

@@ -1,17 +0,0 @@
{
"name": "@yaak/auth-basic",
"displayName": "Basic Authentication",
"description": "Authenticate requests using Basic Auth",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-basic"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

View File

@@ -1,26 +0,0 @@
import type { 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,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,47 +0,0 @@
# Bearer Token Authentication Plugin
A Bearer Token authentication plugin for Yaak that
implements [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750), enabling secure API
access using tokens, API keys, and other bearer credentials.
![Screenshot of bearer auth UI](screenshot.png)
## Overview
This plugin provides Bearer Token authentication support for your API requests in Yaak.
Bearer Token authentication is widely used in modern APIs, especially those following REST
principles and OAuth 2.0 standards. It's the preferred method for APIs that issue access
tokens, API keys, or other bearer credentials.
## How Bearer Token Authentication Works
Bearer Token authentication sends your token in the `Authorization` header with each
request using the Bearer scheme:
```
Authorization: Bearer <your-token>
```
The token is transmitted as-is without any additional encoding, making it simple and
efficient for API authentication.
## Configuration
The plugin requires only one field:
- **Token**: Your bearer token, access token, API key, or other credential
- **Prefix**: The prefix to use for the Authorization header, which will be of the
format "<PREFIX> <TOKEN>"
## Usage
1. Configure the request, folder, or workspace to use Bearer Authentication
2. Enter the token and optional prefix in the authentication configuration
3. The plugin will automatically add the proper `Authorization` header to your requests
## Troubleshooting
- **401 Unauthorized**: Verify your token is valid and not expired
- **403 Forbidden**: Check if your token has the necessary permissions/scopes
- **Invalid Token Format**: Ensure you're using the complete token without truncation
- **Token Expiration**: Refresh or regenerate expired tokens

View File

@@ -1,18 +0,0 @@
{
"name": "@yaak/auth-bearer",
"displayName": "Bearer Authentication",
"description": "Authenticate requests using bearer authentication",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-bearer"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -1,39 +0,0 @@
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
import type { 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,
},
{
type: 'text',
name: 'prefix',
label: 'Prefix',
optional: true,
placeholder: '',
defaultValue: 'Bearer',
description:
'The prefix to use for the Authorization header, which will be of the format "<PREFIX> <TOKEN>".',
},
],
async onApply(_ctx, { values }) {
return { setHeaders: [generateAuthorizationHeader(values)] };
},
},
};
function generateAuthorizationHeader(values: CallHttpAuthenticationRequest['values']) {
const token = String(values.token || '').trim();
const prefix = String(values.prefix || '').trim();
const value = `${prefix} ${token}`.trim();
return { name: 'Authorization', value };
}

View File

@@ -1,67 +0,0 @@
import type { Context } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { plugin } from '../src';
const ctx = {} as Context;
describe('auth-bearer', () => {
test('No values', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: {},
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: '' }] });
});
test('Only token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { token: 'my-token' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'my-token' }] });
});
test('Only prefix', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: 'Hello' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello' }] });
});
test('Prefix and token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: 'Hello', token: 'my-token' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
});
test('Extra spaces', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: '\t Hello ', token: ' \nmy-token ' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
});
});

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,53 +0,0 @@
# JSON Web Token (JWT) Authentication
A [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519) (JWT) authentication
plugin that supports token generation, signing, and automatic header management.
![Screenshot of JWT auth UI](screenshot.png)
## Overview
This plugin provides JWT authentication support for API requests. JWT is a compact,
URL-safe means of representing claims between two parties, commonly used for
authentication and information exchange in modern web applications and APIs.
## How JWT Authentication Works
JWT authentication involves creating a signed token containing claims about the user or
application. The token is sent in the `Authorization` header:
```
Authorization: Bearer <jwt-token>
```
A JWT consists of three parts separated by dots:
- **Header**: Contains the token type and signing algorithm
- **Payload**: Contains the claims (user data, permissions, expiration, etc.)
- **Signature**: Ensures the token hasn't been tampered with
## Usage
1. Configure the request, folder, or workspace to use JWT Authentication
2. Set up your signing algorithm and secret/key
3. Configure the required claims for your JWT
4. The plugin will generate, sign, and include the JWT in your requests
## Common Use Cases
JWT authentication is commonly used for:
- **Microservices Authentication**: Service-to-service communication
- **API Gateway Integration**: Authenticating with API gateways
- **Single Sign-On (SSO)**: Sharing authentication across applications
- **Stateless Authentication**: No server-side session storage required
- **Mobile App APIs**: Secure authentication for mobile applications
- **Third-party Integrations**: Authenticating with external services
## Troubleshooting
- **Invalid Signature**: Check your secret/key and algorithm configuration
- **Token Expired**: Verify expiration time settings
- **Invalid Claims**: Ensure required claims are properly configured
- **Algorithm Mismatch**: Verify the algorithm matches what the API expects
- **Key Format Issues**: Ensure RSA keys are in the correct PEM format

View File

@@ -1,23 +0,0 @@
{
"name": "@yaak/auth-jwt",
"displayName": "JSON Web Tokens",
"description": "Authenticate requests using JSON web tokens (JWT)",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-jwt"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.7"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -1,68 +0,0 @@
import type { 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 (typeof algorithms)[number],
});
const value = `Bearer ${token}`;
return { setHeaders: [{ name: 'Authorization', value }] };
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,72 +0,0 @@
# OAuth 2.0 Authentication
An [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) authentication plugin that
supports multiple grant types and flows, enabling secure API authentication with OAuth 2.0
providers.
![Screenshot of OAuth 2.0 auth UI](screenshot.png)
## Overview
This plugin implements OAuth 2.0 authentication for requests, supporting the most common
OAuth 2.0 grant types used in modern API integrations. It handles token management,
automatic refresh, and [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key
for Code Exchange) for enhanced security.
## Supported Grant Types
### Authorization Code Flow
The most secure and commonly used OAuth 2.0 flow for web applications.
- Standard Authorization Code flow
- Optional PKCE (Proof Key for Code Exchange) for enhanced security
- Supports automatic token refresh
### Client Credentials Flow
Ideal for server-to-server authentication where no user interaction is required.
### Implicit Flow
Legacy flow for single-page applications (deprecated but still supported):
- Direct access token retrieval
- No refresh token support
- Suitable for legacy integrations
### Resource Owner Password Credentials Flow
Direct username/password authentication.
- User credentials are exchanged directly for tokens
- Should only be used with trusted applications
- Supports automatic token refresh
## Features
- **Automatic Token Management**: Handles token storage, expiration, and refresh
automatically
- **PKCE Support**: Enhanced security for Authorization Code flow
- **Token Persistence**: Stores tokens between sessions
- **Flexible Configuration**: Supports custom authorization and token endpoints
- **Scope Management**: Configure required OAuth scopes for your API
- **Error Handling**: Comprehensive error handling and user feedback
## Usage
1. Configure the request, folder, or workspace to use OAuth 2.0 Authentication
2. Select the appropriate grant type for your use case
3. Fill in the required OAuth 2.0 parameters from your API provider
4. The plugin will handle the authentication flow and token management automatically
## Compatibility
This plugin is compatible with OAuth 2.0 providers including:
- Google APIs
- Microsoft Graph
- GitHub API
- Auth0
- Okta
- And many other OAuth 2.0 compliant services

View File

@@ -1,18 +0,0 @@
{
"name": "@yaak/auth-oauth2",
"displayName": "OAuth 2.0",
"description": "Authenticate requests using OAuth 2.0",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-oauth2"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

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,112 +0,0 @@
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
import { deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './util';
export async function getOrRefreshAccessToken(
ctx: Context,
tokenArgs: TokenStoreArgs,
{
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, tokenArgs);
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, tokenArgs);
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, tokenArgs, newResponse);
}

View File

@@ -1,162 +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, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store';
import { extractCode } from '../util';
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 tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: authorizationUrlRaw,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody,
});
if (token != null) {
return token;
}
let authorizationUrl: URL;
try {
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
} catch {
throw new Error(`Invalid authorization 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 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({
dataDirKey,
url: authorizationUrlStr,
label: 'oauth-authorization-url',
async onClose() {
if (!foundCode) {
reject(new Error('Authorization window closed'));
}
},
async onNavigate({ url: urlStr }) {
let code;
try {
code = extractCode(urlStr, redirectUri);
} catch (err) {
reject(err);
close();
return;
}
if (!code) {
return;
}
// 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, tokenArgs, 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,49 +0,0 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import type { TokenStoreArgs } from '../store';
import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
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 tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getToken(ctx, tokenArgs);
if (token && !isTokenExpired(token)) {
return token;
}
const response = await fetchAccessToken(ctx, {
grantType: 'client_credentials',
accessTokenUrl,
audience,
clientId,
clientSecret,
scope,
credentialsInBody,
params: [],
});
return storeToken(ctx, tokenArgs, response);
}

View File

@@ -1,102 +0,0 @@
import type { Context } from '@yaakapp/api';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getDataDirKey , getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
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 tokenArgs = {
contextId,
clientId,
accessTokenUrl: null,
authorizationUrl: authorizationUrlRaw,
};
const token = await getToken(ctx, tokenArgs);
if (token != null && !isTokenExpired(token)) {
return token;
}
let authorizationUrl: URL;
try {
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
} catch {
throw new Error(`Invalid authorization 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 dataDirKey = await getDataDirKey(ctx, contextId);
const { close } = await ctx.window.openUrl({
dataDirKey,
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, tokenArgs, response));
} catch (err) {
reject(err);
}
},
});
});
return newToken;
}

View File

@@ -1,62 +0,0 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken, TokenStoreArgs } 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 tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
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, tokenArgs, response);
}

View File

@@ -1,415 +0,0 @@
import type {
Context,
FormInputSelectOption,
GetHttpAuthenticationConfigRequest,
JsonPrimitive,
PluginDefinition,
} from '@yaakapp/api';
import {
DEFAULT_PKCE_METHOD,
genPkceCodeVerifier,
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, TokenStoreArgs } 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, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
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, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
if (await deleteToken(ctx, tokenArgs)) {
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);
},
},
],
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: 'headerName',
label: 'Header Name',
defaultValue: 'Authorization',
},
{
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, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
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 === '' || accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
authorizationUrl:
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 headerName = stringArg(values, 'headerName') || 'Authorization';
const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim();
return { setHeaders: [{ name: headerName, 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,80 +0,0 @@
import type { Context } from '@yaakapp/api';
import { createHash } from 'node:crypto';
export async function storeToken(
ctx: Context,
args: TokenStoreArgs,
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(args), token);
return token;
}
export async function getToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.get<AccessToken>(tokenStoreKey(args));
}
export async function deleteToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.delete(tokenStoreKey(args));
}
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}`;
}
export interface TokenStoreArgs {
contextId: string;
clientId: string;
accessTokenUrl: string | null;
authorizationUrl: string | null;
}
/**
* Generate a store key to use based on some arguments. The arguments will be normalized a bit to
* account for slight variations (like domains with and without a protocol scheme).
*/
function tokenStoreKey(args: TokenStoreArgs) {
const hash = createHash('md5');
if (args.contextId) hash.update(args.contextId.trim());
if (args.clientId) hash.update(args.clientId.trim());
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
const key = hash.digest('hex');
return ['token', key].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,85 +0,0 @@
import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}
export function extractCode(urlStr: string, redirectUri: string | null): string | null {
const url = new URL(urlStr);
if (!urlMatchesRedirect(url, redirectUri)) {
console.log('[oauth2] URL does not match redirect origin/path; skipping.');
return null;
}
// Prefer query param; fall back to fragment if query lacks it
const query = url.searchParams;
const queryError = query.get('error');
const queryDesc = query.get('error_description');
const queryUri = query.get('error_uri');
let hashParams: URLSearchParams | null = null;
if (url.hash && url.hash.length > 1) {
hashParams = new URLSearchParams(url.hash.slice(1));
}
const hashError = hashParams?.get('error');
const hashDesc = hashParams?.get('error_description');
const hashUri = hashParams?.get('error_uri');
const error = queryError || hashError;
if (error) {
const desc = queryDesc || hashDesc;
const uri = queryUri || hashUri;
let message = `Failed to authorize: ${error}`;
if (desc) message += ` (${desc})`;
if (uri) message += ` [${uri}]`;
throw new Error(message);
}
const queryCode = query.get('code');
if (queryCode) return queryCode;
const hashCode = hashParams?.get('code');
if (hashCode) return hashCode;
console.log('[oauth2] Code not found');
return null;
}
export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean {
if (!redirectUrl) return true;
let redirect;
try {
redirect = new URL(redirectUrl);
} catch {
console.log('[oauth2] Invalid redirect URI; skipping.');
return false;
}
const sameProtocol = url.protocol === redirect.protocol;
const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase();
const normalizePort = (u: URL) =>
(u.protocol === 'https:' && (!u.port || u.port === '443')) ||
(u.protocol === 'http:' && (!u.port || u.port === '80'))
? ''
: u.port;
const samePort = normalizePort(url) === normalizePort(redirect);
const normPath = (p: string) => {
const withLeading = p.startsWith('/') ? p : `/${p}`;
// strip trailing slashes, keep root as "/"
return withLeading.replace(/\/+$/g, '') || '/';
};
// Require redirect path to be a prefix of the navigated URL path
const urlPath = normPath(url.pathname);
const redirectPath = normPath(redirect.pathname);
const pathMatches = urlPath === redirectPath || urlPath.startsWith(`${redirectPath}/`);
return sameProtocol && sameHost && samePort && pathMatches;
}

View File

@@ -1,109 +0,0 @@
import { describe, test, expect } from 'vitest';
import { extractCode } from '../src/util';
describe('extractCode', () => {
test('extracts code from query when same origin + path', () => {
const url = 'https://app.example.com/cb?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc123');
});
test('extracts code from query with weird path', () => {
const url = 'https://app.example.com/cbwithextra?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('allows trailing slash differences', () => {
expect(extractCode('https://app.example.com/cb/?code=abc', 'https://app.example.com/cb')).toBe(
'abc',
);
expect(extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com/cb/')).toBe(
'abc',
);
});
test('treats default ports as equal (https:443, http:80)', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:443/cb'),
).toBe('abc');
expect(extractCode('http://app.example.com/cb?code=abc', 'http://app.example.com:80/cb')).toBe(
'abc',
);
});
test('rejects different port', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:8443/cb'),
).toBeNull();
});
test('rejects different hostname (including subdomain changes)', () => {
expect(
extractCode('https://evil.example.com/cb?code=abc', 'https://app.example.com/cb'),
).toBeNull();
});
test('requires path to start with redirect path (ignoring query/hash)', () => {
// same origin but wrong path -> null
expect(
extractCode('https://app.example.com/other?code=abc', 'https://app.example.com/cb'),
).toBeNull();
// deeper subpath under the redirect path -> allowed (prefix match)
expect(
extractCode('https://app.example.com/cb/deep?code=abc', 'https://app.example.com/cb'),
).toBe('abc');
});
test('works with custom schemes', () => {
expect(extractCode('myapp://cb?code=abc', 'myapp://cb')).toBe('abc');
});
test('prefers query over fragment when both present', () => {
const url = 'https://app.example.com/cb?code=queryCode#code=hashCode';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('queryCode');
});
test('extracts code from fragment when query lacks code', () => {
const url = 'https://app.example.com/cb#code=fromHash&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('fromHash');
});
test('returns null if no code present (query or fragment)', () => {
const url = 'https://app.example.com/cb?state=only';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('returns null when provider reports an error', () => {
const url = 'https://app.example.com/cb?error=access_denied&error_description=oopsy';
const redirect = 'https://app.example.com/cb';
expect(() => extractCode(url, redirect)).toThrow('Failed to authorize: access_denied');
});
test('when redirectUri is null, extracts code from any URL', () => {
expect(extractCode('https://random.example.com/whatever?code=abc', null)).toBe('abc');
});
test('handles extra params gracefully', () => {
const url = 'https://app.example.com/cb?foo=1&bar=2&code=abc&baz=3';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
test('ignores fragment noise when code is in query', () => {
const url = 'https://app.example.com/cb?code=abc#some=thing';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
// If you decide NOT to support fragment-based codes, flip these to expect null or mark as .skip
test('supports fragment-only code for response_mode=fragment providers', () => {
const url = 'https://app.example.com/cb#state=xyz&code=abc';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
});

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,59 +0,0 @@
# JSONPath
A filter plugin that enables [JSONPath](https://en.wikipedia.org/wiki/JSONPath)
extraction and filtering for JSON responses, making it easy to extract specific values
from complex JSON structures.
![Screenshot of JSONPath filtering](screenshot.png)
## Overview
This plugin provides JSONPath filtering for responses in Yaak. JSONPath is a query
language for JSON, similar to XPath for XML, that provides the ability to extract data
from JSON documents using a simple, expressive syntax. This is useful for working with
complex API responses where you need to only view a small subset of response data.
## How JSONPath Works
JSONPath uses a dot-notation syntax to navigate JSON structures:
- `$` - Root element
- `.` - Child element
- `..` - Recursive descent
- `*` - Wildcard
- `[]` - Array index or filter
## JSONPath Syntax Examples
### Basic Navigation
```
$.store.book[0].title # First book title
$.store.book[*].author # All book authors
$.store.book[-1] # Last book
$.store.book[0,1] # First two books
$.store.book[0:2] # First two books (slice)
```
### Filtering
```
$.store.book[?(@.price < 10)] # Books under $10
$.store.book[?(@.author == 'Tolkien')] # Books by Tolkien
$.store.book[?(@.category == 'fiction')] # Fiction books
```
### Recursive Search
```
$..author # All authors anywhere in the document
$..book[2] # Third book anywhere
$..price # All prices in the document
```
## Usage
1. Make an API request that returns JSON data
2. Below the response body, click the filter icon
3. Enter a JSONPath expression
4. View the extracted data in the results panel

View File

@@ -1,23 +0,0 @@
{
"name": "@yaak/filter-jsonpath",
"displayName": "JSONPath Filter",
"description": "Filter JSON response data using JSONPath expressions",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/filter-jsonpath"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

View File

@@ -1,18 +0,0 @@
import type { 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);
try {
const filtered = JSONPath({ path: args.filter, json: parsed });
return { content: JSON.stringify(filtered, null, 2) };
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,16 +0,0 @@
{
"name": "@yaak/filter-xpath",
"displayName": "XPath Filter",
"description": "Filter response XML data using XPath expressions",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"@xmldom/xmldom": "^0.9.8",
"xpath": "^0.0.34"
}
}

View File

@@ -1,25 +0,0 @@
import { DOMParser } from '@xmldom/xmldom';
import type { PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath';
export const plugin: PluginDefinition = {
filter: {
name: 'XPath',
description: 'Filter XPath',
onFilter(_ctx, args) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const doc: any = new DOMParser().parseFromString(args.payload, 'text/xml');
try {
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { content: result.map((r) => String(r)).join('\n') };
} else {
// Not sure what cases this happens in (?)
return { content: String(result) };
}
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,19 +0,0 @@
{
"name": "@yaak/importer-curl",
"displayName": "cURL Importer",
"description": "Import requests from cURL commands",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {
"shell-quote": "^1.8.1"
},
"devDependencies": {
"@types/shell-quote": "^1.7.5"
}
}

View File

@@ -1,439 +0,0 @@
import type { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
import type { ControlOperator, ParseEntry } from 'shell-quote';
import { parse } 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 }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 //
// ~~~~~~~~~~~~~~~~~ //
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
const [baseUrl, search] = splitOnce(urlArg, '?');
const urlParameters: HttpUrlParameter[] =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true };
}) ?? [];
const 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[] {
const 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;
const params = p.split("&");
for (const param of params) {
const [name, value] = splitOnce(param, '=');
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,427 +0,0 @@
import type { 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 },
],
}),
],
},
});
});
test('Imports weird body', () => {
expect(convertCurl(`curl 'https://yaak.app' -X POST --data-raw 'foo=bar=baz'`)).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: "POST",
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [{ name: 'foo', value: 'bar=baz', enabled: true }],
},
headers: [
{
enabled: true,
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
},
],
}),
],
},
});
});
});
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,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1,16 +0,0 @@
{
"name": "@yaak/importer-insomnia",
"displayName": "Insomnia Importer",
"description": "Import data from Insomnia",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {
"yaml": "^2.4.2"
}
}

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: unknown) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isJSString(obj: unknown) {
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,37 +0,0 @@
import type { 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: unknown;
try {
parsed = JSON.parse(contents);
} catch {
// Fall through
}
try {
parsed = parsed ?? YAML.parse(contents);
} catch {
// Fall through
}
if (!isJSObject(parsed)) return null;
const result = convertInsomniaV5(parsed) ?? convertInsomniaV4(parsed);
return deleteUndefinedAttrs(result);
}

View File

@@ -1,206 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV4(parsed: 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: any) => 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] {
isParent ??= e.parentId === workspaceId;
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),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
sortPriority: e.metaSortKey, // Will be added to Yaak later
parentModel: isParent ? 'workspace' : 'environment',
parentId: null,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}

View File

@@ -1,311 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV5(parsed: any) {
// Assert parsed is object
if (parsed == null || typeof parsed !== 'object') {
return null;
}
if (!('collection' in parsed) || !Array.isArray(parsed.collection)) {
return null;
}
const resources: PartialImportResources = {
environments: [],
folders: [],
grpcRequests: [],
httpRequests: [],
websocketRequests: [],
workspaces: [],
};
// Import workspaces
const meta = ('meta' in parsed ? parsed.meta : {}) as Record<string, any>;
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,
...importHeaders(parsed),
...importAuthentication(parsed),
});
// Import environments
resources.environments.push(
importEnvironment(parsed.environments, meta.id, true),
...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)),
);
// Import folders
const nextFolder = (children: any[], parentId: string) => {
for (const child of children ?? []) {
if (!isJSObject(child)) continue;
if (Array.isArray(child.children)) {
const { folder, environment } = importFolder(child, meta.id, parentId);
resources.folders.push(folder);
if (environment) resources.environments.push(environment);
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(obj: any) {
const headers = (obj.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(obj: any) {
let authenticationType: string | null = null;
let authentication = {};
if (obj.authentication?.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(obj.authentication.token),
};
} else if (obj.authentication?.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(obj.authentication.username),
password: convertSyntax(obj.authentication.password),
};
}
return { authenticationType, authentication } as const;
}
function importFolder(
f: any,
workspaceId: string,
parentId: string,
): {
folder: PartialImportResources['folders'][0];
environment: PartialImportResources['environments'][0] | null;
} {
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;
let environment: PartialImportResources['environments'][0] | null = null;
if (Object.keys(f.environment ?? {}).length > 0) {
environment = {
id: convertId(id + 'folder'),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
public: true,
parentModel: 'folder',
parentId: convertId(id),
model: 'environment',
name: 'Folder Environment',
variables: Object.entries(f.environment ?? {}).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}
return {
folder: {
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,
...importAuthentication(f),
...importHeaders(f),
},
environment,
};
}
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,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
sortPriority: sortKey, // Will be added to Yaak later
parentModel: isParent ? 'workspace' : 'environment',
parentId: null,
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,129 +0,0 @@
{
"resources": {
"environments": [
{
"createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209",
"sortPriority": 1736781343767,
"parentId": null,
"parentModel": "workspace",
"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,
"parentId": null,
"parentModel": "environment",
"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,
"parentId": null,
"parentModel": "environment",
"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,74 +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
environment:
folder_env_var: testing
- 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,118 +0,0 @@
{
"resources": {
"environments": [
{
"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",
"parentId": null,
"parentModel": "workspace"
},
{
"createdAt": "2025-05-16T16:48:12.298",
"id": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7folder",
"model": "environment",
"name": "Folder Environment",
"parentId": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
"parentModel": "folder",
"public": true,
"updatedAt": "2025-05-16T16:49:02.427",
"variables": [
{
"enabled": true,
"name": "folder_env_var",
"value": "testing"
}
],
"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",
"authentication": {},
"authenticationType": null,
"headers": [
{
"enabled": true,
"name": "foo",
"value": "bar"
}
]
}
],
"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",
"authentication": {},
"authenticationType": null,
"headers": []
}
]
}
}

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"

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