Compare commits

..

31 Commits

Author SHA1 Message Date
Gregory Schier
9ab02130b0 Fix sync import issues:
https://feedback.yaak.app/p/yaml-error-missing-field-type-at-line-4521-column-1
2025-06-27 13:32:52 -07:00
Gregory Schier
25d50246c0 Revert notification endpoint URL for dev 2025-06-27 11:58:04 -07:00
Gregory Schier
bb0cc16a70 Use API client for notifications/license 2025-06-25 08:17:17 -07:00
Gregory Schier
8817be679b Fix PKCE flow and clean up other flows 2025-06-25 07:10:11 -07:00
Gregory Schier
f476d87613 Add back unsigned memory entitlement 2025-06-24 06:21:07 -07:00
Gregory Schier
1438e8bacc Upgrade eslint and fix issues 2025-06-23 14:09:09 -07:00
Gregory Schier
7be2767527 Fix lint error 2025-06-23 09:51:44 -07:00
Gregory Schier
a1b1eafd39 Add links to plugins 2025-06-23 09:46:54 -07:00
Gregory Schier
1948fb78bd Fix bad import 2025-06-23 08:57:31 -07:00
Gregory Schier
cb7c44cc65 Install plugins from Yaak plugin registry (#230) 2025-06-23 08:55:38 -07:00
Gregory Schier
b5620fcdf3 Merge pull request #227
* Search and install plugins PoC

* Checksum

* Tab sidebar for settings

* Fix nested tabs, and tweaks

* Table for plugin results

* Deep links working

* Focus window during deep links

* Merge branch 'master' into plugin-directory

* More stuff
2025-06-22 07:06:43 -07:00
Mr0Bread
b8e6dbc7c7 GraphQL Documentation explorer (#208) 2025-06-17 17:08:39 -07:00
Gregory Schier
aadfbfdfca Fix lint errors 2025-06-10 08:16:02 -07:00
Gregory Schier
383fd05c6c Split appearance settings into theme/interface 2025-06-09 21:19:44 -07:00
Gregory Schier
be0a8fc27a Add proxy bypass setting and rewrite proxy logic 2025-06-09 14:29:12 -07:00
Gregory Schier
648a1ac53c Update DEVELOPMENT.md 2025-06-08 22:49:43 -07:00
Gregory Schier
9fab37fb17 Custom font selection (#226) 2025-06-08 22:48:27 -07:00
Gregory Schier
e0aaa33ccb Update README 2025-06-08 08:17:11 -07:00
Gregory Schier
20f7d20031 Enable socks reqwest feature 2025-06-08 08:10:55 -07:00
Gregory Schier
4d90bc78b1 Link docs in readme 2025-06-08 08:05:59 -07:00
Gregory Schier
97763a1301 Add README to types package 2025-06-08 08:03:11 -07:00
Gregory Schier
d8b5a201b6 I'm stupid 2025-06-07 20:17:28 -07:00
Gregory Schier
88e87a1999 Fix stupid typo 2025-06-07 20:15:32 -07:00
Gregory Schier
2c4c1abd20 Pin tauri cli 2025-06-07 20:04:04 -07:00
Gregory Schier
67026fc5b3 Tweak 2025-06-07 19:37:28 -07:00
Gregory Schier
423a1a0a52 Fix environment color editing 2025-06-07 19:32:23 -07:00
Gregory Schier
1abe01aa5a Embed migrations into Rust binary 2025-06-07 19:25:36 -07:00
Gregory Schier
d0fde99b1c Environment colors (#225) 2025-06-07 18:21:54 -07:00
Gregory Schier
27901231dc Clarify proxy HTTP/HTTPS setting 2025-06-06 20:34:23 -07:00
Gregory Schier
1d9d80319b Upgrade dependencies 2025-06-06 19:32:25 -07:00
Gregory Schier
f62e90297d Fix recent workspaces when open in new window 2025-06-06 14:10:30 -07:00
190 changed files with 5457 additions and 2973 deletions

View File

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

View File

@@ -1,49 +0,0 @@
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

@@ -50,11 +50,9 @@ New migrations can be created from the `src-tauri/` directory:
npm run migration
```
Run the app to apply the migrations.
Rerun the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
_Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation

88
eslint.config.cjs Normal file
View File

@@ -0,0 +1,88 @@
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/',
'**/.eslintrc.cjs',
'**/.prettierrc.cjs',
'src-web/postcss.config.cjs',
'src-web/vite.config.ts',
]),
]);

1367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -66,20 +67,23 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
"eslint": "^8",
"eslint-config-prettier": "^8",
"eslint-plugin-import": "^2.31.0",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
}

View File

@@ -0,0 +1,28 @@
# 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 @yaakapp/api
```

View File

@@ -1,6 +1,20 @@
{
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"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",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -0,0 +1,8 @@
// 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,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
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 { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } 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, };
@@ -378,7 +372,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & 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": "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_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 InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_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;

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -0,0 +1,5 @@
// 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,5 +1,6 @@
import {
BootRequest,
BootResponse,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
@@ -52,9 +53,22 @@ export class PluginInstance {
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
this.#mod = {};
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const bootResponse: BootResponse = {
name: this.#pkg.name ?? 'unknown',
version: this.#pkg.version ?? '0.0.1',
};
const fileChangeCallback = async () => {
this.#importModule();
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
return this.#sendPayload(
windowContextNone,
{ type: 'reload_response', ...bootResponse },
null,
);
};
if (this.#workerData.bootRequest.watch) {
@@ -62,12 +76,6 @@ 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();
}

View File

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

View File

@@ -1,8 +1,8 @@
import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { AccessTokenRawResponse } from './store';
import type { AccessTokenRawResponse } from './store';
export async function getAccessToken(
export async function fetchAccessToken(
ctx: Context,
{
accessTokenUrl,

View File

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

View File

@@ -1,29 +1,34 @@
import { Context, HttpRequest } from '@yaakapp/api';
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse } from './store';
import { deleteToken, getToken, storeToken } from './store';
export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
forceRefresh?: boolean;
}): Promise<AccessToken | null> {
export async function getOrRefreshAccessToken(
ctx: Context,
contextId: string,
{
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
forceRefresh?: boolean;
},
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
if (token == null) {
return null;
}
const now = Date.now();
const isExpired = token.expiresAt && now > token.expiresAt;
const isExpired = isTokenExpired(token);
// Return the current access token if it's still valid
if (!isExpired && !forceRefresh) {
@@ -79,7 +84,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
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);
throw new Error(
'Failed to refresh access token with status=' + resp.status + ' and body=' + body,
);
}
let response;
@@ -90,7 +97,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
}
if (response.error) {
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
throw new Error(
`Failed to fetch access token with ${response.error} -> ${response.error_description}`,
);
}
const newResponse: AccessTokenRawResponse = {

View File

@@ -1,8 +1,9 @@
import { Context } from '@yaakapp/api';
import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import { getAccessToken } from '../getAccessToken';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, getDataDirKey, storeToken } from '../store';
import type { AccessToken } from '../store';
import { getDataDirKey, storeToken } from '../store';
export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain';
@@ -34,8 +35,8 @@ export async function getAuthorizationCode(
audience: string | null;
credentialsInBody: boolean;
pkce: {
challengeMethod: string | null;
codeVerifier: string | null;
challengeMethod: string;
codeVerifier: string;
} | null;
tokenName: 'access_token' | 'id_token';
},
@@ -59,26 +60,25 @@ export async function getAuthorizationCode(
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set(
'code_challenge',
createPkceCodeChallenge(verifier, challengeMethod),
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
);
authorizationUrl.searchParams.set('code_challenge_method', challengeMethod);
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
console.log('[oauth2] Authorizing', authorizationUrlStr);
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
// eslint-disable-next-line no-async-promise-executor
const code = await new Promise<string>(async (resolve, reject) => {
let foundCode = false;
let { close } = await ctx.window.openUrl({
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
dataDirKey: await getDataDirKey(ctx, contextId),
dataDirKey,
async onClose() {
if (!foundCode) {
reject(new Error('Authorization window closed'));
@@ -89,6 +89,7 @@ export async function getAuthorizationCode(
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
if (url.searchParams.has('error')) {
close();
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
}
@@ -101,37 +102,35 @@ export async function getAuthorizationCode(
// Close the window here, because we don't need it anymore!
foundCode = true;
close();
console.log('[oauth2] Code found');
const response = await getAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
try {
resolve(await storeToken(ctx, contextId, response, tokenName));
} catch (err) {
reject(err);
}
resolve(code);
},
});
});
console.log('[oauth2] Code found');
const response = await fetchAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
return storeToken(ctx, contextId, response, tokenName);
}
function createPkceCodeVerifier() {
export function genPkceCodeVerifier() {
return encodeForPkce(randomBytes(32));
}
function createPkceCodeChallenge(verifier: string, method: string) {
function pkceCodeChallenge(verifier: string, method: string) {
if (method === 'plain') {
return verifier;
}

View File

@@ -1,5 +1,6 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import { getToken, storeToken } from '../store';
export async function getClientCredentials(
@@ -22,13 +23,11 @@ export async function getClientCredentials(
},
) {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
if (token && !isTokenExpired(token)) {
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
grantType: 'client_credentials',
accessTokenUrl,
audience,

View File

@@ -1,7 +1,9 @@
import { Context } from '@yaakapp/api';
import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store';
import type { Context } from '@yaakapp/api';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getToken, storeToken } from '../store';
export function getImplicit(
export async function getImplicit(
ctx: Context,
contextId: string,
{
@@ -24,31 +26,30 @@ export function getImplicit(
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
}
const token = await getToken(ctx, contextId);
if (token != null && !isTokenExpired(token)) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
const authorizationUrlStr = authorizationUrl.toString();
// eslint-disable-next-line no-async-promise-executor
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
let foundAccessToken = false;
let { close } = await ctx.window.openUrl({
const authorizationUrlStr = authorizationUrl.toString();
const { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: 'oauth-authorization-url',
async onClose() {
@@ -76,11 +77,13 @@ export function getImplicit(
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try {
resolve(await storeToken(ctx, contextId, response));
resolve(storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
},
});
});
return newToken;
}

View File

@@ -1,7 +1,8 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, storeToken } from '../store';
import type { AccessToken} from '../store';
import { storeToken } from '../store';
export async function getPassword(
ctx: Context,
@@ -37,7 +38,7 @@ export async function getPassword(
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,

View File

@@ -1,4 +1,4 @@
import {
import type {
Context,
FormInputSelectOption,
GetHttpAuthenticationConfigRequest,
@@ -6,6 +6,7 @@ import {
PluginDefinition,
} from '@yaakapp/api';
import {
genPkceCodeVerifier,
DEFAULT_PKCE_METHOD,
getAuthorizationCode,
PKCE_PLAIN,
@@ -14,7 +15,8 @@ import {
import { getClientCredentials } from './grants/clientCredentials';
import { getImplicit } from './grants/implicit';
import { getPassword } from './grants/password';
import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store';
import type { AccessToken } from './store';
import { deleteToken, getToken, resetDataDirKey } from './store';
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
@@ -219,9 +221,9 @@ export const plugin: PluginDefinition = {
},
{
type: 'text',
name: 'pkceCodeVerifier',
name: 'pkceCodeChallenge',
label: 'Code Verifier',
placeholder: 'Automatically generated if not provided',
placeholder: 'Automatically generated when not set',
optional: true,
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
},
@@ -325,8 +327,8 @@ export const plugin: PluginDefinition = {
credentialsInBody,
pkce: values.usePkce
? {
challengeMethod: stringArg(values, 'pkceChallengeMethod'),
codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'),
challengeMethod: stringArg(values, 'pkceChallengeMethod') || DEFAULT_PKCE_METHOD,
codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(),
}
: null,
tokenName: tokenName,

View File

@@ -27,7 +27,7 @@ async function createMigration() {
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/migrations');
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {

3018
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ pub enum Error {
#[error(transparent)]
SyncError(#[from] yaak_sync::error::Error),
#[error(transparent)]
CryptoError(#[from] yaak_crypto::error::Error),
@@ -28,18 +28,21 @@ pub enum Error {
#[error(transparent)]
PluginError(#[from] yaak_plugins::error::Error),
#[error(transparent)]
CommonError(#[from] yaak_common::error::Error),
#[error("Updater error: {0}")]
UpdaterError(#[from] tauri_plugin_updater::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::error::Error),
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
#[error("Event source error: {0}")]
EventSourceError(#[from] eventsource_client::Error),
#[error("I/O error: {0}")]
IOError(#[from] io::Error),

View File

@@ -43,18 +43,6 @@ pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> Laun
info
}
pub fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
pub async fn get_num_launches<R: Runtime>(app_handle: &AppHandle<R>) -> i32 {
app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0)
}

View File

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{Method, Response};
use reqwest::{Method, NoProxy, Response};
use reqwest::{Proxy, Url, multipart};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -120,25 +120,39 @@ pub async fn send_http_request<R: Runtime>(
https,
auth,
disabled,
bypass,
}) if !disabled => {
debug!("Using proxy http={http} https={https}");
let mut proxy = Proxy::custom(move |url| {
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
let proxy_url = match (url.scheme(), http, https) {
("http", Some(proxy_url), _) => Some(proxy_url),
("https", _, Some(proxy_url)) => Some(proxy_url),
_ => None,
debug!("Using proxy http={http} https={https} bypass={bypass}");
if !http.is_empty() {
match Proxy::http(http) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth.clone() {
debug!("Using http proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply http proxy {e:?}");
}
};
}
if !https.is_empty() {
match Proxy::https(https) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using https proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply https proxy {e:?}");
}
};
proxy_url
});
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
client_builder = client_builder.proxy(proxy);
}
_ => {} // Nothing to do for this one, as it is the default
}

105
src-tauri/src/import.rs Normal file
View File

@@ -0,0 +1,105 @@
use crate::error::Result;
use log::info;
use std::collections::BTreeMap;
use std::fs::read_to_string;
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::models::{
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
use yaak_plugins::manager::PluginManager;
pub(crate) async fn import_data<R: Runtime>(
window: &WebviewWindow<R>,
file_path: &str,
) -> Result<BatchUpsertResult> {
let plugin_manager = window.state::<PluginManager>();
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let import_result = plugin_manager.import_data(window, file_contents).await?;
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
let resources = import_result.resources;
let workspaces: Vec<Workspace> = resources
.workspaces
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Workspace>(v.id.as_str(), &mut id_map);
v
})
.collect();
let environments: Vec<Environment> = resources
.environments
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Environment>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v
})
.collect();
let folders: Vec<Folder> = resources
.folders
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Folder>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let http_requests: Vec<HttpRequest> = resources
.http_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<HttpRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let grpc_requests: Vec<GrpcRequest> = resources
.grpc_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<GrpcRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let websocket_requests: Vec<WebsocketRequest> = resources
.websocket_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<WebsocketRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
info!("Importing data");
let upserted = window.with_tx(|tx| {
tx.batch_upsert(
workspaces,
environments,
folders,
http_requests,
grpc_requests,
websocket_requests,
&UpdateSource::Import,
)
})?;
Ok(upserted)
}

View File

@@ -3,14 +3,15 @@ use crate::encoding::read_response_body;
use crate::error::Error::GenericError;
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
use crate::http_request::send_http_request;
use crate::import::import_data;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_template};
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use crate::uri_scheme::handle_uri_scheme;
use crate::uri_scheme::handle_deep_link;
use error::Result as YaakResult;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
use std::fs::{File, create_dir_all};
use std::path::PathBuf;
use std::str::FromStr;
@@ -19,31 +20,29 @@ use std::{fs, panic};
use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow, is_dev};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind};
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::fs::read_to_string;
use tokio::sync::Mutex;
use tokio::task::block_in_place;
use yaak_common::window::WorkspaceWindowTrait;
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message};
use yaak_models::models::{
CookieJar, Environment, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, WebsocketRequest, Workspace,
WorkspaceMeta,
CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace, WorkspaceMeta,
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{
BatchUpsertResult, UpdateSource, get_workspace_export_resources, maybe_gen_id, maybe_gen_id_opt,
};
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive,
PluginWindowContext, RenderPurpose,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json;
@@ -55,6 +54,7 @@ mod error;
mod grpc;
mod history;
mod http_request;
mod import;
mod notifications;
mod plugin_events;
mod render;
@@ -778,98 +778,9 @@ async fn cmd_get_sse_events(file_path: &str) -> YaakResult<Vec<ServerSentEvent>>
#[tauri::command]
async fn cmd_import_data<R: Runtime>(
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
plugin_manager: State<'_, PluginManager>,
file_path: &str,
) -> YaakResult<BatchUpsertResult> {
let file = read_to_string(file_path)
.await
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let import_result = plugin_manager.import_data(&window, file_contents).await?;
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
let resources = import_result.resources;
let workspaces: Vec<Workspace> = resources
.workspaces
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Workspace>(v.id.as_str(), &mut id_map);
v
})
.collect();
let environments: Vec<Environment> = resources
.environments
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Environment>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v
})
.collect();
let folders: Vec<Folder> = resources
.folders
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Folder>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let http_requests: Vec<HttpRequest> = resources
.http_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<HttpRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let grpc_requests: Vec<GrpcRequest> = resources
.grpc_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<GrpcRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
let websocket_requests: Vec<WebsocketRequest> = resources
.websocket_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<WebsocketRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v
})
.collect();
info!("Importing data");
let upserted = app_handle.with_tx(|tx| {
tx.batch_upsert(
workspaces,
environments,
folders,
http_requests,
grpc_requests,
websocket_requests,
&UpdateSource::Import,
)
})?;
Ok(upserted)
import_data(&window, file_path).await
}
#[tauri::command]
@@ -1066,7 +977,7 @@ async fn cmd_install_plugin<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<Plugin> {
plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory, true).await?;
plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory).await?;
Ok(app_handle.db().upsert_plugin(
&Plugin {
@@ -1129,14 +1040,13 @@ async fn cmd_plugin_info<R: Runtime>(
id: &str,
app_handle: AppHandle<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<BootResponse> {
) -> YaakResult<PluginMetadata> {
let plugin = app_handle.db().get_plugin(id)?;
Ok(plugin_manager
.get_plugin_by_dir(plugin.directory.as_str())
.await
.ok_or(GenericError("Failed to find plugin for info".to_string()))?
.info()
.await)
.info())
}
#[tauri::command]
@@ -1255,6 +1165,7 @@ pub fn run() {
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
@@ -1265,12 +1176,28 @@ pub fn run() {
.plugin(yaak_models::init())
.plugin(yaak_plugins::init())
.plugin(yaak_crypto::init())
.plugin(yaak_fonts::init())
.plugin(yaak_git::init())
.plugin(yaak_ws::init())
.plugin(yaak_sync::init());
builder
.setup(|app| {
{
let app_handle = app.app_handle().clone();
app.deep_link().on_open_url(move |event| {
info!("Handling deep link open");
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
for url in event.urls() {
if let Err(e) = handle_deep_link(&app_handle, &url).await {
warn!("Failed to handle deep link {}: {e:?}", url.to_string());
};
}
});
});
};
let app_data_dir = app.path().app_data_dir().unwrap();
create_dir_all(app_data_dir.clone()).expect("Problem creating App directory!");
@@ -1331,7 +1258,6 @@ pub fn run() {
crate::commands::cmd_secure_template,
crate::commands::cmd_show_workspace_key,
])
.register_uri_scheme_protocol("yaak", handle_uri_scheme)
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| {
@@ -1376,7 +1302,7 @@ pub fn run() {
tokio::time::sleep(Duration::from_millis(4000)).await;
let val: State<'_, Mutex<YaakNotifier>> = w.state();
let mut n = val.lock().await;
if let Err(e) = n.check(&w).await {
if let Err(e) = n.maybe_check(&w).await {
warn!("Failed to check for notifications {}", e)
}
});

View File

@@ -1,13 +1,14 @@
use std::time::SystemTime;
use crate::error::Result;
use crate::history::{get_num_launches, get_os};
use chrono::{DateTime, Duration, Utc};
use crate::history::get_num_launches;
use chrono::{DateTime, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use yaak_common::api_client::yaak_api_client;
use yaak_common::platform::get_os;
use yaak_license::{LicenseCheckStatus, check_license};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
@@ -27,6 +28,7 @@ pub struct YaakNotifier {
#[serde(default, rename_all = "camelCase")]
pub struct YaakNotification {
timestamp: DateTime<Utc>,
timeout: Option<f64>,
id: String,
message: String,
action: Option<YaakNotificationAction>,
@@ -61,7 +63,7 @@ impl YaakNotifier {
Ok(())
}
pub async fn check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> {
pub async fn maybe_check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> {
let app_handle = window.app_handle();
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
@@ -80,7 +82,7 @@ impl YaakNotifier {
let settings = window.db().get_settings();
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = reqwest::Client::default()
let req = yaak_api_client(app_handle)?
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
("version", info.version.to_string().as_str()),
@@ -95,22 +97,9 @@ impl YaakNotifier {
return Ok(());
}
let result = resp.json::<Value>().await?;
// Support both single and multiple notifications.
// TODO: Remove support for single after April 2025
let notifications = match result {
Value::Array(a) => a
.into_iter()
.map(|a| serde_json::from_value(a).unwrap())
.collect::<Vec<YaakNotification>>(),
a @ _ => vec![serde_json::from_value(a).unwrap()],
};
for notification in notifications {
let age = notification.timestamp.signed_duration_since(Utc::now());
for notification in resp.json::<Vec<YaakNotification>>().await? {
let seen = get_kv(app_handle).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
if seen.contains(&notification.id) {
debug!("Already seen notification {}", notification.id);
continue;
}

View File

@@ -86,8 +86,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(),
&cb,
)
.await
.expect("Failed to render http request");
.await
.expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
@@ -115,7 +115,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
plugin_handle.name().await,
plugin_handle.info().name,
resp.error
),
color: Some(Color::Danger),
@@ -126,7 +126,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
InternalEventPayload::ReloadResponse(r) => {
let plugins = app_handle.db().list_plugins().unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
@@ -142,7 +142,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let toast_event = plugin_handle.build_event_to_send(
&window_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
message: format!("Reloaded plugin {}@{}", r.name, r.version),
icon: Some(Icon::Info),
..Default::default()
}),
@@ -188,7 +188,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
.await;
let http_response = match result {
Ok(r) => r,
@@ -257,17 +257,17 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
None
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let name = plugin_handle.info().name;
app_handle.db().set_plugin_key_value(&name, &req.key, &req.value);
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
}
InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let name = plugin_handle.info().name;
let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
}
InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let name = plugin_handle.info().name;
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}

View File

@@ -1,25 +1,66 @@
use crate::error::Result;
use crate::import::import_data;
use log::{info, warn};
use tauri::{Manager, Runtime, UriSchemeContext};
use std::collections::HashMap;
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use yaak_plugins::events::{Color, ShowToastRequest};
use yaak_plugins::install::download_and_install;
pub(crate) fn handle_uri_scheme<R: Runtime>(
a: UriSchemeContext<R>,
req: http::Request<Vec<u8>>,
) -> http::Response<Vec<u8>> {
println!("------------- Yaak URI scheme invoked!");
let uri = req.uri();
let window = a
.app_handle()
.get_webview_window(a.webview_label())
.expect("Failed to get webview window for URI scheme event");
info!("Yaak URI scheme invoked with {uri:?} {window:?}");
pub(crate) async fn handle_deep_link<R: Runtime>(
app_handle: &AppHandle<R>,
url: &Url,
) -> Result<()> {
let command = url.domain().unwrap_or_default();
info!("Yaak URI scheme invoked {}?{}", command, url.query().unwrap_or_default());
let path = uri.path();
if path == "/data/import" {
warn!("TODO: import data")
} else if path == "/plugins/install" {
warn!("TODO: install plugin")
let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
let windows = app_handle.webview_windows();
let (_, window) = windows.iter().next().unwrap();
match command {
"install-plugin" => {
let name = query_map.get("name").unwrap();
let version = query_map.get("version").cloned();
_ = window.set_focus();
let confirmed_install = app_handle
.dialog()
.message(format!("Install plugin {name} {version:?}?",))
.kind(MessageDialogKind::Info)
.buttons(MessageDialogButtons::OkCustom("Install".to_string()))
.blocking_show();
if !confirmed_install {
// Cancelled installation
return Ok(());
}
let pv = download_and_install(window, name, version).await?;
app_handle.emit(
"show_toast",
ShowToastRequest {
message: format!("Installed {name}@{}", pv.version),
color: Some(Color::Success),
icon: None,
},
)?;
}
"import-data" => {
let file_path = query_map.get("path").unwrap();
let results = import_data(window, file_path).await?;
_ = window.set_focus();
window.emit(
"show_toast",
ShowToastRequest {
message: format!("Imported data for {} workspaces", results.workspaces.len()),
color: Some(Color::Success),
icon: None,
},
)?;
}
_ => {
warn!("Unknown deep link command: {command}");
}
}
let msg = format!("No handler found for {path}");
tauri::http::Response::builder().status(404).body(msg.as_bytes().to_vec()).unwrap()
Ok(())
}

View File

@@ -23,7 +23,6 @@
},
"plugins": {
"deep-link": {
"mobile": [],
"desktop": {
"schemes": [
"yaak"
@@ -57,7 +56,6 @@
],
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [
"migrations",
"vendored/protoc/include",
"vendored/plugins",
"vendored/plugin-runtime"

View File

@@ -6,4 +6,7 @@ publish = false
[dependencies]
tauri = { workspace = true }
reqwest = { workspace = true, features = ["system-proxy", "gzip"] }
thiserror = { workspace = true }
regex = "1.11.0"
serde = { workspace = true, features = ["derive"] }

View File

@@ -0,0 +1,24 @@
use crate::error::Result;
use crate::platform::{get_ua_arch, get_ua_platform};
use reqwest::Client;
use std::time::Duration;
use tauri::http::{HeaderMap, HeaderValue};
use tauri::{AppHandle, Runtime};
pub fn yaak_api_client<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Client> {
let platform = get_ua_platform();
let version = app_handle.package_info().version.clone();
let arch = get_ua_arch();
let ua = format!("Yaak/{version} ({platform}; {arch})");
let mut default_headers = HeaderMap::new();
default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
let client = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(20))
.default_headers(default_headers)
.gzip(true)
.user_agent(ua)
.build()?;
Ok(client)
}

View File

@@ -0,0 +1,19 @@
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1 +1,4 @@
pub mod window;
pub mod window;
pub mod platform;
pub mod api_client;
pub mod error;

View File

@@ -0,0 +1,36 @@
pub fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
pub fn get_ua_platform() -> &'static str {
if cfg!(target_os = "windows") {
"Win"
} else if cfg!(target_os = "macos") {
"Mac"
} else if cfg!(target_os = "linux") {
"Linux"
} else {
"Unknown"
}
}
pub fn get_ua_arch() -> &'static str {
if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "x86") {
"i386"
} else if cfg!(target_arch = "arm") {
"ARM"
} else if cfg!(target_arch = "aarch64") {
"ARM64"
} else {
"Unknown"
}}

View File

@@ -13,7 +13,7 @@ keyring = { version = "4.0.0-rc.1" }
log = "0.4.26"
serde = { workspace = true, features = ["derive"] }
tauri = { workspace = true }
thiserror = "2.0.12"
thiserror = { workspace = true }
yaak-models = { workspace = true }
[build-dependencies]

View File

@@ -0,0 +1,16 @@
[package]
name = "yaak-fonts"
links = "yaak-fonts"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
font-loader = "0.11.0"
tauri = { workspace = true }
ts-rs = { workspace = true }
serde = "1.0"
thiserror = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Fonts = { editorFonts: Array<string>, uiFonts: Array<string>, };

View File

@@ -0,0 +1,5 @@
const COMMANDS: &[&str] = &["list"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Fonts } from './bindings/gen_fonts';
export async function listFonts() {
return invoke<Fonts>('plugin:yaak-fonts|list', {});
}
export function useFonts() {
return useQuery({
queryKey: ['list_fonts'],
queryFn: () => listFonts(),
});
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/fonts",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-list"]

View File

@@ -0,0 +1,41 @@
use crate::Result;
use font_loader::system_fonts;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tauri::command;
use ts_rs::TS;
#[derive(Default, Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_fonts.ts")]
pub struct Fonts {
pub editor_fonts: Vec<String>,
pub ui_fonts: Vec<String>,
}
#[command]
pub(crate) async fn list() -> Result<Fonts> {
let mut ui_fonts = HashSet::new();
let mut editor_fonts = HashSet::new();
let mut property = system_fonts::FontPropertyBuilder::new().monospace().build();
for font in &system_fonts::query_specific(&mut property) {
editor_fonts.insert(font.to_string());
}
for font in &system_fonts::query_all() {
if !editor_fonts.contains(font) {
ui_fonts.insert(font.to_string());
}
}
let mut ui_fonts: Vec<String> = ui_fonts.into_iter().collect();
let mut editor_fonts: Vec<String> = editor_fonts.into_iter().collect();
ui_fonts.sort();
editor_fonts.sort();
Ok(Fonts {
ui_fonts,
editor_fonts,
})
}

View File

@@ -0,0 +1,15 @@
use serde::{ser::Serializer, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,15 @@
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
Runtime,
};
mod commands;
mod error;
use crate::commands::list;
pub use error::{Error, Result};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-fonts").invoke_handler(generate_handler![list]).build()
}

View File

@@ -6,7 +6,7 @@ edition = "2024"
publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
chrono = { workspace = true, features = ["serde"] }
git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] }
log = "0.4.22"
serde = { workspace = true, features = ["derive"] }

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -15,7 +15,7 @@ pub enum Error {
#[error("Yaml error: {0}")]
YamlParseError(#[from] serde_yaml::Error),
#[error("Yaml error: {0}")]
#[error(transparent)]
ModelError(#[from] yaak_models::error::Error),
#[error("Sync error: {0}")]
@@ -24,10 +24,10 @@ pub enum Error {
#[error("I/o error: {0}")]
IoError(#[from] io::Error),
#[error("Yaml error: {0}")]
#[error("JSON error: {0}")]
JsonParseError(#[from] serde_json::Error),
#[error("Yaml error: {0}")]
#[error("UTF8 error: {0}")]
Utf8ConversionError(#[from] FromUtf8Error),
#[error("Git error: {0}")]

View File

@@ -8,9 +8,8 @@ publish = false
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
hyper = "1.5.2"
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy"] }
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
log = "0.4.20"
md5 = "0.7.0"
prost = "0.13.4"
@@ -25,4 +24,4 @@ tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"
uuid = { version = "1.7.0", features = ["v4"] }
yaak-http = { workspace = true }
yaak-http = { workspace = true }

View File

@@ -6,7 +6,7 @@ publish = false
[dependencies]
yaak-models = { workspace = true }
regex = "1.11.0"
regex = "1.11.1"
rustls = { workspace = true, default-features = false, features = ["ring"] }
rustls-platform-verifier = { workspace = true }
urlencoding = "2.1.3"

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls::crypto::ring;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_platform_verifier::BuilderVerifierExt;
use std::sync::Arc;
pub fn get_config(validate_certificates: bool) -> ClientConfig {
let arc_crypto_provider = Arc::new(ring::default_provider());
@@ -12,9 +12,7 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
.unwrap();
if validate_certificates {
// Use platform-native verifier to validate certificates
config_builder
.with_platform_verifier()
.with_no_client_auth()
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
} else {
config_builder
.dangerous()

View File

@@ -15,6 +15,7 @@ tauri = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
yaak-models = { workspace = true }
yaak-common = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -15,6 +15,9 @@ pub enum Error {
#[error(transparent)]
ModelError(#[from] yaak_models::error::Error),
#[error(transparent)]
CommonError(#[from] yaak_common::error::Error),
#[error("Internal server error")]
ServerError,
}

View File

@@ -16,15 +16,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.invoke_handler(generate_handler![check, activate, deactivate])
.build()
}
pub(crate) fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}

View File

@@ -7,6 +7,8 @@ use std::ops::Add;
use std::time::Duration;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
use ts_rs::TS;
use yaak_common::api_client::yaak_api_client;
use yaak_common::platform::get_os;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
@@ -68,7 +70,7 @@ pub async fn activate_license<R: Runtime>(
let client = reqwest::Client::new();
let payload = ActivateLicenseRequestPayload {
license_key: license_key.to_string(),
app_platform: crate::get_os().to_string(),
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
};
let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?;
@@ -107,7 +109,7 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
let client = reqwest::Client::new();
let path = format!("/licenses/activations/{}/deactivate", activation_id);
let payload = DeactivateLicenseRequestPayload {
app_platform: crate::get_os().to_string(),
app_platform: get_os().to_string(),
app_version: window.app_handle().package_info().version.to_string(),
};
let response = client.post(build_url(&path)).json(&payload).send().await?;
@@ -149,7 +151,7 @@ pub enum LicenseCheckStatus {
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
let payload = CheckActivationRequestPayload {
app_platform: crate::get_os().to_string(),
app_platform: get_os().to_string(),
app_version: window.package_info().version.to_string(),
};
let activation_id = get_activation_id(window.app_handle()).await;
@@ -169,7 +171,7 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
(true, _) => {
info!("Checking license activation");
// A license has been activated, so let's check the license server
let client = reqwest::Client::new();
let client = yaak_api_client(window.app_handle())?;
let path = format!("/licenses/activations/{activation_id}/check");
let response = client.post(build_url(&path)).json(&payload).send().await?;

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use hex_color::HexColor;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
@@ -65,12 +66,7 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor
}
}
fn position_traffic_lights(
ns_window_handle: UnsafeWindowHandle,
x: f64,
y: f64,
label: String,
) {
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
if !label.starts_with(MAIN_WINDOW_PREFIX) {
return;
}

View File

@@ -7,6 +7,8 @@ publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
hex = { workspace = true }
include_dir = "0.7"
log = "0.4.22"
nanoid = "0.4.0"
r2d2 = "0.8.10"
@@ -16,13 +18,12 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = "0.10.9"
tauri = { workspace = true}
sha2 = { workspace = true }
tauri = { workspace = true }
tauri-plugin-dialog = { workspace = true }
thiserror = "2.0.11"
thiserror = { workspace = true }
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
hex = "0.4.3"
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -58,11 +58,11 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, coloredMethods: boolean, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -1,9 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
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 { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

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