Compare commits

..

43 Commits

Author SHA1 Message Date
Gregory Schier
9fa0650647 Add scrollbar to sidebar
Fixes: https://feedback.yaak.app/p/missing-scrollbar-on-request-list
2025-04-22 07:48:34 -07:00
Gregory Schier
b8c42677ca Fix cmd+p filtering reference
https://feedback.yaak.app/p/search-doesnt-actually-search-through-all-the-apis
2025-04-22 07:46:10 -07:00
Gregory Schier
2eb3c2241c Fix duration tag
Closes: https://feedback.yaak.app/p/elapsed-time-not-stopping-on-failed-request
2025-04-22 07:29:17 -07:00
Gregory Schier
8fb7bbfe2e Don't prompt user for keychain password more than once 2025-04-22 07:23:05 -07:00
Gregory Schier
52eba74151 Handle no text 2025-04-22 07:01:48 -07:00
Gregory Schier
e651760713 Merge remote-tracking branch 'origin/master' 2025-04-22 06:59:11 -07:00
Gregory Schier
82451a26f6 Use mimeType for response viewer 2025-04-22 06:58:53 -07:00
jzhangdev
cc15f60fb6 Fix header layout (#182) 2025-04-22 06:51:39 -07:00
Gregory Schier
2f8b2a81c7 Fix jotai/index imports 2025-04-21 07:08:13 -07:00
Gregory Schier
6d4fdc91fe Fix text decoding when no content-type
Closes https://feedback.yaak.app/p/not-rendering-response
2025-04-21 06:54:03 -07:00
Gregory Schier
faca29c789 Fix key/value re-render issue 2025-04-20 07:08:46 -07:00
Gregory Schier
1ab937aae4 Fix infinite GraphQL render loop 2025-04-17 14:45:33 -07:00
Gregory Schier
45fcea1954 Real-time response time
Closes https://feedback.yaak.app/p/real-time-display-of-request-execution-timer
2025-04-17 14:16:10 -07:00
Gregory Schier
73554078d1 Add elapsed after headers 2025-04-17 07:01:31 -07:00
Gregory Schier
a42a88de7b Don't parse URI for HTTP requests anymore.
Fixes https://feedback.yaak.app/p/using-chinese-characters-in-request-parameters-can-result-in-errors
2025-04-17 06:48:39 -07:00
Gregory Schier
14a6079176 Fix URL grammar for path parameters 2025-04-17 06:30:48 -07:00
Gregory Schier
6c513616c0 Don't vendor keyring (libdbus) 2025-04-16 10:46:12 -07:00
Gregory Schier
cdf5f1b7a5 Fix vite and top-level-await build error 2025-04-15 07:57:32 -07:00
Gregory Schier
6566857d54 Adjust keychain config for dev 2025-04-15 07:28:01 -07:00
Gregory Schier
2e55a1bd6d [WIP] Encryption for secure values (#183) 2025-04-15 07:18:26 -07:00
Gregory Schier
e114a85c39 Render gRPC request for reflection.
Closes https://feedback.yaak.app/p/grpc-address-reflection-and-address-bar-issues
2025-03-31 12:26:07 -07:00
Gregory Schier
92be088e6c useClickOutside account for right click 2025-03-31 11:57:50 -07:00
Gregory Schier
f1757ae427 Generalized frontend model store (#193) 2025-03-31 11:56:17 -07:00
Gregory Schier
ce885c3551 port window ext from encryption PR 2025-03-26 11:46:55 -07:00
Gregory Schier
17657a4d04 plugin:yaak-models|upsert PoC 2025-03-26 09:54:42 -07:00
Gregory Schier
b7f62b78b1 Clean up DB refactor access (#192) 2025-03-26 07:54:58 -07:00
Gregory Schier
006284b99c Fix scrollbars 2025-03-25 09:38:15 -07:00
Gregory Schier
bac3968aac Revert scrollbar fix 2025-03-25 09:23:45 -07:00
Gregory Schier
e5fa044eda Merge remote-tracking branch 'origin/master' 2025-03-25 09:23:33 -07:00
Gregory Schier
5969120140 Fix folder upsert 2025-03-25 09:23:26 -07:00
dependabot[bot]
8801936ad2 Bump vite from 6.0.6 to 6.0.12 (#191)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 09:13:56 -07:00
Gregory Schier
1d37d46130 Database access refactor (#190) 2025-03-25 08:35:10 -07:00
Gregory Schier
445c30f3a9 Fix iframe scrollbar 2025-03-23 06:57:36 -07:00
Gregory Schier
5fedea38c2 Fix lint error 2025-03-21 07:42:05 -07:00
Gregory Schier
d86549f492 Always show GQL schema dropdown.
Fixes https://feedback.yaak.app/p/unable-to-disable-graphql-automatic-introspection
2025-03-21 07:28:08 -07:00
Gregory Schier
4c4eaba7d2 Queries now use AppHandle instead of Window (#189) 2025-03-20 09:43:14 -07:00
Gregory Schier
cf8f8743bb Remove non-existing import 2025-03-20 06:23:36 -07:00
Andy Bao
aa75636026 Fix labels in GRPC service/method selector dropdown (#188) 2025-03-20 06:07:31 -07:00
Gregory Schier
2c41b243b6 Some cleanup around window creation 2025-03-20 06:05:17 -07:00
Gregory Schier
6aea343d4f Shorten data directory description 2025-03-19 11:41:14 -07:00
Gregory Schier
c300e8cbd5 Fix dropdown refresh after Git init 2025-03-19 11:35:20 -07:00
dependabot[bot]
6e25c26e9f Bump zip from 2.1.6 to 2.4.1 in /src-tauri (#185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-19 09:09:02 -07:00
Gregory Schier
dce1455be7 Inline to_fixed_hash fn 2025-03-19 08:52:02 -07:00
397 changed files with 11031 additions and 36074 deletions

1558
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,10 @@
"packages/plugin-runtime",
"packages/plugin-runtime-types",
"packages/common-lib",
"src-tauri/yaak-license",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
"src-tauri/yaak-plugins",
"src-tauri/yaak-sse",
@@ -35,10 +37,13 @@
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"dependencies": {
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.7",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"@tauri-apps/cli": "^2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"eslint": "^8",
"eslint-config-prettier": "^8",
"eslint-plugin-import": "^2.31.0",
@@ -48,6 +53,6 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
"typescript": "^5.8.2"
}
}

View File

@@ -20,8 +20,6 @@
"@types/node": "^22.5.4"
},
"devDependencies": {
"cpy-cli": "^5.0.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.6.2"
"cpy-cli": "^5.0.0"
}
}

View File

@@ -344,7 +344,7 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
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": "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;
@@ -356,6 +356,8 @@ export type OpenWindowRequest = { url: string,
*/
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
@@ -393,14 +395,17 @@ export type TemplateFunction = { name: string, description?: string,
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
/**
* Similar to FormInput, but contains
*/
export type TemplateFunctionArg = FormInput;
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -24,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, name: string, description: string, 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,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };
export type JsonValue = number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null;

View File

@@ -15,7 +15,6 @@ export class PluginHandle {
bootRequest: this.bootRequest,
};
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
console.log('Created plugin worker for ', this.bootRequest.dir);
}
sendToWorker(event: InternalEvent) {

View File

@@ -1,3 +1,4 @@
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
import type {
BootRequest,
Context,
@@ -17,7 +18,6 @@ import type {
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
WindowContext,
} from '@yaakapp/api';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
@@ -45,12 +45,12 @@ export class PluginInstance {
this.#appToPluginEvents = new EventChannel();
// Forward incoming events to onMessage()
this.#appToPluginEvents.listen(async event => {
this.#appToPluginEvents.listen(async (event) => {
await this.#onMessage(event);
})
});
// Reload plugin if the JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const windowContextNone: PluginWindowContext = { type: 'none' };
const fileChangeCallback = async () => {
this.#importModule();
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
@@ -261,10 +261,10 @@ export class PluginInstance {
payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const action = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') {
applyFormInputDefaults(action.args, payload.args.values);
const result = await action.onRender(ctx, payload.args);
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') {
applyFormInputDefaults(fn.args, payload.args.values);
const result = await fn.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
@@ -317,7 +317,7 @@ export class PluginInstance {
}
#buildEventToSend(
windowContext: WindowContext,
windowContext: PluginWindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
@@ -332,7 +332,7 @@ export class PluginInstance {
}
#sendPayload(
windowContext: WindowContext,
windowContext: PluginWindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
@@ -348,12 +348,12 @@ export class PluginInstance {
this.#pluginToAppEvents.emit(event);
}
#sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
#sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string {
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: WindowContext,
windowContext: PluginWindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
@@ -379,7 +379,7 @@ export class PluginInstance {
}
#sendAndListenForEvents(
windowContext: WindowContext,
windowContext: PluginWindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
@@ -551,7 +551,7 @@ function genId(len = 5): string {
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: FormInput[],
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
@@ -580,18 +580,3 @@ function watchFile(filepath: string, cb: (filepath: string) => void) {
watchedFiles[filepath] = stat;
});
}
// function prefixStdout(s: string) {
// if (!s.includes('%s')) {
// throw new Error('Console prefix must contain a "%s" replacer');
// }
// interceptStdout((text: string) => {
// const lines = text.split(/\n/);
// let newText = '';
// for (let i = 0; i < lines.length; i++) {
// if (lines[i] == '') continue;
// newText += util.format(s, lines[i]) + '\n';
// }
// return newText.trimEnd();
// });
// }

View File

@@ -1,4 +1,4 @@
edition = "2018"
edition = "2024"
# Widths
chain_width = 100

View File

@@ -4,3 +4,8 @@ target/
vendored/*
!vendored/plugins
gen/*
**/permissions/autogenerated
**/permissions/schemas

1161
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
[workspace]
members = [
"yaak-crypto",
"yaak-git",
"yaak-grpc",
"yaak-http",
"yaak-license",
"yaak-mac-window",
"yaak-models",
"yaak-plugins",
"yaak-sse",
@@ -15,7 +17,7 @@ members = [
[package]
name = "yaak-app"
version = "0.0.0"
edition = "2021"
edition = "2024"
authors = ["Gregory Schier"]
publish = false
@@ -31,11 +33,7 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.0.6", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.26.0"
tauri-build = { version = "2.1.1", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -44,17 +42,15 @@ openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu inst
chrono = { version = "0.4.31", features = ["serde"] }
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0"
http = { version = "1.2.0", default-features = false }
log = "0.4.21"
log = "0.4.27"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
regex = "1.10.2"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest_cookie_store = "0.8.0"
rustls = { version = "0.23.22", default-features = false, features = ["custom-provider", "ring"] }
rustls-platform-verifier = "0.5.0"
rustls = { version = "0.23.25", default-features = false, features = ["custom-provider", "ring"] }
rustls-platform-verifier = "0.5.1"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
@@ -68,14 +64,17 @@ 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"
tokio = { version = "1.43.0", features = ["sync"] }
tokio-stream = "0.1.17"
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-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" }
yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" }
yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
@@ -84,17 +83,20 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
reqwest = "0.12.12"
serde = "1.0.215"
serde_json = "1.0.132"
tauri = "2.2.5"
tauri-plugin = "2.0.4"
tauri-plugin-shell = "2.2.0"
thiserror = "2.0.3"
ts-rs = "10.0.0"
reqwest = "0.12.15"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
thiserror = "2.0.12"
ts-rs = "10.1.0"
yaak-common = { path = "yaak-common" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-plugins = { path = "yaak-plugins" }
yaak-sync = { path = "yaak-sync" }
yaak-sse = { path = "yaak-sse" }
yaak-sync = { path = "yaak-sync" }
yaak-templates = { path = "yaak-templates" }
yaak-crypto = { path = "yaak-crypto" }

View File

@@ -50,8 +50,11 @@
"opener:allow-open-url",
"opener:allow-reveal-item-in-dir",
"shell:allow-open",
"yaak-license:default",
"yaak-crypto:default",
"yaak-git:default",
"yaak-license:default",
"yaak-mac-window:default",
"yaak-models:default",
"yaak-sync:default",
"yaak-ws:default"
]

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","opener:allow-default-urls","opener:allow-open-path","opener:allow-open-url","opener:allow-reveal-item-in-dir","shell:allow-open","yaak-license:default","yaak-git:default","yaak-sync:default","yaak-ws:default"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
-- 1. Create the new table with `id` as the primary key
CREATE TABLE key_values_new
(
id TEXT PRIMARY KEY,
model TEXT DEFAULT 'key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL
);
-- 2. Copy data from the old table
INSERT INTO key_values_new (id, model, created_at, updated_at, deleted_at, namespace, key, value)
SELECT (
-- This is the best way to generate a random string in SQLite, apparently
'kv_' || SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1)
) AS id,
model,
created_at,
updated_at,
deleted_at,
namespace,
key,
value
FROM key_values;
-- 3. Drop the old table
DROP TABLE key_values;
-- 4. Rename the new table
ALTER TABLE key_values_new
RENAME TO key_values;

View File

@@ -0,0 +1 @@
ALTER TABLE workspace_metas ADD COLUMN encryption_key TEXT NULL DEFAULT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE workspaces ADD COLUMN encryption_key_challenge TEXT NULL;

40
src-tauri/src/commands.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::error::Result;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use yaak_crypto::manager::EncryptionManagerExt;
use yaak_plugins::events::PluginWindowContext;
use yaak_plugins::native_template_functions::{decrypt_secure_template_function, encrypt_secure_template_function};
#[command]
pub(crate) async fn cmd_show_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
let key = window.crypto().reveal_workspace_key(workspace_id)?;
window
.dialog()
.message(format!("Your workspace key is \n\n{}", key))
.kind(MessageDialogKind::Info)
.show(|_v| {});
Ok(())
}
#[command]
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
window: WebviewWindow<R>,
template: &str,
) -> Result<String> {
let app_handle = window.app_handle();
let window_context = &PluginWindowContext::new(&window);
Ok(decrypt_secure_template_function(&app_handle, window_context, template)?)
}
#[command]
pub(crate) async fn cmd_secure_template<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
template: &str,
) -> Result<String> {
let window_context = &PluginWindowContext::new(&window);
Ok(encrypt_secure_template_function(&app_handle, window_context, template)?)
}

View File

@@ -1,29 +1,48 @@
use std::io;
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Render error: {0}")]
#[error(transparent)]
TemplateError(#[from] yaak_templates::error::Error),
#[error("Model error: {0}")]
#[error(transparent)]
ModelError(#[from] yaak_models::error::Error),
#[error("Sync error: {0}")]
#[error(transparent)]
SyncError(#[from] yaak_sync::error::Error),
#[error(transparent)]
CryptoError(#[from] yaak_crypto::error::Error),
#[error("Git error: {0}")]
#[error(transparent)]
GitError(#[from] yaak_git::error::Error),
#[error("Websocket error: {0}")]
#[error(transparent)]
WebsocketError(#[from] yaak_ws::error::Error),
#[error("License error: {0}")]
#[error(transparent)]
LicenseError(#[from] yaak_license::error::Error),
#[error("Plugin error: {0}")]
#[error(transparent)]
PluginError(#[from] yaak_plugins::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),
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),

View File

@@ -1,9 +1,6 @@
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{
get_key_value_int, get_key_value_string,
set_key_value_int, set_key_value_string, UpdateSource,
};
use tauri::{AppHandle, Runtime};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
@@ -16,33 +13,32 @@ pub struct LaunchEventInfo {
pub num_launches: i32,
}
pub async fn store_launch_history<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
info.num_launches = get_num_launches(app_handle).await + 1;
info.current_version = app_handle.package_info().version.to_string();
if info.previous_version.is_empty() {
} else {
info.launched_after_update = info.current_version != info.previous_version;
};
app_handle
.with_tx(|tx| {
info.previous_version =
tx.get_key_value_string(NAMESPACE, last_tracked_version_key, "");
// Update key values
if !info.previous_version.is_empty() {
info.launched_after_update = info.current_version != info.previous_version;
};
set_key_value_string(
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
&UpdateSource::Background,
)
.await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
.await;
// Update key values
let source = &UpdateSource::Background;
let version = info.current_version.as_str();
tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
info
}
@@ -59,6 +55,6 @@ pub fn get_os() -> &'static str {
}
}
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
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

@@ -3,14 +3,14 @@ use crate::error::Result;
use crate::render::render_http_request;
use crate::response_err;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{multipart, Proxy, Url};
use reqwest::{Method, Response};
use rustls::crypto::ring;
use reqwest::{Proxy, Url, multipart};
use rustls::ClientConfig;
use rustls::crypto::ring;
use rustls_platform_verifier::BuilderVerifierExt;
use serde_json::Value;
use std::collections::BTreeMap;
@@ -20,20 +20,18 @@ use std::sync::Arc;
use std::time::Duration;
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::fs;
use tokio::fs::{create_dir_all, File};
use tokio::fs::{File, create_dir_all};
use tokio::io::AsyncWriteExt;
use tokio::sync::watch::Receiver;
use tokio::sync::{oneshot, Mutex};
use tokio::sync::{Mutex, oneshot};
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
HttpResponseState, ProxySetting, ProxySettingAuth,
};
use yaak_models::queries::{
get_base_environment, get_http_response, get_or_create_settings, get_workspace,
update_response_if_id, upsert_cookie_jar, UpdateSource,
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
CallHttpAuthenticationRequest, HttpHeader, RenderPurpose, WindowContext,
CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -46,19 +44,27 @@ pub async fn send_http_request<R: Runtime>(
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse> {
let plugin_manager = window.state::<PluginManager>();
let workspace = get_workspace(window, &unrendered_request.workspace_id).await?;
let base_environment = get_base_environment(window, &unrendered_request.workspace_id).await?;
let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(window),
RenderPurpose::Send,
);
let app_handle = window.app_handle().clone();
let plugin_manager = app_handle.state::<PluginManager>();
let (settings, workspace) = {
let db = window.db();
let settings = db.get_settings();
let workspace = db.get_workspace(&unrendered_request.workspace_id)?;
(settings, workspace)
};
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let update_source = UpdateSource::from_window(window);
let request = match render_http_request(
&unrendered_request,
&base_environment,
@@ -68,7 +74,14 @@ pub async fn send_http_request<R: Runtime>(
.await
{
Ok(r) => r,
Err(e) => return Ok(response_err(&*response.lock().await, e.to_string(), window).await),
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
let mut url_string = request.url;
@@ -174,27 +187,15 @@ pub async fn send_http_request<R: Runtime>(
query_params.push((p.name, p.value));
}
let uri = match Uri::from_str(url_string.as_str()) {
let url = match Url::from_str(&url_string) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await);
}
};
// Yes, we're parsing both URI and URL because they could return different errors
let url = match Url::from_str(uri.to_string().as_str()) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await);
&update_source,
));
}
};
@@ -296,7 +297,12 @@ pub async fn send_http_request<R: Runtime>(
request_builder = request_builder.body(f);
}
Err(e) => {
return Ok(response_err(&*response.lock().await, e, window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e,
&update_source,
));
}
}
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
@@ -323,11 +329,11 @@ pub async fn send_http_request<R: Runtime>(
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
window,
)
.await);
&update_source,
));
}
}
};
@@ -340,11 +346,11 @@ pub async fn send_http_request<R: Runtime>(
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
window,
)
.await);
&update_source,
));
}
};
} else if !file_path.is_empty() {
@@ -356,16 +362,16 @@ pub async fn send_http_request<R: Runtime>(
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
window,
)
.await);
&update_source,
));
}
};
}
// Set file path if not empty
// Set file path if it is not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
@@ -397,7 +403,12 @@ pub async fn send_http_request<R: Runtime>(
Ok(r) => r,
Err(e) => {
warn!("Failed to build request builder {e:?}");
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
@@ -419,11 +430,16 @@ pub async fn send_http_request<R: Runtime>(
})
.collect(),
};
let auth_result = plugin_manager.call_http_authentication(window, &auth_name, req).await;
let auth_result = plugin_manager.call_http_authentication(&window, &auth_name, req).await;
let plugin_result = match auth_result {
Ok(r) => r,
Err(e) => {
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
@@ -448,22 +464,26 @@ pub async fn send_http_request<R: Runtime>(
let raw_response = tokio::select! {
Ok(r) = resp_rx => r,
_ = cancelled_rx.changed() => {
debug!("Request cancelled");
return Ok(response_err(&*response.lock().await, "Request was cancelled".to_string(), window).await);
let mut r = response.lock().await;
r.elapsed_headers = start.elapsed().as_millis() as i32;
r.elapsed = start.elapsed().as_millis() as i32;
return Ok(response_err(&app_handle, &r, "Request was cancelled".to_string(), &update_source));
}
};
{
let app_handle = app_handle.clone();
let window = window.clone();
let cancelled_rx = cancelled_rx.clone();
let response_id = response_id.clone();
let response = response.clone();
let update_source = update_source.clone();
tokio::spawn(async move {
match raw_response {
Ok(mut v) => {
let content_length = v.content_length();
let response_headers = v.headers().clone();
let dir = window.app_handle().path().app_data_dir().unwrap();
let dir = app_handle.path().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).await.expect("Failed to create responses dir");
let body_path = if response_id.is_empty() {
@@ -476,6 +496,7 @@ pub async fn send_http_request<R: Runtime>(
let mut r = response.lock().await;
r.body_path = Some(body_path.to_str().unwrap().to_string());
r.elapsed_headers = start.elapsed().as_millis() as i32;
r.elapsed = start.elapsed().as_millis() as i32;
r.status = v.status().as_u16() as i32;
r.status_reason = v.status().canonical_reason().map(|s| s.to_string());
r.headers = response_headers
@@ -497,8 +518,9 @@ pub async fn send_http_request<R: Runtime>(
};
r.state = HttpResponseState::Connected;
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &update_source)
.expect("Failed to update response after connected");
}
@@ -526,21 +548,27 @@ pub async fn send_http_request<R: Runtime>(
f.flush().await.expect("Failed to flush file");
written_bytes += bytes.len();
r.content_length = Some(written_bytes as i32);
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &update_source)
.expect("Failed to update response");
}
Ok(None) => {
break;
}
Err(e) => {
response_err(&*response.lock().await, e.to_string(), &window).await;
response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
);
break;
}
}
}
// Set final content length
// Set the final content length
{
let mut r = response.lock().await;
r.content_length = match content_length {
@@ -548,8 +576,9 @@ pub async fn send_http_request<R: Runtime>(
None => Some(written_bytes as i32),
};
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &UpdateSource::from_window(&window))
.expect("Failed to update response");
};
@@ -574,8 +603,9 @@ pub async fn send_http_request<R: Runtime>(
})
.collect::<Vec<_>>();
cookie_jar.cookies = json_cookies;
if let Err(e) =
upsert_cookie_jar(&window, &cookie_jar, &UpdateSource::Window).await
if let Err(e) = app_handle
.db()
.upsert_cookie_jar(&cookie_jar, &UpdateSource::from_window(&window))
{
error!("Failed to update cookie jar: {}", e);
};
@@ -583,7 +613,12 @@ pub async fn send_http_request<R: Runtime>(
}
Err(e) => {
warn!("Failed to execute request {e}");
response_err(&*response.lock().await, format!("{e}{e:?}"), &window).await;
response_err(
&app_handle,
&*response.lock().await,
format!("{e}{e:?}"),
&update_source,
);
}
};
@@ -592,16 +627,20 @@ pub async fn send_http_request<R: Runtime>(
});
};
let app_handle = app_handle.clone();
Ok(tokio::select! {
Ok(r) = done_rx => r,
_ = cancelled_rx.changed() => {
match get_http_response(window, response_id.as_str()).await {
match app_handle.with_db(|c| c.get_http_response(&response_id)) {
Ok(mut r) => {
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r, &UpdateSource::Window).await.expect("Failed to update response")
r.elapsed = start.elapsed().as_millis() as i32;
r.elapsed_headers = start.elapsed().as_millis() as i32;
app_handle.db().update_http_response_if_id(&r, &UpdateSource::from_window(window))
.expect("Failed to update response")
},
_ => {
response_err(&*response.lock().await, "Ephemeral request was cancelled".to_string(), &window).await
response_err(&app_handle, &*response.lock().await, "Ephemeral request was cancelled".to_string(), &update_source)
}.clone(),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
use std::time::SystemTime;
use crate::error::Result;
use crate::history::{get_num_launches, get_os};
use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
use yaak_models::queries::{get_key_value_raw, set_key_value_raw, UpdateSource};
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
// Check for updates every hour
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
@@ -43,16 +45,23 @@ impl YaakNotifier {
}
}
pub async fn seen<R: Runtime>(&mut self, w: &WebviewWindow<R>, id: &str) -> Result<(), String> {
let mut seen = get_kv(w).await?;
pub async fn seen<R: Runtime>(&mut self, window: &WebviewWindow<R>, id: &str) -> Result<()> {
let app_handle = window.app_handle();
let mut seen = get_kv(app_handle).await?;
seen.push(id.to_string());
debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str(), &UpdateSource::Window).await;
let seen_json = serde_json::to_string(&seen)?;
window.db().set_key_value_raw(
KV_NAMESPACE,
KV_KEY,
seen_json.as_str(),
&UpdateSource::from_window(window),
);
Ok(())
}
pub async fn check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<(), String> {
pub async fn 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;
if ignore_check {
@@ -61,22 +70,22 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let num_launches = get_num_launches(window).await;
let info = window.app_handle().package_info().clone();
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
("version", info.version.to_string().as_str()),
("launches", num_launches.to_string().as_str()),
("platform", get_os())
("platform", get_os()),
]);
let resp = req.send().await.map_err(|e| e.to_string())?;
let resp = req.send().await?;
if resp.status() != 200 {
debug!("Skipping notification status code {}", resp.status());
return Ok(());
}
let result = resp.json::<Value>().await.map_err(|e| e.to_string())?;
let result = resp.json::<Value>().await?;
// Support both single and multiple notifications.
// TODO: Remove support for single after April 2025
@@ -90,14 +99,14 @@ impl YaakNotifier {
for notification in notifications {
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(window).await?;
let seen = get_kv(app_handle).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
continue;
}
debug!("Got notification {:?}", notification);
let _ = window.emit_to(window.label(), "notification", notification.clone());
let _ = app_handle.emit_to(window.label(), "notification", notification.clone());
break; // Only show one notification
}
@@ -105,9 +114,9 @@ impl YaakNotifier {
}
}
async fn get_kv<R: Runtime>(w: &WebviewWindow<R>) -> Result<Vec<String>, String> {
match get_key_value_raw(w, "notifications", "seen").await {
async fn get_kv<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<String>> {
match app_handle.db().get_key_value_raw("notifications", "seen") {
None => Ok(Vec::new()),
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
Some(v) => Ok(serde_json::from_str(&v.value)?),
}
}

View File

@@ -1,6 +1,6 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::window::{create_window, CreateWindowConfig};
use crate::window::{CreateWindowConfig, create_window};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
@@ -10,16 +10,13 @@ use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::{
create_default_http_response, delete_plugin_key_value, get_base_environment, get_http_request,
get_plugin_key_value, list_http_responses_for_request, list_plugins, set_plugin_key_value,
upsert_plugin, UpdateSource,
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext, WindowNavigateEvent,
PluginWindowContext, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse,
ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
@@ -42,7 +39,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
PluginWindowContext::Label { label, .. } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
@@ -55,19 +52,16 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
call_frontend(window, event).await
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
let http_responses = app_handle
.db()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
let http_request = app_handle.db().get_http_request(&req.id).ok();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
@@ -76,12 +70,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window);
let base_environment = app_handle
.db()
.get_base_environment(&workspace.id)
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
@@ -100,12 +94,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window);
let base_environment = app_handle
.db()
.get_base_environment(&workspace.id)
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb)
@@ -114,10 +108,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&window_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
@@ -133,9 +125,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
let plugins = app_handle.db().list_plugins().unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
@@ -145,10 +135,10 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin).unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&window_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
@@ -163,32 +153,35 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request;
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window);
let environment = environment_from_window(&window);
if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id;
}
let resp = if http_request.id.is_empty() {
HttpResponse::new()
let http_response = if http_request.id.is_empty() {
HttpResponse::default()
} else {
create_default_http_response(
&window,
http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap()
window
.db()
.upsert_http_response(
&HttpResponse {
request_id: http_request.id.clone(),
workspace_id: http_request.workspace_id.clone(),
..Default::default()
},
&UpdateSource::Plugin,
)
.unwrap()
};
let result = send_http_request(
&window,
&http_request,
&resp,
&http_response,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
@@ -223,13 +216,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let label = label.clone();
let window_context = window_context.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = navigation_rx.recv().await {
let url = url.to_string();
let label = label.clone();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&window_context, // NOTE: Sending existing context on purpose here
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
@@ -241,12 +233,11 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let label = label.clone();
let window_context = window_context.clone();
tauri::async_runtime::spawn(async move {
while let Some(_) = close_rx.recv().await {
let label = label.clone();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&window_context,
&InternalEventPayload::WindowCloseEvent,
Some(event_id.clone()),
);
@@ -265,17 +256,17 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
set_plugin_key_value(app_handle, &name, &req.key, &req.value).await;
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 value = get_plugin_key_value(app_handle, &name, &req.key).await.map(|v| v.value);
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 deleted = delete_plugin_key_value(app_handle, &name, &req.key).await;
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
_ => None,

View File

@@ -1,12 +1,13 @@
use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use crate::error::Result;
use log::info;
use tauri::{AppHandle, Manager};
use tauri::{Manager, Runtime, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;
use tokio::task::block_in_place;
use yaak_models::queries::get_or_create_settings;
use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::manager::PluginManager;
use crate::is_dev;
@@ -59,30 +60,30 @@ impl YaakUpdater {
}
}
pub async fn check_now(
pub async fn check_now<R: Runtime>(
&mut self,
app_handle: &AppHandle,
window: &WebviewWindow<R>,
mode: UpdateMode,
update_trigger: UpdateTrigger,
) -> Result<bool, tauri_plugin_updater::Error> {
let settings = get_or_create_settings(app_handle).await;
) -> Result<bool> {
let settings = window.db().get_settings();
let update_key = format!("{:x}", md5::compute(settings.id));
self.last_update_check = SystemTime::now();
info!("Checking for updates mode={}", mode);
let h = app_handle.clone();
let update_check_result = app_handle
let w = window.clone();
let update_check_result = w
.updater_builder()
.on_before_exit(move || {
// Kill plugin manager before exit or NSIS installer will fail to replace sidecar
// while it's running.
// NOTE: This is only called on Windows
let h = h.clone();
let w = w.clone();
block_in_place(|| {
tauri::async_runtime::block_on(async move {
info!("Shutting down plugin manager before update");
let plugin_manager = h.state::<PluginManager>();
let plugin_manager = w.state::<PluginManager>();
plugin_manager.terminate().await;
});
});
@@ -100,11 +101,11 @@ impl YaakUpdater {
.check()
.await;
match update_check_result {
Ok(Some(update)) => {
let h = app_handle.clone();
app_handle
.dialog()
let result = match update_check_result? {
None => false,
Some(update) => {
let w = window.clone();
w.dialog()
.message(format!(
"{} is available. Would you like to download and install it now?",
update.version
@@ -121,7 +122,7 @@ impl YaakUpdater {
tauri::async_runtime::spawn(async move {
match update.download_and_install(|_, _| {}, || {}).await {
Ok(_) => {
if h.dialog()
if w.dialog()
.message("Would you like to restart the app?")
.title("Update Installed")
.buttons(MessageDialogButtons::OkCancelCustom(
@@ -130,27 +131,27 @@ impl YaakUpdater {
))
.blocking_show()
{
h.restart();
w.app_handle().restart();
}
}
Err(e) => {
h.dialog()
w.dialog()
.message(format!("The update failed to install: {}", e));
}
}
});
});
Ok(true)
true
}
Ok(None) => Ok(false),
Err(e) => Err(e),
}
};
Ok(result)
}
pub async fn maybe_check(
pub async fn maybe_check<R: Runtime>(
&mut self,
app_handle: &AppHandle,
window: &WebviewWindow<R>,
mode: UpdateMode,
) -> Result<bool, tauri_plugin_updater::Error> {
) -> Result<bool> {
let update_period_seconds = match mode {
UpdateMode::Stable => MAX_UPDATE_CHECK_HOURS_STABLE,
UpdateMode::Beta => MAX_UPDATE_CHECK_HOURS_BETA,
@@ -167,6 +168,6 @@ impl YaakUpdater {
return Ok(false);
}
self.check_now(app_handle, mode, UpdateTrigger::Background).await
self.check_now(window, mode, UpdateTrigger::Background).await
}
}

View File

@@ -1,6 +1,6 @@
use crate::window_menu::app_menu;
use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH};
use log::{info, warn};
use rand::random;
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent,
@@ -8,6 +8,15 @@ use tauri::{
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
const MIN_WINDOW_WIDTH: f64 = 300.0;
const MIN_WINDOW_HEIGHT: f64 = 300.0;
pub(crate) const MAIN_WINDOW_PREFIX: &str = "main_";
const OTHER_WINDOW_PREFIX: &str = "other_";
#[derive(Default, Debug)]
pub(crate) struct CreateWindowConfig<'s> {
pub url: &'s str,
@@ -55,7 +64,10 @@ pub(crate) fn create_window<R: Runtime>(
// macOS doesn't support data dir so must use this fn instead
#[cfg(target_os = "macos")]
{
win_builder = win_builder.data_store_identifier(to_fixed_hash(&key));
let hash = md5::compute(key.as_bytes());
let mut id = [0u8; 16];
id.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash
win_builder = win_builder.data_store_identifier(id);
}
}
@@ -159,9 +171,94 @@ pub(crate) fn create_window<R: Runtime>(
win
}
fn to_fixed_hash(s: &str) -> [u8; 16] {
let hash = md5::compute(s.as_bytes());
let mut fixed = [0u8; 16];
fixed.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash
fixed
pub(crate) fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
let mut counter = 0;
let label = loop {
let label = format!("{MAIN_WINDOW_PREFIX}{counter}");
match handle.webview_windows().get(label.as_str()) {
None => break Some(label),
Some(_) => counter += 1,
}
}
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
url,
label: label.as_str(),
title: "Yaak",
inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)),
position: Some((
// Offset by random amount so it's easier to differentiate
100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0,
)),
hide_titlebar: true,
..Default::default()
};
create_window(handle, config)
}
pub(crate) fn create_child_window(parent_window: &WebviewWindow, url: &str, label: &str, title: &str, inner_size: (f64, f64)) -> WebviewWindow {
let app_handle = parent_window.app_handle();
let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
let scale_factor = parent_window.scale_factor().unwrap();
let current_pos = parent_window.inner_position().unwrap().to_logical::<f64>(scale_factor);
let current_size = parent_window.inner_size().unwrap().to_logical::<f64>(scale_factor);
// Position the new window in the middle of the parent
let position = (
current_pos.x + current_size.width / 2.0 - inner_size.0 / 2.0,
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
);
let config = CreateWindowConfig {
label: label.as_str(),
title,
url,
inner_size: Some(inner_size),
position: Some(position),
hide_titlebar: true,
..Default::default()
};
let child_window = create_window(&app_handle, config);
// NOTE: These listeners will remain active even when the windows close. Unfortunately,
// there's no way to unlisten to events for now, so we just have to be defensive.
{
let parent_window = parent_window.clone();
let child_window = child_window.clone();
child_window.clone().on_window_event(move |e| match e {
// When the new window is destroyed, bring the other up behind it
WindowEvent::Destroyed => {
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
w.set_focus().unwrap();
}
}
_ => {}
});
}
{
let parent_window = parent_window.clone();
let child_window = child_window.clone();
parent_window.clone().on_window_event(move |e| match e {
// When the parent window is closed, close the child
WindowEvent::CloseRequested { .. } => child_window.destroy().unwrap(),
// When the parent window is focused, bring the child above
WindowEvent::Focused(focus) => {
if *focus {
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
w.set_focus().unwrap();
};
}
}
_ => {}
});
}
child_window
}

View File

@@ -1,54 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_node_fs = __toESM(require("node:fs"));
var plugin = {
templateFunctions: [{
name: "fs.readFile",
args: [{ title: "Select File", type: "file", name: "path", label: "File" }],
async onRender(_ctx, args) {
if (!args.values.path) return null;
try {
return import_node_fs.default.promises.readFile(args.values.path, "utf-8");
} catch (err) {
return null;
}
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -1,9 +0,0 @@
{
"name": "@yaakapp/template-function-file",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "yaak-common"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
tauri = { workspace = true }
regex = "1.11.0"

View File

@@ -0,0 +1 @@
pub mod window;

View File

@@ -0,0 +1,31 @@
use regex::Regex;
use tauri::{Runtime, WebviewWindow};
pub trait WorkspaceWindowTrait {
fn workspace_id(&self) -> Option<String>;
fn cookie_jar_id(&self) -> Option<String>;
fn environment_id(&self) -> Option<String>;
}
impl<R: Runtime> WorkspaceWindowTrait for WebviewWindow<R> {
fn workspace_id(&self) -> Option<String> {
let url = self.url().unwrap();
let re = Regex::new(r"/workspaces/(?<id>\w+)").unwrap();
match re.captures(url.as_str()) {
None => None,
Some(captures) => captures.name("id").map(|c| c.as_str().to_string()),
}
}
fn cookie_jar_id(&self) -> Option<String> {
let url = self.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "cookie_jar_id").map(|(_k, v)| v.to_string())
}
fn environment_id(&self) -> Option<String> {
let url = self.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "environment_id").map(|(_k, v)| v.to_string())
}
}

View File

@@ -0,0 +1,20 @@
[package]
name = "yaak-crypto"
links = "yaak-crypto"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
base32 = "0.5.1" # For encoding human-readable key
base64 = "0.22.1" # For encoding in the database
chacha20poly1305 = "0.10.1"
keyring = { version = "4.0.0-rc.1" }
log = "0.4.26"
serde = { workspace = true, features = ["derive"] }
tauri = { workspace = true }
thiserror = "2.0.12"
yaak-models = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

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

View File

@@ -0,0 +1,13 @@
import { invoke } from '@tauri-apps/api/core';
export function enableEncryption(workspaceId: string) {
return invoke<void>('plugin:yaak-crypto|enable_encryption', { workspaceId });
}
export function revealWorkspaceKey(workspaceId: string) {
return invoke<string>('plugin:yaak-crypto|reveal_workspace_key', { workspaceId });
}
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
return invoke<void>('plugin:yaak-crypto|set_workspace_key', args);
}

View File

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

View File

@@ -0,0 +1,7 @@
[default]
description = "Default permissions for the plugin"
permissions = [
"allow-enable-encryption",
"allow-reveal-workspace-key",
"allow-set-workspace-key",
]

View File

@@ -0,0 +1,31 @@
use crate::error::Result;
use crate::manager::EncryptionManagerExt;
use tauri::{command, Runtime, WebviewWindow};
#[command]
pub(crate) async fn enable_encryption<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
window.crypto().ensure_workspace_key(workspace_id)?;
window.crypto().reveal_workspace_key(workspace_id)?;
Ok(())
}
#[command]
pub(crate) async fn reveal_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
) -> Result<String> {
Ok(window.crypto().reveal_workspace_key(workspace_id)?)
}
#[command]
pub(crate) async fn set_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
key: &str,
) -> Result<()> {
window.crypto().set_human_key(workspace_id, key)?;
Ok(())
}

View File

@@ -0,0 +1,98 @@
use crate::error::Error::{DecryptionError, EncryptionError, InvalidEncryptedData};
use crate::error::Result;
use chacha20poly1305::aead::generic_array::typenum::Unsigned;
use chacha20poly1305::aead::{Aead, AeadCore, Key, KeyInit, OsRng};
use chacha20poly1305::XChaCha20Poly1305;
const ENCRYPTION_TAG: &str = "yA4k3nC";
const ENCRYPTION_VERSION: u8 = 1;
pub(crate) fn encrypt_data(data: &[u8], key: &Key<XChaCha20Poly1305>) -> Result<Vec<u8>> {
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let ciphered_data = cipher.encrypt(&nonce, data).map_err(|_| EncryptionError)?;
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(ENCRYPTION_TAG.as_bytes()); // Tag
data.push(ENCRYPTION_VERSION); // Version
data.extend_from_slice(&nonce.as_slice()); // Nonce
data.extend_from_slice(&ciphered_data); // Ciphertext
Ok(data)
}
pub(crate) fn decrypt_data(cipher_data: &[u8], key: &Key<XChaCha20Poly1305>) -> Result<Vec<u8>> {
// Yaak Tag + ID + Version + Nonce + ... ciphertext ...
let (tag, rest) =
cipher_data.split_at_checked(ENCRYPTION_TAG.len()).ok_or(InvalidEncryptedData)?;
if tag != ENCRYPTION_TAG.as_bytes() {
return Err(InvalidEncryptedData);
}
let (version, rest) = rest.split_at_checked(1).ok_or(InvalidEncryptedData)?;
if version[0] != ENCRYPTION_VERSION {
return Err(InvalidEncryptedData);
}
let nonce_bytes = <XChaCha20Poly1305 as AeadCore>::NonceSize::to_usize();
let (nonce, ciphered_data) = rest.split_at_checked(nonce_bytes).ok_or(InvalidEncryptedData)?;
let cipher = XChaCha20Poly1305::new(&key);
cipher.decrypt(nonce.into(), ciphered_data).map_err(|_| DecryptionError)
}
#[cfg(test)]
mod test {
use crate::encryption::{decrypt_data, encrypt_data};
use crate::error::Error::InvalidEncryptedData;
use crate::error::Result;
use chacha20poly1305::aead::OsRng;
use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
#[test]
fn test_encrypt_decrypt() -> Result<()> {
let key = XChaCha20Poly1305::generate_key(OsRng);
let encrypted = encrypt_data("hello world".as_bytes(), &key)?;
let decrypted = decrypt_data(encrypted.as_slice(), &key)?;
assert_eq!(String::from_utf8(decrypted).unwrap(), "hello world");
Ok(())
}
#[test]
fn test_decrypt_empty() -> Result<()> {
let key = XChaCha20Poly1305::generate_key(OsRng);
let encrypted = encrypt_data(&[], &key)?;
assert_eq!(encrypted.len(), 48);
let decrypted = decrypt_data(encrypted.as_slice(), &key)?;
assert_eq!(String::from_utf8(decrypted).unwrap(), "");
Ok(())
}
#[test]
fn test_decrypt_bad_version() -> Result<()> {
let key = XChaCha20Poly1305::generate_key(OsRng);
let mut encrypted = encrypt_data("hello world".as_bytes(), &key)?;
encrypted[7] = 0;
let decrypted = decrypt_data(encrypted.as_slice(), &key);
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
Ok(())
}
#[test]
fn test_decrypt_bad_tag() -> Result<()> {
let key = XChaCha20Poly1305::generate_key(OsRng);
let mut encrypted = encrypt_data("hello world".as_bytes(), &key)?;
encrypted[0] = 2;
let decrypted = decrypt_data(encrypted.as_slice(), &key);
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
Ok(())
}
#[test]
fn test_decrypt_unencrypted_data() -> Result<()> {
let key = XChaCha20Poly1305::generate_key(OsRng);
let decrypted = decrypt_data("123".as_bytes(), &key);
assert!(matches!(decrypted, Err(InvalidEncryptedData)));
Ok(())
}
}

View File

@@ -0,0 +1,47 @@
use serde::{Serialize, Serializer};
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
DbError(#[from] yaak_models::error::Error),
#[error("Keyring error: {0}")]
KeyringError(#[from] keyring::Error),
#[error("Missing workspace encryption key")]
MissingWorkspaceKey,
#[error("Incorrect workspace key")]
IncorrectWorkspaceKey,
#[error("Crypto IO error: {0}")]
IoError(#[from] io::Error),
#[error("Failed to encrypt")]
EncryptionError,
#[error("Failed to decrypt")]
DecryptionError,
#[error("Invalid encrypted data")]
InvalidEncryptedData,
#[error("Invalid key provided")]
InvalidHumanKey,
#[error("Encryption error: {0}")]
GenericError(String),
}
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,27 @@
extern crate core;
use crate::commands::*;
use crate::manager::EncryptionManager;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{generate_handler, Manager, Runtime};
mod commands;
pub mod encryption;
pub mod error;
pub mod manager;
mod master_key;
mod workspace_key;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-crypto")
.invoke_handler(generate_handler![
enable_encryption,
reveal_workspace_key,
set_workspace_key
])
.setup(|app, _api| {
app.manage(EncryptionManager::new(app.app_handle()));
Ok(())
})
.build()
}

View File

@@ -0,0 +1,172 @@
use crate::error::Error::{GenericError, IncorrectWorkspaceKey, MissingWorkspaceKey};
use crate::error::{Error, Result};
use crate::master_key::MasterKey;
use crate::workspace_key::WorkspaceKey;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use log::{info, warn};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager, Runtime, State};
use yaak_models::models::{EncryptedKey, Workspace, WorkspaceMeta};
use yaak_models::query_manager::{QueryManager, QueryManagerExt};
use yaak_models::util::{generate_id_of_length, UpdateSource};
const KEY_USER: &str = "encryption-key";
pub trait EncryptionManagerExt<'a, R> {
fn crypto(&'a self) -> State<'a, EncryptionManager>;
}
impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
fn crypto(&'a self) -> State<'a, EncryptionManager> {
self.state::<EncryptionManager>()
}
}
#[derive(Debug, Clone)]
pub struct EncryptionManager {
cached_master_key: Arc<Mutex<Option<MasterKey>>>,
cached_workspace_keys: Arc<Mutex<HashMap<String, WorkspaceKey>>>,
query_manager: QueryManager,
app_id: String,
}
impl EncryptionManager {
pub fn new<R: Runtime>(app_handle: &AppHandle<R>) -> Self {
Self {
cached_master_key: Default::default(),
cached_workspace_keys: Default::default(),
query_manager: app_handle.db_manager().inner().clone(),
app_id: app_handle.config().identifier.to_string(),
}
}
pub fn encrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
let workspace_secret = self.get_workspace_key(workspace_id)?;
workspace_secret.encrypt(data)
}
pub fn decrypt(&self, workspace_id: &str, data: &[u8]) -> Result<Vec<u8>> {
let workspace_secret = self.get_workspace_key(workspace_id)?;
workspace_secret.decrypt(data)
}
pub fn reveal_workspace_key(&self, workspace_id: &str) -> Result<String> {
let key = self.get_workspace_key(workspace_id)?;
key.to_human()
}
pub fn set_human_key(&self, workspace_id: &str, human_key: &str) -> Result<WorkspaceMeta> {
let wkey = WorkspaceKey::from_human(human_key)?;
let workspace = self.query_manager.connect().get_workspace(workspace_id)?;
let encryption_key_challenge = match workspace.encryption_key_challenge {
None => return self.set_workspace_key(workspace_id, &wkey),
Some(c) => c,
};
let encryption_key_challenge = match BASE64_STANDARD.decode(encryption_key_challenge) {
Ok(c) => c,
Err(_) => return Err(GenericError("Failed to decode workspace challenge".to_string())),
};
if let Err(_) = wkey.decrypt(encryption_key_challenge.as_slice()) {
return Err(IncorrectWorkspaceKey);
};
self.set_workspace_key(workspace_id, &wkey)
}
pub(crate) fn set_workspace_key(
&self,
workspace_id: &str,
wkey: &WorkspaceKey,
) -> Result<WorkspaceMeta> {
info!("Created workspace key for {workspace_id}");
let encrypted_key = BASE64_STANDARD.encode(self.get_master_key()?.encrypt(wkey.raw_key())?);
let encrypted_key = EncryptedKey { encrypted_key };
let encryption_key_challenge = wkey.encrypt(generate_id_of_length(50).as_bytes())?;
let encryption_key_challenge = Some(BASE64_STANDARD.encode(encryption_key_challenge));
let workspace_meta = self.query_manager.with_tx::<WorkspaceMeta, Error>(|tx| {
let workspace = tx.get_workspace(workspace_id)?;
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;
tx.upsert_workspace(
&Workspace {
encryption_key_challenge,
..workspace
},
&UpdateSource::Background,
)?;
Ok(tx.upsert_workspace_meta(
&WorkspaceMeta {
encryption_key: Some(encrypted_key.clone()),
..workspace_meta
},
&UpdateSource::Background,
)?)
})?;
let mut cache = self.cached_workspace_keys.lock().unwrap();
cache.insert(workspace_id.to_string(), wkey.clone());
Ok(workspace_meta)
}
pub(crate) fn ensure_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceMeta> {
let workspace_meta =
self.query_manager.connect().get_or_create_workspace_meta(workspace_id)?;
// Already exists
if let Some(_) = workspace_meta.encryption_key {
warn!("Tried to create workspace key when one already exists for {workspace_id}");
return Ok(workspace_meta);
}
let wkey = WorkspaceKey::create()?;
self.set_workspace_key(workspace_id, &wkey)
}
fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
{
let cache = self.cached_workspace_keys.lock().unwrap();
if let Some(k) = cache.get(workspace_id) {
return Ok(k.clone());
}
};
let db = self.query_manager.connect();
let workspace_meta = db.get_or_create_workspace_meta(workspace_id)?;
let key = match workspace_meta.encryption_key {
None => return Err(MissingWorkspaceKey),
Some(k) => k,
};
let mkey = self.get_master_key()?;
let decoded_key = BASE64_STANDARD
.decode(key.encrypted_key)
.map_err(|e| GenericError(format!("Failed to decode workspace key {e:?}")))?;
let raw_key = mkey.decrypt(decoded_key.as_slice())?;
info!("Got existing workspace key for {workspace_id}");
let wkey = WorkspaceKey::from_raw_key(raw_key.as_slice());
Ok(wkey)
}
fn get_master_key(&self) -> Result<MasterKey> {
// NOTE: This locks the key for the entire function which seems wrong, but this prevents
// concurrent access from prompting the user for a keychain password multiple times.
let mut master_secret = self.cached_master_key.lock().unwrap();
if let Some(k) = master_secret.as_ref() {
return Ok(k.to_owned());
}
let mkey = MasterKey::get_or_create(&self.app_id, KEY_USER)?;
*master_secret = Some(mkey.clone());
Ok(mkey)
}
}

View File

@@ -0,0 +1,79 @@
use crate::encryption::{decrypt_data, encrypt_data};
use crate::error::Error::GenericError;
use crate::error::Result;
use base32::Alphabet;
use chacha20poly1305::aead::{Key, KeyInit, OsRng};
use chacha20poly1305::XChaCha20Poly1305;
use keyring::{Entry, Error};
use log::info;
const HUMAN_PREFIX: &str = "YKM_";
#[derive(Debug, Clone)]
pub(crate) struct MasterKey {
key: Key<XChaCha20Poly1305>,
}
impl MasterKey {
pub(crate) fn get_or_create(app_id: &str, user: &str) -> Result<Self> {
let id = format!("{app_id}.EncryptionKey");
let entry = Entry::new(&id, user)?;
let key = match entry.get_password() {
Ok(encoded) => {
let without_prefix = encoded.strip_prefix(HUMAN_PREFIX).unwrap_or(&encoded);
let key_bytes = base32::decode(Alphabet::Crockford {}, &without_prefix)
.ok_or(GenericError("Failed to decode master key".to_string()))?;
Key::<XChaCha20Poly1305>::clone_from_slice(key_bytes.as_slice())
}
Err(Error::NoEntry) => {
info!("Creating new master key");
let key = XChaCha20Poly1305::generate_key(OsRng);
let encoded = base32::encode(Alphabet::Crockford {}, key.as_slice());
let with_prefix = format!("{HUMAN_PREFIX}{encoded}");
entry.set_password(&with_prefix)?;
key
}
Err(e) => return Err(GenericError(e.to_string())),
};
Ok(Self { key })
}
pub(crate) fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
encrypt_data(data, &self.key)
}
pub(crate) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
decrypt_data(data, &self.key)
}
#[cfg(test)]
pub(crate) fn test_key() -> Self {
let key: Key<XChaCha20Poly1305> = Key::<XChaCha20Poly1305>::clone_from_slice(
"00000000000000000000000000000000".as_bytes(),
);
Self { key }
}
}
#[cfg(test)]
mod tests {
use crate::error::Result;
use crate::master_key::MasterKey;
#[test]
fn test_master_key() -> Result<()> {
// Test out the master key
let mkey = MasterKey::test_key();
let encrypted = mkey.encrypt("hello".as_bytes())?;
let decrypted = mkey.decrypt(encrypted.as_slice()).unwrap();
assert_eq!(decrypted, "hello".as_bytes().to_vec());
let mkey = MasterKey::test_key();
let decrypted = mkey.decrypt(encrypted.as_slice()).unwrap();
assert_eq!(decrypted, "hello".as_bytes().to_vec());
Ok(())
}
}

View File

@@ -0,0 +1,116 @@
use crate::encryption::{decrypt_data, encrypt_data};
use crate::error::Error::InvalidHumanKey;
use crate::error::Result;
use base32::Alphabet;
use chacha20poly1305::aead::{Key, KeyInit, OsRng};
use chacha20poly1305::{KeySizeUser, XChaCha20Poly1305};
#[derive(Debug, Clone)]
pub struct WorkspaceKey {
key: Key<XChaCha20Poly1305>,
}
const HUMAN_PREFIX: &str = "YK";
impl WorkspaceKey {
pub(crate) fn to_human(&self) -> Result<String> {
let encoded = base32::encode(Alphabet::Crockford {}, self.key.as_slice());
let with_prefix = format!("{HUMAN_PREFIX}{encoded}");
let with_separators = with_prefix
.chars()
.collect::<Vec<_>>()
.chunks(6)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("-");
Ok(with_separators)
}
#[allow(dead_code)]
pub(crate) fn from_human(human_key: &str) -> Result<Self> {
let without_prefix = human_key.strip_prefix(HUMAN_PREFIX).unwrap_or(human_key);
let without_separators = without_prefix.replace("-", "");
let key =
base32::decode(Alphabet::Crockford {}, &without_separators).ok_or(InvalidHumanKey)?;
if key.len() != XChaCha20Poly1305::key_size() {
return Err(InvalidHumanKey);
}
Ok(Self::from_raw_key(key.as_slice()))
}
pub(crate) fn from_raw_key(key: &[u8]) -> Self {
Self {
key: Key::<XChaCha20Poly1305>::clone_from_slice(key),
}
}
pub(crate) fn raw_key(&self) -> &[u8] {
self.key.as_slice()
}
pub(crate) fn create() -> Result<Self> {
let key = XChaCha20Poly1305::generate_key(OsRng);
Ok(Self::from_raw_key(key.as_slice()))
}
pub(crate) fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
encrypt_data(data, &self.key)
}
pub(crate) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
decrypt_data(data, &self.key)
}
#[cfg(test)]
pub(crate) fn test_key() -> Self {
Self::from_raw_key("f1a2d4b3c8e799af1456be3478a4c3f2".as_bytes())
}
}
#[cfg(test)]
mod tests {
use crate::error::Error::InvalidHumanKey;
use crate::error::Result;
use crate::workspace_key::WorkspaceKey;
#[test]
fn test_persisted_key() -> Result<()> {
let key = WorkspaceKey::test_key();
let encrypted = key.encrypt("hello".as_bytes())?;
assert_eq!(key.decrypt(encrypted.as_slice())?, "hello".as_bytes());
Ok(())
}
#[test]
fn test_human_format() -> Result<()> {
let key = WorkspaceKey::test_key();
let encrypted = key.encrypt("hello".as_bytes())?;
assert_eq!(key.decrypt(encrypted.as_slice())?, "hello".as_bytes());
let human = key.to_human()?;
assert_eq!(human, "YKCRRP-2CK46H-H36RSR-CMVKJE-B1CRRK-8D9PC9-JK6D1Q-71GK8R-SKCRS0");
assert_eq!(
WorkspaceKey::from_human(&human)?.decrypt(encrypted.as_slice())?,
"hello".as_bytes()
);
Ok(())
}
#[test]
fn test_from_human_invalid() -> Result<()> {
assert!(matches!(
WorkspaceKey::from_human(
"YKCRRP-2CK46H-H36RSR-CMVKJE-B1CRRK-8D9PC9-JK6D1Q-71GK8R-SKCRS0-H3X38D",
),
Err(InvalidHumanKey)
));
assert!(matches!(WorkspaceKey::from_human("bad-key",), Err(InvalidHumanKey)));
assert!(matches!(WorkspaceKey::from_human("",), Err(InvalidHumanKey)));
Ok(())
}
}

View File

@@ -2,15 +2,15 @@
name = "yaak-git"
links = "yaak-git"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
git2 = { version = "0.20.0" , features = ["vendored-libgit2", "vendored-openssl"]}
git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] }
log = "0.4.22"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9.34"
tauri = { workspace = true }
thiserror = { workspace = true }
@@ -19,4 +19,4 @@ yaak-models = { workspace = true }
yaak-sync = { workspace = true }
[build-dependencies]
tauri-plugin = { version = "2.0.3", features = ["build"] }
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -20,4 +20,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
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, name: string, description: string, 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

@@ -52,7 +52,7 @@ export function useGit(dir: string) {
onSuccess,
}),
commitAndPush: useMutation<PushResult, string, { message: string }>({
mutationKey: ['git', 'commitpush', dir],
mutationKey: ['git', 'commit_push', dir],
mutationFn: async (args) => {
await invoke('plugin:yaak-git|commit', { dir, ...args });
return invoke('plugin:yaak-git|push', { dir });
@@ -79,10 +79,18 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|unstage', { dir, ...args }),
onSuccess,
}),
init: useGitInit(),
},
] as const;
}
export async function gitInit(dir: string) {
await invoke('plugin:yaak-git|initialize', { dir });
export function useGitInit() {
const queryClient = useQueryClient();
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
return useMutation<void, string, { dir: string }>({
mutationKey: ['git', 'init'],
mutationFn: (args) => invoke('plugin:yaak-git|initialize', { ...args }),
onSuccess,
});
}

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-add"
description = "Enables the add command without any pre-configured scope."
commands.allow = ["add"]
[[permission]]
identifier = "deny-add"
description = "Denies the add command without any pre-configured scope."
commands.deny = ["add"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-branch"
description = "Enables the branch command without any pre-configured scope."
commands.allow = ["branch"]
[[permission]]
identifier = "deny-branch"
description = "Denies the branch command without any pre-configured scope."
commands.deny = ["branch"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-checkout"
description = "Enables the checkout command without any pre-configured scope."
commands.allow = ["checkout"]
[[permission]]
identifier = "deny-checkout"
description = "Denies the checkout command without any pre-configured scope."
commands.deny = ["checkout"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-checkout-remote"
description = "Enables the checkout_remote command without any pre-configured scope."
commands.allow = ["checkout_remote"]
[[permission]]
identifier = "deny-checkout-remote"
description = "Denies the checkout_remote command without any pre-configured scope."
commands.deny = ["checkout_remote"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-commit"
description = "Enables the commit command without any pre-configured scope."
commands.allow = ["commit"]
[[permission]]
identifier = "deny-commit"
description = "Denies the commit command without any pre-configured scope."
commands.deny = ["commit"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-delete-branch"
description = "Enables the delete_branch command without any pre-configured scope."
commands.allow = ["delete_branch"]
[[permission]]
identifier = "deny-delete-branch"
description = "Denies the delete_branch command without any pre-configured scope."
commands.deny = ["delete_branch"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-fetch-all"
description = "Enables the fetch_all command without any pre-configured scope."
commands.allow = ["fetch_all"]
[[permission]]
identifier = "deny-fetch-all"
description = "Denies the fetch_all command without any pre-configured scope."
commands.deny = ["fetch_all"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-initialize"
description = "Enables the initialize command without any pre-configured scope."
commands.allow = ["initialize"]
[[permission]]
identifier = "deny-initialize"
description = "Denies the initialize command without any pre-configured scope."
commands.deny = ["initialize"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-log"
description = "Enables the log command without any pre-configured scope."
commands.allow = ["log"]
[[permission]]
identifier = "deny-log"
description = "Denies the log command without any pre-configured scope."
commands.deny = ["log"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-merge-branch"
description = "Enables the merge_branch command without any pre-configured scope."
commands.allow = ["merge_branch"]
[[permission]]
identifier = "deny-merge-branch"
description = "Denies the merge_branch command without any pre-configured scope."
commands.deny = ["merge_branch"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-pull"
description = "Enables the pull command without any pre-configured scope."
commands.allow = ["pull"]
[[permission]]
identifier = "deny-pull"
description = "Denies the pull command without any pre-configured scope."
commands.deny = ["pull"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-push"
description = "Enables the push command without any pre-configured scope."
commands.allow = ["push"]
[[permission]]
identifier = "deny-push"
description = "Denies the push command without any pre-configured scope."
commands.deny = ["push"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-status"
description = "Enables the status command without any pre-configured scope."
commands.allow = ["status"]
[[permission]]
identifier = "deny-status"
description = "Denies the status command without any pre-configured scope."
commands.deny = ["status"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-unstage"
description = "Enables the unstage command without any pre-configured scope."
commands.allow = ["unstage"]
[[permission]]
identifier = "deny-unstage"
description = "Denies the unstage command without any pre-configured scope."
commands.deny = ["unstage"]

View File

@@ -1,391 +0,0 @@
## Default Permission
Default permissions for the plugin
- `allow-add`
- `allow-branch`
- `allow-checkout`
- `allow-commit`
- `allow-delete-branch`
- `allow-fetch-all`
- `allow-initialize`
- `allow-log`
- `allow-merge-branch`
- `allow-pull`
- `allow-push`
- `allow-status`
- `allow-unstage`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`yaak-git:allow-add`
</td>
<td>
Enables the add command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-add`
</td>
<td>
Denies the add command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-branch`
</td>
<td>
Enables the branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-branch`
</td>
<td>
Denies the branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-checkout`
</td>
<td>
Enables the checkout command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-checkout`
</td>
<td>
Denies the checkout command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-checkout-remote`
</td>
<td>
Enables the checkout_remote command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-checkout-remote`
</td>
<td>
Denies the checkout_remote command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-commit`
</td>
<td>
Enables the commit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-commit`
</td>
<td>
Denies the commit command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-delete-branch`
</td>
<td>
Enables the delete_branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-delete-branch`
</td>
<td>
Denies the delete_branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-fetch-all`
</td>
<td>
Enables the fetch_all command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-fetch-all`
</td>
<td>
Denies the fetch_all command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-initialize`
</td>
<td>
Enables the initialize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-initialize`
</td>
<td>
Denies the initialize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-log`
</td>
<td>
Enables the log command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-log`
</td>
<td>
Denies the log command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-merge-branch`
</td>
<td>
Enables the merge_branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-merge-branch`
</td>
<td>
Denies the merge_branch command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-pull`
</td>
<td>
Enables the pull command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-pull`
</td>
<td>
Denies the pull command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-push`
</td>
<td>
Enables the push command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-push`
</td>
<td>
Denies the push command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-status`
</td>
<td>
Enables the status command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-status`
</td>
<td>
Denies the status command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-unstage`
</td>
<td>
Enables the unstage command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-unstage`
</td>
<td>
Denies the unstage command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -1,445 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PermissionFile",
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
"type": "object",
"properties": {
"default": {
"description": "The default permission set for the plugin",
"anyOf": [
{
"$ref": "#/definitions/DefaultPermission"
},
{
"type": "null"
}
]
},
"set": {
"description": "A list of permissions sets defined",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionSet"
}
},
"permission": {
"description": "A list of inlined permissions",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Permission"
}
}
},
"definitions": {
"DefaultPermission": {
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
"type": "object",
"required": [
"permissions"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"description": {
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"PermissionSet": {
"description": "A set of direct permissions grouped together under a new name.",
"type": "object",
"required": [
"description",
"identifier",
"permissions"
],
"properties": {
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does.",
"type": "string"
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionKind"
}
}
}
},
"Permission": {
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"commands": {
"description": "Allowed or denied commands when using this permission.",
"default": {
"allow": [],
"deny": []
},
"allOf": [
{
"$ref": "#/definitions/Commands"
}
]
},
"scope": {
"description": "Allowed or denied scoped when using this permission.",
"allOf": [
{
"$ref": "#/definitions/Scopes"
}
]
},
"platforms": {
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"Commands": {
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
"type": "object",
"properties": {
"allow": {
"description": "Allowed command.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"deny": {
"description": "Denied command, which takes priority.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
}
},
"Scopes": {
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
"type": "object",
"properties": {
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"PermissionKind": {
"type": "string",
"oneOf": [
{
"description": "Enables the add command without any pre-configured scope.",
"type": "string",
"const": "allow-add"
},
{
"description": "Denies the add command without any pre-configured scope.",
"type": "string",
"const": "deny-add"
},
{
"description": "Enables the branch command without any pre-configured scope.",
"type": "string",
"const": "allow-branch"
},
{
"description": "Denies the branch command without any pre-configured scope.",
"type": "string",
"const": "deny-branch"
},
{
"description": "Enables the checkout command without any pre-configured scope.",
"type": "string",
"const": "allow-checkout"
},
{
"description": "Denies the checkout command without any pre-configured scope.",
"type": "string",
"const": "deny-checkout"
},
{
"description": "Enables the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "allow-checkout-remote"
},
{
"description": "Denies the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "deny-checkout-remote"
},
{
"description": "Enables the commit command without any pre-configured scope.",
"type": "string",
"const": "allow-commit"
},
{
"description": "Denies the commit command without any pre-configured scope.",
"type": "string",
"const": "deny-commit"
},
{
"description": "Enables the delete_branch command without any pre-configured scope.",
"type": "string",
"const": "allow-delete-branch"
},
{
"description": "Denies the delete_branch command without any pre-configured scope.",
"type": "string",
"const": "deny-delete-branch"
},
{
"description": "Enables the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "allow-fetch-all"
},
{
"description": "Denies the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "deny-fetch-all"
},
{
"description": "Enables the initialize command without any pre-configured scope.",
"type": "string",
"const": "allow-initialize"
},
{
"description": "Denies the initialize command without any pre-configured scope.",
"type": "string",
"const": "deny-initialize"
},
{
"description": "Enables the log command without any pre-configured scope.",
"type": "string",
"const": "allow-log"
},
{
"description": "Denies the log command without any pre-configured scope.",
"type": "string",
"const": "deny-log"
},
{
"description": "Enables the merge_branch command without any pre-configured scope.",
"type": "string",
"const": "allow-merge-branch"
},
{
"description": "Denies the merge_branch command without any pre-configured scope.",
"type": "string",
"const": "deny-merge-branch"
},
{
"description": "Enables the pull command without any pre-configured scope.",
"type": "string",
"const": "allow-pull"
},
{
"description": "Denies the pull command without any pre-configured scope.",
"type": "string",
"const": "deny-pull"
},
{
"description": "Enables the push command without any pre-configured scope.",
"type": "string",
"const": "allow-push"
},
{
"description": "Denies the push command without any pre-configured scope.",
"type": "string",
"const": "deny-push"
},
{
"description": "Enables the status command without any pre-configured scope.",
"type": "string",
"const": "allow-status"
},
{
"description": "Denies the status command without any pre-configured scope.",
"type": "string",
"const": "deny-status"
},
{
"description": "Enables the unstage command without any pre-configured scope.",
"type": "string",
"const": "allow-unstage"
},
{
"description": "Denies the unstage command without any pre-configured scope.",
"type": "string",
"const": "deny-unstage"
},
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "default"
}
]
}
}
}

View File

@@ -1,11 +1,11 @@
[package]
name = "yaak-grpc"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]
anyhow = "1.0.79"
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
hyper = "1.5.2"
@@ -18,11 +18,11 @@ md5 = "0.7.0"
prost = "0.13.4"
prost-reflect = { version = "0.14.4", default-features = false, features = ["serde", "derive"] }
prost-types = "0.13.4"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "fs"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] }
tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"

View File

@@ -1,7 +1,7 @@
[package]
name = "yaak-http"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View File

@@ -2,15 +2,15 @@
name = "yaak-license"
links = "yaak-license"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]
chrono = "0.4.38"
log = "0.4.26"
reqwest = { workspace = true, features = ["json"] }
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.132"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tauri = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-activate"
description = "Enables the activate command without any pre-configured scope."
commands.allow = ["activate"]
[[permission]]
identifier = "deny-activate"
description = "Denies the activate command without any pre-configured scope."
commands.deny = ["activate"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check"
description = "Enables the check command without any pre-configured scope."
commands.allow = ["check"]
[[permission]]
identifier = "deny-check"
description = "Denies the check command without any pre-configured scope."
commands.deny = ["check"]

View File

@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-deactivate"
description = "Enables the deactivate command without any pre-configured scope."
commands.allow = ["deactivate"]
[[permission]]
identifier = "deny-deactivate"
description = "Denies the deactivate command without any pre-configured scope."
commands.deny = ["deactivate"]

View File

@@ -1,95 +0,0 @@
## Default Permission
Default permissions for the plugin
- `allow-check`
- `allow-activate`
- `allow-deactivate`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`yaak-license:allow-activate`
</td>
<td>
Enables the activate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-activate`
</td>
<td>
Denies the activate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:allow-check`
</td>
<td>
Enables the check command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-check`
</td>
<td>
Denies the check command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:allow-deactivate`
</td>
<td>
Enables the deactivate command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-license:deny-deactivate`
</td>
<td>
Denies the deactivate command without any pre-configured scope.
</td>
</tr>
</table>

View File

@@ -1,335 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PermissionFile",
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
"type": "object",
"properties": {
"default": {
"description": "The default permission set for the plugin",
"anyOf": [
{
"$ref": "#/definitions/DefaultPermission"
},
{
"type": "null"
}
]
},
"set": {
"description": "A list of permissions sets defined",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionSet"
}
},
"permission": {
"description": "A list of inlined permissions",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Permission"
}
}
},
"definitions": {
"DefaultPermission": {
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
"type": "object",
"required": [
"permissions"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"description": {
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"PermissionSet": {
"description": "A set of direct permissions grouped together under a new name.",
"type": "object",
"required": [
"description",
"identifier",
"permissions"
],
"properties": {
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does.",
"type": "string"
},
"permissions": {
"description": "All permissions this set contains.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionKind"
}
}
}
},
"Permission": {
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"version": {
"description": "The version of the permission.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1.0
},
"identifier": {
"description": "A unique identifier for the permission.",
"type": "string"
},
"description": {
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
]
},
"commands": {
"description": "Allowed or denied commands when using this permission.",
"default": {
"allow": [],
"deny": []
},
"allOf": [
{
"$ref": "#/definitions/Commands"
}
]
},
"scope": {
"description": "Allowed or denied scoped when using this permission.",
"allOf": [
{
"$ref": "#/definitions/Scopes"
}
]
},
"platforms": {
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"Commands": {
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
"type": "object",
"properties": {
"allow": {
"description": "Allowed command.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"deny": {
"description": "Denied command, which takes priority.",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
}
},
"Scopes": {
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
"type": "object",
"properties": {
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"PermissionKind": {
"type": "string",
"oneOf": [
{
"description": "Enables the activate command without any pre-configured scope.",
"type": "string",
"const": "allow-activate"
},
{
"description": "Denies the activate command without any pre-configured scope.",
"type": "string",
"const": "deny-activate"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "allow-check"
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "deny-check"
},
{
"description": "Enables the deactivate command without any pre-configured scope.",
"type": "string",
"const": "allow-deactivate"
},
{
"description": "Denies the deactivate command without any pre-configured scope.",
"type": "string",
"const": "deny-deactivate"
},
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "default"
}
]
}
}
}

View File

@@ -5,16 +5,16 @@ use crate::{
};
use log::{debug, info};
use std::string::ToString;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
use tauri::{command, Manager, Runtime, WebviewWindow};
#[command]
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
pub async fn check<R: Runtime>(window: WebviewWindow<R>) -> Result<LicenseCheckStatus> {
debug!("Checking license");
check_license(
&app_handle,
&window,
CheckActivationRequestPayload {
app_platform: get_os().to_string(),
app_version: app_handle.package_info().version.to_string(),
app_version: window.package_info().version.to_string(),
},
)
.await

View File

@@ -12,6 +12,9 @@ pub enum Error {
#[error("{message}")]
ClientError { message: String, error: String },
#[error(transparent)]
ModelError(#[from] yaak_models::error::Error),
#[error("Internal server error")]
ServerError,
}

View File

@@ -7,7 +7,8 @@ use std::ops::Add;
use std::time::Duration;
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::queries::UpdateSource;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
const KV_NAMESPACE: &str = "license";
const KV_ACTIVATION_ID_KEY: &str = "activation_id";
@@ -80,14 +81,12 @@ pub async fn activate_license<R: Runtime>(
}
let body: ActivateLicenseResponsePayload = response.json().await?;
yaak_models::queries::set_key_value_string(
window,
window.app_handle().db().set_key_value_string(
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
body.activation_id.as_str(),
&UpdateSource::Window,
)
.await;
&UpdateSource::from_window(&window),
);
if let Err(e) = window.emit("license-activated", true) {
warn!("Failed to emit check-license event: {}", e);
@@ -100,7 +99,8 @@ pub async fn deactivate_license<R: Runtime>(
window: &WebviewWindow<R>,
p: DeactivateLicenseRequestPayload,
) -> Result<()> {
let activation_id = get_activation_id(window).await;
let app_handle = window.app_handle();
let activation_id = get_activation_id(app_handle).await;
let client = reqwest::Client::new();
let path = format!("/licenses/activations/{}/deactivate", activation_id);
@@ -118,15 +118,13 @@ pub async fn deactivate_license<R: Runtime>(
return Err(ServerError);
}
yaak_models::queries::delete_key_value(
window,
app_handle.db().delete_key_value(
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
&UpdateSource::Window,
)
.await;
&UpdateSource::from_window(&window),
)?;
if let Err(e) = window.emit("license-deactivated", true) {
if let Err(e) = app_handle.emit("license-deactivated", true) {
warn!("Failed to emit deactivate-license event: {}", e);
}
@@ -143,9 +141,12 @@ pub enum LicenseCheckStatus {
Trialing { end: NaiveDateTime },
}
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>, payload: CheckActivationRequestPayload) -> Result<LicenseCheckStatus> {
let activation_id = get_activation_id(app_handle).await;
let settings = yaak_models::queries::get_or_create_settings(app_handle).await;
pub async fn check_license<R: Runtime>(
window: &WebviewWindow<R>,
payload: CheckActivationRequestPayload,
) -> Result<LicenseCheckStatus> {
let activation_id = get_activation_id(window.app_handle()).await;
let settings = window.db().get_settings();
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
debug!("Trial ending at {trial_end:?}");
@@ -195,7 +196,10 @@ fn build_url(path: &str) -> String {
}
}
pub async fn get_activation_id<R: Runtime>(mgr: &impl Manager<R>) -> String {
yaak_models::queries::get_key_value_string(mgr, KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
.await
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
app_handle.db().get_key_value_string(
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
"",
)
}

View File

@@ -0,0 +1,19 @@
[package]
name = "yaak-mac-window"
links = "yaak-mac-window"
version = "0.1.0"
edition = "2024"
publish = false
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.26.0"
hex_color = "3.0.0"
log = "0.4.27"
objc = "0.2.7"
rand = "0.9.0"
[dependencies]
tauri = { workspace = true }

View File

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

View File

@@ -0,0 +1,9 @@
import { invoke } from '@tauri-apps/api/core';
export function setWindowTitle(title: string) {
invoke('plugin:yaak-mac-window|set_title', { title }).catch(console.error);
}
export function setWindowTheme(bgColor: string) {
invoke('plugin:yaak-mac-window|set_theme', { bgColor }).catch(console.error);
}

View File

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

View File

@@ -0,0 +1,6 @@
[default]
description = "Default permissions for the plugin"
permissions = [
"allow-set-title",
"allow-set-theme",
]

View File

@@ -0,0 +1,35 @@
use tauri::{Runtime, Window, command};
#[command]
pub(crate) fn set_title<R: Runtime>(window: Window<R>, title: &str) {
#[cfg(target_os = "macos")]
{
crate::mac::update_window_title(window, title.to_string());
}
#[cfg(not(target_os = "macos"))]
{
let _ = window.set_title(title);
}
}
#[command]
#[allow(unused)]
pub(crate) fn set_theme<R: Runtime>(window: Window<R>, bg_color: &str) {
#[cfg(target_os = "macos")]
{
match hex_color::HexColor::parse_rgb(bg_color.trim()) {
Ok(color) => {
crate::mac::update_window_theme(window, color);
}
Err(err) => {
log::warn!("Failed to parse background color '{}': {}", bg_color, err)
}
}
}
#[cfg(not(target_os = "macos"))]
{
// Nothing yet for non-Mac platforms
}
}

View File

@@ -0,0 +1,23 @@
mod commands;
#[cfg(target_os = "macos")]
mod mac;
use crate::commands::{set_theme, set_title};
use tauri::{
Runtime, generate_handler,
plugin::{Builder, TauriPlugin},
};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[allow(unused)]
Builder::new("yaak-mac-window")
.invoke_handler(generate_handler![set_title, set_theme])
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
{
mac::setup_traffic_light_positioner(&window);
}
})
.build()
}

View File

@@ -1,16 +1,6 @@
use crate::MAIN_WINDOW_PREFIX;
use hex_color::HexColor;
use log::warn;
use objc::{msg_send, sel, sel_impl};
use rand::distr::Alphanumeric;
use rand::Rng;
use tauri::{
plugin::{Builder, TauriPlugin},
Emitter, Listener, Manager, Runtime, Window, WindowEvent,
};
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
use tauri::{Emitter, Runtime, Window};
struct UnsafeWindowHandle(*mut std::ffi::c_void);
@@ -18,65 +8,11 @@ unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("mac_window")
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
{
setup_traffic_light_positioner(&window);
let h = window.app_handle();
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
const MAIN_WINDOW_PREFIX: &str = "main_";
let window_for_theme = window.clone();
let id1 = h.listen("yaak_bg_changed", move |ev| {
let color_str: String = match serde_json::from_str(ev.payload()) {
Ok(color) => color,
Err(err) => {
warn!("Failed to JSON parse color '{}': {}", ev.payload(), err);
return;
}
};
match HexColor::parse_rgb(color_str.trim()) {
Ok(color) => {
update_window_theme(window_for_theme.clone(), color);
}
Err(err) => {
warn!("Failed to parse background color '{}': {}", color_str, err)
}
}
});
let window_for_title = window.clone();
let id2 = h.listen("yaak_title_changed", move |ev| {
let title: String = match serde_json::from_str(ev.payload()) {
Ok(title) => title,
Err(err) => {
warn!("Failed to parse window title \"{}\": {}", ev.payload(), err);
return;
}
};
update_window_title(window_for_title.clone(), title);
});
let h = h.clone();
window.on_window_event(move |e| {
match e {
WindowEvent::Destroyed => {
h.unlisten(id1);
h.unlisten(id2);
}
_ => {}
};
});
}
return;
})
.build()
}
#[cfg(target_os = "macos")]
fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
use cocoa::{appkit::NSWindow, base::nil, foundation::NSString};
unsafe {
@@ -98,8 +34,7 @@ fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
}
}
#[cfg(target_os = "macos")]
fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor) {
pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor) {
use cocoa::appkit::{
NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow,
};
@@ -130,8 +65,12 @@ fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor) {
}
}
#[cfg(target_os = "macos")]
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;
}
@@ -140,6 +79,7 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
#[allow(unexpected_cfgs)]
unsafe {
let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize =
@@ -168,19 +108,19 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct WindowState<R: Runtime> {
window: Window<R>,
}
#[cfg(target_os = "macos")]
pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, BOOL};
use cocoa::base::{BOOL, id};
use cocoa::delegate;
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use rand::Rng;
use rand::distr::Alphanumeric;
use std::ffi::c_void;
position_traffic_lights(
@@ -202,6 +142,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
func(ptr);
}
#[allow(unexpected_cfgs)]
unsafe {
let ns_win =
window.ns_window().expect("NS Window should exist to mount traffic light delegate.")
@@ -432,26 +373,26 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
(windowShouldClose:) => on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern "C" fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern "C" fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern "C" fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern "C" fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern "C" fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern "C" fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern "C" fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id)
}))
}
}

View File

@@ -1,7 +1,8 @@
[package]
name = "yaak-models"
links = "yaak-models"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]
@@ -13,9 +14,13 @@ r2d2_sqlite = { version = "0.25.0" }
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] }
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { version = "0.8.0", default-features = false, features = ["migrate", "sqlite", "runtime-tokio-rustls"] }
tauri = { workspace = true }
thiserror = "2.0.11"
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

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 AnyModel = CookieJar | Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | Plugin | Settings | KeyValue | Workspace | WorkspaceMeta | WebsocketConnection | WebsocketEvent | WebsocketRequest;
export type AnyModel = CookieJar | Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
@@ -12,6 +12,8 @@ export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, up
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -42,9 +44,11 @@ export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type KeyValue = { model: "key_value", createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type ModelPayload = { model: AnyModel, windowLabel: string, updateSource: UpdateSource, };
export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
@@ -54,13 +58,11 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type SyncHistory = { model: "sync_history", id: string, workspaceId: string, createdAt: string, states: Array<SyncState>, checksum: string, relPath: string, syncDir: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
export type UpdateSource = "sync" | "window" | "plugin" | "background" | "import";
export type UpdateSource = { "type": "background" } | { "type": "import" } | { "type": "plugin" } | { "type": "sync" } | { "type": "window", label: string, };
export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array<HttpResponseHeader>, state: WebsocketConnectionState, status: number, url: string, };
@@ -74,6 +76,6 @@ export type WebsocketMessageType = "text" | "binary";
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, name: string, description: string, 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, };
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, settingSyncDir: string | null, };
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };

View File

@@ -0,0 +1,9 @@
// 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";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -0,0 +1,13 @@
const COMMANDS: &[&str] = &[
"delete",
"duplicate",
"get_settings",
"grpc_events",
"upsert",
"websocket_events",
"workspace_models",
];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,80 @@
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import type { AnyModel } from '../bindings/gen_models';
import { ExtractModel } from './types';
import { newStoreData } from './util';
export const modelStoreDataAtom = atom(newStoreData());
export const cookieJarsAtom = createOrderedModelAtom('cookie_jar', 'name', 'asc');
export const environmentsAtom = createOrderedModelAtom('environment', 'name', 'asc');
export const foldersAtom = createModelAtom('folder');
export const grpcConnectionsAtom = createOrderedModelAtom('grpc_connection', 'createdAt', 'desc');
export const grpcEventsAtom = createOrderedModelAtom('grpc_event', 'createdAt', 'asc');
export const grpcRequestsAtom = createModelAtom('grpc_request');
export const httpRequestsAtom = createModelAtom('http_request');
export const httpResponsesAtom = createOrderedModelAtom('http_response', 'createdAt', 'desc');
export const keyValuesAtom = createModelAtom('key_value');
export const pluginsAtom = createModelAtom('plugin');
export const settingsAtom = createSingularModelAtom('settings');
export const websocketRequestsAtom = createModelAtom('websocket_request');
export const websocketEventsAtom = createOrderedModelAtom('websocket_event', 'createdAt', 'asc');
export const websocketConnectionsAtom = createOrderedModelAtom(
'websocket_connection',
'createdAt',
'desc',
);
export const workspaceMetasAtom = createModelAtom('workspace_meta');
export const workspacesAtom = createOrderedModelAtom('workspace', 'name', 'asc');
export function createModelAtom<M extends AnyModel['model']>(modelType: M) {
return selectAtom(
modelStoreDataAtom,
(data) => Object.values(data[modelType] ?? {}),
shallowEqual,
);
}
export function createSingularModelAtom<M extends AnyModel['model']>(modelType: M) {
return selectAtom(modelStoreDataAtom, (data) => {
const modelData = Object.values(data[modelType] ?? {});
const item = modelData[0];
if (item == null) throw new Error('Failed creating singular model with no data: ' + modelType);
return item;
});
}
export function createOrderedModelAtom<M extends AnyModel['model']>(
modelType: M,
field: keyof ExtractModel<AnyModel, M>,
order: 'asc' | 'desc',
) {
return selectAtom(
modelStoreDataAtom,
(data) => {
const modelData = data[modelType] ?? {};
return Object.values(modelData).sort(
(a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => {
const n = a[field] > b[field] ? 1 : -1;
return order === 'desc' ? n * -1 : n;
},
);
},
shallowEqual,
);
}
function shallowEqual<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,11 @@
import { AnyModel } from '../bindings/gen_models';
export * from '../bindings/gen_models';
export * from '../bindings/gen_util';
export * from './store';
export * from './atoms';
export function modelTypeLabel(m: AnyModel): string {
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
return m.model.split('_').map(capitalize).join(' ');
}

View File

@@ -0,0 +1,205 @@
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { AnyModel, ModelPayload } from '../bindings/gen_models';
import { modelStoreDataAtom } from './atoms';
import { ExtractModel, JotaiStore, ModelStoreData } from './types';
import { newStoreData } from './util';
let _store: JotaiStore | null = null;
export function initModelStore(store: JotaiStore) {
_store = store;
getCurrentWebviewWindow()
.listen<ModelPayload>('upserted_model', ({ payload }) => {
if (shouldIgnoreModel(payload)) return;
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
return {
...prev,
[payload.model.model]: {
...prev[payload.model.model],
[payload.model.id]: payload.model,
},
};
});
})
.catch(console.error);
getCurrentWebviewWindow()
.listen<ModelPayload>('deleted_model', ({ payload }) => {
if (shouldIgnoreModel(payload)) return;
console.log('Delete model', payload);
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
const modelData = { ...prev[payload.model.model] };
delete modelData[payload.model.id];
return { ...prev, [payload.model.model]: modelData };
});
})
.catch(console.error);
}
function mustStore(): JotaiStore {
if (_store == null) {
throw new Error('Model store was not initialized');
}
return _store;
}
let _activeWorkspaceId: string | null = null;
export async function changeModelStoreWorkspace(workspaceId: string | null) {
console.log('Syncing models with new workspace', workspaceId);
const workspaceModels = await invoke<AnyModel[]>('plugin:yaak-models|workspace_models', {
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
});
const data = newStoreData();
for (const model of workspaceModels) {
data[model.model][model.id] = model;
}
mustStore().set(modelStoreDataAtom, data);
console.log('Synced model store with workspace', workspaceId, data);
_activeWorkspaceId = workspaceId;
}
export function getAnyModel(id: string): AnyModel | null {
let data = mustStore().get(modelStoreDataAtom);
for (const modelData of Object.values(data)) {
let model = modelData[id];
if (model != null) {
return model;
}
}
return null;
}
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
modelType: M | M[],
id: string,
): T | null {
let data = mustStore().get(modelStoreDataAtom);
for (const t of Array.isArray(modelType) ? modelType : [modelType]) {
let v = data[t][id];
if (v?.model === t) return v as T;
}
return null;
}
export function patchModelById<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: M,
id: string,
patch: Partial<T> | ((prev: T) => T),
): Promise<string> {
let prev = getModel<M, T>(model, id);
if (prev == null) {
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
}
const newModel = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
return updateModel(newModel);
}
export async function patchModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
base: Pick<T, 'id' | 'model'>,
patch: Partial<T>,
): Promise<string> {
return patchModelById<M, T>(base.model, base.id, patch);
}
export async function updateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T,
): Promise<string> {
return invoke<string>('plugin:yaak-models|upsert', { model });
}
export async function deleteModelById<
M extends AnyModel['model'],
T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) {
let model = getModel<M, T>(modelType, id);
await deleteModel(model);
}
export async function deleteModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T | null,
) {
if (model == null) {
throw new Error('Failed to delete null model');
}
await invoke<string>('plugin:yaak-models|delete', { model });
}
export function duplicateModelById<
M extends AnyModel['model'],
T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) {
let model = getModel<M, T>(modelType, id);
return duplicateModel(model);
}
export function duplicateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T | null,
) {
if (model == null) {
throw new Error('Failed to delete null model');
}
return invoke<string>('plugin:yaak-models|duplicate', { model });
}
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, 'model'>,
): Promise<string> {
return invoke<string>('plugin:yaak-models|upsert', { model: patch });
}
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, 'model' | 'workspaceId'>,
): Promise<string> {
return invoke<string>('plugin:yaak-models|upsert', { model: patch });
}
export function replaceModelsInStore<
M extends AnyModel['model'],
T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[]) {
const newModels: Record<string, T> = {};
for (const model of models) {
newModels[model.id] = model;
}
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
return {
...prev,
[model]: newModels,
};
});
}
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
// Never ignore updates from non-user sources
if (updateSource.type !== 'window') {
return false;
}
// Never ignore same-window updates
if (updateSource.label === getCurrentWebviewWindow().label) {
return false;
}
// Only sync models that belong to this workspace, if a workspace ID is present
if ('workspaceId' in model && model.workspaceId !== _activeWorkspaceId) {
return true;
}
if (model.model === 'key_value' && model.namespace === 'no_sync') {
return true;
}
return false;
}

View File

@@ -0,0 +1,8 @@
import { createStore } from 'jotai';
import { AnyModel } from '../bindings/gen_models';
export type ExtractModel<T, M> = T extends { model: M } ? T : never;
export type ModelStoreData<T extends AnyModel = AnyModel> = {
[M in T['model']]: Record<string, Extract<T, { model: M }>>;
};
export type JotaiStore = ReturnType<typeof createStore>;

View File

@@ -0,0 +1,23 @@
import { ModelStoreData } from './types';
export function newStoreData(): ModelStoreData {
return {
cookie_jar: {},
environment: {},
folder: {},
grpc_connection: {},
grpc_event: {},
grpc_request: {},
http_request: {},
http_response: {},
key_value: {},
plugin: {},
settings: {},
sync_state: {},
websocket_connection: {},
websocket_event: {},
websocket_request: {},
workspace: {},
workspace_meta: {},
};
}

View File

@@ -1 +0,0 @@
export * from './bindings/gen_models';

View File

@@ -2,5 +2,5 @@
"name": "@yaakapp-internal/models",
"private": true,
"version": "1.0.0",
"main": "index.ts"
"main": "guest-js/index.ts"
}

View File

@@ -0,0 +1,11 @@
[default]
description = "Default permissions for the plugin"
permissions = [
"allow-delete",
"allow-duplicate",
"allow-get-settings",
"allow-grpc-events",
"allow-upsert",
"allow-websocket-events",
"allow-workspace-models",
]

View File

@@ -0,0 +1,124 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::models::{AnyModel, GrpcEvent, Settings, WebsocketEvent};
use crate::query_manager::QueryManagerExt;
use crate::util::UpdateSource;
use tauri::{AppHandle, Runtime, WebviewWindow};
#[tauri::command]
pub(crate) fn upsert<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
let db = window.db();
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::CookieJar(m) => db.upsert_cookie_jar(&m, source)?.id,
AnyModel::Environment(m) => db.upsert_environment(&m, source)?.id,
AnyModel::Folder(m) => db.upsert_folder(&m, source)?.id,
AnyModel::GrpcRequest(m) => db.upsert_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => db.upsert_http_request(&m, source)?.id,
AnyModel::HttpResponse(m) => db.upsert_http_response(&m, source)?.id,
AnyModel::KeyValue(m) => db.upsert_key_value(&m, source)?.id,
AnyModel::Plugin(m) => db.upsert_plugin(&m, source)?.id,
AnyModel::Settings(m) => db.upsert_settings(&m, source)?.id,
AnyModel::WebsocketRequest(m) => db.upsert_websocket_request(&m, source)?.id,
AnyModel::Workspace(m) => db.upsert_workspace(&m, source)?.id,
AnyModel::WorkspaceMeta(m) => db.upsert_workspace_meta(&m, source)?.id,
a => return Err(GenericError(format!("Cannot upsert AnyModel {a:?})"))),
};
Ok(id)
}
#[tauri::command]
pub(crate) fn delete<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
// Use transaction for deletions because it might recurse
window.with_tx(|tx| {
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::CookieJar(m) => tx.delete_cookie_jar(&m, source)?.id,
AnyModel::Environment(m) => tx.delete_environment(&m, source)?.id,
AnyModel::Folder(m) => tx.delete_folder(&m, source)?.id,
AnyModel::GrpcConnection(m) => tx.delete_grpc_connection(&m, source)?.id,
AnyModel::GrpcRequest(m) => tx.delete_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => tx.delete_http_request(&m, source)?.id,
AnyModel::HttpResponse(m) => tx.delete_http_response(&m, source)?.id,
AnyModel::Plugin(m) => tx.delete_plugin(&m, source)?.id,
AnyModel::WebsocketConnection(m) => tx.delete_websocket_connection(&m, source)?.id,
AnyModel::WebsocketRequest(m) => tx.delete_websocket_request(&m, source)?.id,
AnyModel::Workspace(m) => tx.delete_workspace(&m, source)?.id,
a => return Err(GenericError(format!("Cannot delete AnyModel {a:?})"))),
};
Ok(id)
})
}
#[tauri::command]
pub(crate) fn duplicate<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
// Use transaction for duplications because it might recurse
window.with_tx(|tx| {
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::Environment(m) => tx.duplicate_environment(&m, source)?.id,
AnyModel::Folder(m) => tx.duplicate_folder(&m, source)?.id,
AnyModel::GrpcRequest(m) => tx.duplicate_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => tx.duplicate_http_request(&m, source)?.id,
AnyModel::WebsocketRequest(m) => tx.duplicate_websocket_request(&m, source)?.id,
a => return Err(GenericError(format!("Cannot duplicate AnyModel {a:?})"))),
};
Ok(id)
})
}
#[tauri::command]
pub(crate) fn websocket_events<R: Runtime>(
app_handle: AppHandle<R>,
connection_id: &str,
) -> Result<Vec<WebsocketEvent>> {
Ok(app_handle.db().list_websocket_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn grpc_events<R: Runtime>(
app_handle: AppHandle<R>,
connection_id: &str,
) -> Result<Vec<GrpcEvent>> {
Ok(app_handle.db().list_grpc_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn get_settings<R: Runtime>(app_handle: AppHandle<R>) -> Result<Settings> {
Ok(app_handle.db().get_settings())
}
#[tauri::command]
pub(crate) fn workspace_models<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: Option<&str>,
) -> Result<Vec<AnyModel>> {
let db = window.db();
let mut l: Vec<AnyModel> = Vec::new();
// Add the settings
l.push(db.get_settings().into());
// Add global models
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
// Add the workspace children
if let Some(wid) = workspace_id {
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_environments(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_connections(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_http_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_http_responses(wid, None)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_websocket_connections(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_websocket_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_workspace_metas(wid)?.into_iter().map(Into::into).collect());
}
Ok(l)
}

View File

@@ -0,0 +1,25 @@
use r2d2::PooledConnection;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{Connection, Statement, ToSql, Transaction};
pub enum ConnectionOrTx<'a> {
Connection(PooledConnection<SqliteConnectionManager>),
Transaction(&'a Transaction<'a>),
}
impl<'a> ConnectionOrTx<'a> {
pub(crate) fn resolve(&self) -> &Connection {
match self {
ConnectionOrTx::Connection(c) => c,
ConnectionOrTx::Transaction(c) => c,
}
}
pub(crate) fn prepare(&self, sql: &str) -> rusqlite::Result<Statement<'_>> {
self.resolve().prepare(sql)
}
pub(crate) fn execute(&self, sql: &str, params: &[&dyn ToSql]) -> rusqlite::Result<usize> {
self.resolve().execute(sql, params)
}
}

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