mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-17 17:12:13 +02:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8413c441 | ||
|
|
0e14625e62 | ||
|
|
8c72538102 | ||
|
|
ffa6a610b8 | ||
|
|
8b031db685 | ||
|
|
96c8a95094 |
@@ -103,18 +103,6 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake ninja-build libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libnss3 patchelf xdg-utils
|
||||
# crates/yaak-web compiles SQLite to wasm via sqlite-wasm-rs, whose C shim
|
||||
# uses C23 [[noreturn]] and expects a freestanding wasm32 target. Ubuntu
|
||||
# 22.04 ships only clang <=15: 14 rejects the attribute, and 15 falls
|
||||
# through to host glibc headers ("bits/libc-header-start.h" not found).
|
||||
# clang-18 handles it (it is what ubuntu-24.04 uses). Install it from
|
||||
# apt.llvm.org since 22.04's repos stop at 15. Only the wasm build uses
|
||||
# this compiler, so the shipped binary keeps 22.04's glibc floor.
|
||||
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
|
||||
chmod +x /tmp/llvm.sh
|
||||
sudo /tmp/llvm.sh 18
|
||||
echo "CC_wasm32_unknown_unknown=/usr/bin/clang-18" >> "$GITHUB_ENV"
|
||||
echo "AR_wasm32_unknown_unknown=/usr/bin/llvm-ar-18" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Protoc for plugin-runtime
|
||||
uses: arduino/setup-protoc@v3
|
||||
|
||||
Generated
+30
-29
@@ -249,7 +249,7 @@ dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
@@ -265,7 +265,7 @@ dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
@@ -585,9 +585,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.18.0"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -595,15 +595,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.44.0"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4464,7 +4463,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
dependencies = [
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5024,14 +5023,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
@@ -5055,18 +5055,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.6.1+3.6.3"
|
||||
version = "300.5.0+3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||
checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.117"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5880,7 +5880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||
dependencies = [
|
||||
"phf_shared 0.10.0",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5890,7 +5890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6455,9 +6455,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.7"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
@@ -6466,9 +6466,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -7348,7 +7348,7 @@ dependencies = [
|
||||
"borsh",
|
||||
"bytes",
|
||||
"num-traits",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
"rkyv",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9509,7 +9509,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -9726,7 +9726,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log 0.4.29",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
@@ -9996,7 +9996,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -11241,7 +11241,7 @@ dependencies = [
|
||||
"pretty_graphql",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"reqwest 0.12.20",
|
||||
"rlimit",
|
||||
"serde",
|
||||
@@ -11326,7 +11326,7 @@ dependencies = [
|
||||
"log 0.4.29",
|
||||
"oxc_resolver",
|
||||
"predicates",
|
||||
"rand 0.8.7",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.20",
|
||||
"rolldown",
|
||||
"schemars 0.8.22",
|
||||
@@ -11352,6 +11352,7 @@ dependencies = [
|
||||
name = "yaak-commands"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
@@ -11534,7 +11535,7 @@ dependencies = [
|
||||
"csscolorparser",
|
||||
"log 0.4.29",
|
||||
"objc",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
]
|
||||
@@ -11576,7 +11577,7 @@ dependencies = [
|
||||
"log 0.4.29",
|
||||
"md5 0.7.0",
|
||||
"path-slash",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"reqwest 0.12.20",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -11754,7 +11755,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"rand 0.9.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
||||
@@ -1,29 +1,9 @@
|
||||
import type { Diagnostic } from "@codemirror/lint";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { type ParseError, parse, printParseErrorCode } from "jsonc-parser";
|
||||
import { parse as jsonLintParse } from "@prantlf/jsonlint";
|
||||
|
||||
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
||||
|
||||
// jsonc-parser reports error codes, so these are the words the editor shows for them
|
||||
const MESSAGES: Record<string, string> = {
|
||||
InvalidSymbol: "Invalid symbol",
|
||||
InvalidNumberFormat: "Invalid number format",
|
||||
PropertyNameExpected: "Property name expected",
|
||||
ValueExpected: "Value expected",
|
||||
ColonExpected: "Colon expected",
|
||||
CommaExpected: "Comma expected",
|
||||
CloseBraceExpected: "Closing brace expected",
|
||||
CloseBracketExpected: "Closing bracket expected",
|
||||
EndOfFileExpected: "End of file expected",
|
||||
InvalidCommentToken: "Comments are not allowed",
|
||||
UnexpectedEndOfComment: "Unexpected end of comment",
|
||||
UnexpectedEndOfString: "Unexpected end of string",
|
||||
UnexpectedEndOfNumber: "Unexpected end of number",
|
||||
InvalidUnicode: "Invalid unicode sequence",
|
||||
InvalidEscapeCharacter: "Invalid escape character",
|
||||
InvalidCharacter: "Invalid character",
|
||||
};
|
||||
|
||||
interface JsonLintOptions {
|
||||
allowComments?: boolean;
|
||||
allowTrailingCommas?: boolean;
|
||||
@@ -31,28 +11,34 @@ interface JsonLintOptions {
|
||||
|
||||
export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
return (view: EditorView): Diagnostic[] => {
|
||||
const doc = view.state.doc.toString();
|
||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
||||
try {
|
||||
const doc = view.state.doc.toString();
|
||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
||||
jsonLintParse(escapedDoc, {
|
||||
mode: (options?.allowComments ?? true) ? "cjson" : "json",
|
||||
ignoreTrailingCommas: options?.allowTrailingCommas ?? false,
|
||||
});
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
} catch (err: any) {
|
||||
if (!("location" in err)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors: ParseError[] = [];
|
||||
parse(escapedDoc, errors, {
|
||||
allowTrailingComma: options?.allowTrailingCommas ?? false,
|
||||
disallowComments: !(options?.allowComments ?? true),
|
||||
});
|
||||
|
||||
// Later errors are mostly consequences of the first one, so only that one is shown
|
||||
const error = errors[0];
|
||||
if (error == null) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
from: error.offset,
|
||||
to: error.offset + error.length,
|
||||
severity: "error",
|
||||
message: MESSAGES[printParseErrorCode(error.error)] ?? "Invalid JSON",
|
||||
},
|
||||
];
|
||||
// const line = location?.start?.line;
|
||||
// const column = location?.start?.column;
|
||||
if (err.location.start.offset) {
|
||||
return [
|
||||
{
|
||||
from: err.location.start.offset,
|
||||
to: err.location.start.offset,
|
||||
severity: "error",
|
||||
message: err.message,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -333,9 +333,7 @@ export function formatHotkeyString(trigger: string): string[] {
|
||||
} else if (p === "Alt") {
|
||||
labelParts.push("⌥");
|
||||
} else if (p === "Enter") {
|
||||
// U+21A9 has an emoji presentation, which Chromium's font fallback picks
|
||||
// (a blue glyph among monochrome ones). U+FE0E forces the text form.
|
||||
labelParts.push("↩︎");
|
||||
labelParts.push("↩");
|
||||
} else if (p === "Tab") {
|
||||
labelParts.push("⇥");
|
||||
} else if (p === "Backspace") {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.3",
|
||||
"@mjackson/multipart-parser": "^0.10.1",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
@@ -53,7 +54,6 @@
|
||||
"jotai": "^2.18.0",
|
||||
"jotai-family": "^1.0.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.4.7",
|
||||
@@ -93,12 +93,14 @@
|
||||
"@yaakapp-internal/theme": "^1.0.0",
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"decompress": "^4.2.1",
|
||||
"internal-ip": "^8.0.0",
|
||||
"rollup": "^4.60.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.9",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||
"vite-plugin-static-copy": "^3.3.0",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vite-plus": "^0.2.9"
|
||||
"vite-plus": "^0.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ const WorkspacesWorkspaceIdRequestsRequestIdRoute =
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
'/workspaces': typeof WorkspacesIndexRoute
|
||||
'/workspaces/$workspaceId/settings': typeof WorkspacesWorkspaceIdSettingsRoute
|
||||
'/workspaces/$workspaceId/': typeof WorkspacesWorkspaceIdIndexRoute
|
||||
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
|
||||
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
@@ -70,9 +70,9 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/workspaces/'
|
||||
| '/workspaces'
|
||||
| '/workspaces/$workspaceId/settings'
|
||||
| '/workspaces/$workspaceId/'
|
||||
| '/workspaces/$workspaceId'
|
||||
| '/workspaces/$workspaceId/requests/$requestId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -110,14 +110,14 @@ declare module '@tanstack/react-router' {
|
||||
'/workspaces/': {
|
||||
id: '/workspaces/'
|
||||
path: '/workspaces'
|
||||
fullPath: '/workspaces/'
|
||||
fullPath: '/workspaces'
|
||||
preLoaderRoute: typeof WorkspacesIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/$workspaceId/': {
|
||||
id: '/workspaces/$workspaceId/'
|
||||
path: '/workspaces/$workspaceId'
|
||||
fullPath: '/workspaces/$workspaceId/'
|
||||
fullPath: '/workspaces/$workspaceId'
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite-plus";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -42,11 +43,10 @@ export default defineConfig(async () => {
|
||||
: {},
|
||||
},
|
||||
// The browser host runs the model layer in a worker; that bundle needs the
|
||||
// same wasm handling as the main one. Top-level await needs no transform
|
||||
// because the build targets esnext.
|
||||
// same wasm and top-level-await handling as the main one.
|
||||
worker: {
|
||||
format: "es" as const,
|
||||
plugins: () => [wasm()],
|
||||
plugins: () => [wasm(), topLevelAwait()],
|
||||
},
|
||||
plugins: [
|
||||
wasm(),
|
||||
@@ -58,6 +58,7 @@ export default defineConfig(async () => {
|
||||
}),
|
||||
svgr(),
|
||||
react(),
|
||||
topLevelAwait(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: cMapsDir, dest: "" },
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.9",
|
||||
"vite-plus": "^0.2.9"
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||
"vite-plus": "^0.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,11 +181,7 @@ async fn dev(args: PluginPathArg) -> CommandResult {
|
||||
ui::info(&format!("Rebuilding plugin {display_path}"));
|
||||
}
|
||||
WatcherEvent::Event(BundleEvent::BundleEnd(_)) => {
|
||||
// Assets are staged on every rebuild, so a changed asset or
|
||||
// declaration is picked up without restarting.
|
||||
let result = copy_build_assets(&watch_root)
|
||||
.and_then(|()| generate_plugin_metadata(&watch_root));
|
||||
match result {
|
||||
match generate_plugin_metadata(&watch_root) {
|
||||
Ok(()) => ui::success(&format!(
|
||||
"Generated plugin metadata at {}",
|
||||
watch_root.join("build/metadata.json").display()
|
||||
@@ -412,7 +408,6 @@ struct PublishResponse {
|
||||
|
||||
async fn build_plugin_bundle(plugin_dir: &Path) -> CommandResult<Vec<String>> {
|
||||
prepare_build_output_dir(plugin_dir)?;
|
||||
copy_build_assets(plugin_dir)?;
|
||||
let mut bundler = Bundler::new(bundler_options(plugin_dir, false))
|
||||
.map_err(|err| format!("Failed to initialize Rolldown: {err}"))?;
|
||||
let output = bundler.write().await.map_err(|err| format!("Plugin build failed:\n{err}"))?;
|
||||
@@ -503,63 +498,6 @@ fn prepare_build_output_dir(plugin_dir: &Path) -> CommandResult {
|
||||
.map_err(|e| format!("Failed to create build directory {}: {e}", build_dir.display()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct PluginManifest {
|
||||
#[serde(default)]
|
||||
yaak: PluginManifestConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct PluginManifestConfig {
|
||||
/// Files to place beside the bundle, as paths relative to the plugin
|
||||
/// directory. Publishing ships everything in `build/`, so these travel with
|
||||
/// the plugin.
|
||||
#[serde(default, rename = "buildAssets")]
|
||||
build_assets: Vec<String>,
|
||||
}
|
||||
|
||||
/// Copy the plugin's declared assets into `build/`.
|
||||
///
|
||||
/// This runs after the directory is cleared and before the bundle is written,
|
||||
/// because a bundle may read an asset from its own directory at import time and
|
||||
/// metadata generation imports the bundle.
|
||||
fn copy_build_assets(plugin_dir: &Path) -> CommandResult {
|
||||
let manifest_path = plugin_dir.join("package.json");
|
||||
let manifest: PluginManifest = serde_json::from_str(
|
||||
&fs::read_to_string(&manifest_path)
|
||||
.map_err(|e| format!("Failed to read {}: {e}", manifest_path.display()))?,
|
||||
)
|
||||
.map_err(|e| format!("Failed to parse {}: {e}", manifest_path.display()))?;
|
||||
|
||||
let build_dir = plugin_dir.join("build");
|
||||
let mut names = HashSet::new();
|
||||
for asset in manifest.yaak.build_assets {
|
||||
let src = plugin_dir.join(&asset);
|
||||
let name = src
|
||||
.file_name()
|
||||
.ok_or_else(|| format!("yaak.buildAssets entry is not a file path: {asset}"))?;
|
||||
// A copy that later gets overwritten would pass the build and fail on
|
||||
// load, so anything the build itself writes, or a second asset with
|
||||
// the same name, is rejected up front. Names are compared without
|
||||
// case, because a plugin is installed on case-insensitive filesystems
|
||||
// wherever it was built.
|
||||
let key = name.to_string_lossy().to_lowercase();
|
||||
if key == "index.js" || key == "metadata.json" {
|
||||
return Err(format!("Build asset {asset} would be overwritten by the build output"));
|
||||
}
|
||||
if !names.insert(key) {
|
||||
return Err(format!("Two build assets share the name {}", name.display()));
|
||||
}
|
||||
if !src.is_file() {
|
||||
return Err(format!("Build asset does not exist: {}", src.display()));
|
||||
}
|
||||
fs::copy(&src, build_dir.join(name))
|
||||
.map_err(|e| format!("Failed to copy build asset {}: {e}", src.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundler_options(plugin_dir: &Path, watch: bool) -> BundlerOptions {
|
||||
BundlerOptions {
|
||||
input: Some(vec![InputItem { import: "./src/index.ts".to_string(), ..Default::default() }]),
|
||||
@@ -812,10 +750,7 @@ describe("Example Plugin", () => {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
copy_build_assets, create_publish_archive, generate_plugin_metadata,
|
||||
prepare_build_output_dir,
|
||||
};
|
||||
use super::{create_publish_archive, generate_plugin_metadata};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
@@ -860,100 +795,6 @@ mod tests {
|
||||
assert!(!names.contains("ignored/secret.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_build_output_dir_clears_stale_output() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
let build = root.join("build");
|
||||
fs::create_dir_all(&build).expect("create build");
|
||||
fs::write(build.join("index.js"), "stale").expect("write index.js");
|
||||
fs::write(build.join("left-behind.js"), "stale").expect("write extra");
|
||||
|
||||
prepare_build_output_dir(root).expect("prepare build dir");
|
||||
|
||||
// Publishing ships everything under build/, so nothing may survive.
|
||||
assert!(build.is_dir());
|
||||
assert_eq!(fs::read_dir(&build).expect("read build").count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_build_assets_places_declared_files_beside_the_bundle() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
fs::create_dir_all(root.join("build")).expect("create build");
|
||||
fs::create_dir_all(root.join("vendor")).expect("create vendor");
|
||||
fs::write(root.join("vendor/core_bg.wasm"), "asset").expect("write asset");
|
||||
fs::write(
|
||||
root.join("package.json"),
|
||||
r#"{"yaak":{"buildAssets":["vendor/core_bg.wasm"]}}"#,
|
||||
)
|
||||
.expect("write package.json");
|
||||
|
||||
copy_build_assets(root).expect("copy assets");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("build/core_bg.wasm")).expect("read copied asset"),
|
||||
"asset"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_build_assets_is_a_noop_without_declarations() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
fs::create_dir_all(root.join("build")).expect("create build");
|
||||
fs::write(root.join("package.json"), r#"{"name":"demo"}"#).expect("write package.json");
|
||||
|
||||
copy_build_assets(root).expect("copy assets");
|
||||
|
||||
assert_eq!(fs::read_dir(root.join("build")).expect("read build").count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_build_assets_rejects_names_the_build_writes() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
fs::create_dir_all(root.join("build")).expect("create build");
|
||||
fs::write(root.join("index.js"), "asset").expect("write asset");
|
||||
fs::write(root.join("package.json"), r#"{"yaak":{"buildAssets":["index.js"]}}"#)
|
||||
.expect("write package.json");
|
||||
|
||||
let err = copy_build_assets(root).expect_err("reserved name should fail");
|
||||
assert!(err.contains("overwritten by the build output"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_build_assets_rejects_duplicate_names() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
fs::create_dir_all(root.join("build")).expect("create build");
|
||||
fs::create_dir_all(root.join("a")).expect("create a");
|
||||
fs::create_dir_all(root.join("b")).expect("create b");
|
||||
// Differ only by case: one file on macOS and Windows.
|
||||
fs::write(root.join("a/core.wasm"), "one").expect("write a");
|
||||
fs::write(root.join("b/Core.wasm"), "two").expect("write b");
|
||||
fs::write(
|
||||
root.join("package.json"),
|
||||
r#"{"yaak":{"buildAssets":["a/core.wasm","b/Core.wasm"]}}"#,
|
||||
)
|
||||
.expect("write package.json");
|
||||
|
||||
let err = copy_build_assets(root).expect_err("duplicate name should fail");
|
||||
assert!(err.contains("share the name"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_build_assets_fails_on_a_missing_asset() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
let root = dir.path();
|
||||
fs::create_dir_all(root.join("build")).expect("create build");
|
||||
fs::write(root.join("package.json"), r#"{"yaak":{"buildAssets":["nope.wasm"]}}"#)
|
||||
.expect("write package.json");
|
||||
|
||||
let err = copy_build_assets(root).expect_err("missing asset should fail");
|
||||
assert!(err.contains("Build asset does not exist"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_plugin_metadata_detects_api_types() {
|
||||
let dir = TempDir::new().expect("temp dir");
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use tauri::{Runtime, State, WebviewWindow};
|
||||
use yaak_plugins::events::GetThemesResponse;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
pub(crate) async fn cmd_get_themes<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<Vec<GetThemesResponse>> {
|
||||
Ok(plugin_manager.get_themes(&window.plugin_context()).await?)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use KeyAndValueRef::{Ascii, Binary};
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||
@@ -20,6 +21,22 @@ pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String>
|
||||
entries
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_grpc_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &GrpcRequest,
|
||||
) -> Result<(GrpcRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
window.db().resolve_auth_for_grpc_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let metadata = window.db().resolve_metadata_for_grpc_request(request)?;
|
||||
new_request.metadata = metadata;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_metadata<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
|
||||
@@ -179,3 +179,19 @@ async fn send_http_request_inner<R: Runtime>(
|
||||
Ok(SentHttpRequest { response: result.response, body: result.response_body })
|
||||
}
|
||||
|
||||
pub fn resolve_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &HttpRequest,
|
||||
) -> Result<(HttpRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
window.db().resolve_auth_for_http_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let headers = window.db().resolve_headers_for_http_request(request)?;
|
||||
new_request.headers = headers;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ extern crate core;
|
||||
use crate::encoding::read_response_body;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::grpc::{build_metadata, metadata_to_map};
|
||||
use crate::http_request::send_http_request;
|
||||
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
|
||||
use crate::http_request::{resolve_http_request, send_http_request};
|
||||
use crate::import::{import_data, import_url};
|
||||
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_grpc_request, render_template};
|
||||
use crate::render::{render_grpc_request, render_json_value, render_template};
|
||||
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||
use crate::uri_scheme::handle_deep_link;
|
||||
use error::Result as YaakResult;
|
||||
use eventsource_client::{EventParser, SSE};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -30,20 +31,26 @@ use tokio::task::block_in_place;
|
||||
use tokio::time;
|
||||
use yaak::send::ResponseBody;
|
||||
use yaak_commands::responses::locate_response_body;
|
||||
use yaak_commands::resolve::resolve_grpc_request;
|
||||
use yaak_common::command::new_checked_command;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
||||
use yaak_grpc::{Code, ServiceDefinition};
|
||||
use yaak_mac_window::AppHandleMacWindowExt;
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseState, Workspace,
|
||||
};
|
||||
use yaak_models::util::{BatchUpsertResult, UpdateSource};
|
||||
use yaak_plugins::events::{
|
||||
Color, ErrorResponse, FilterResponse, InternalEvent, InternalEventPayload, PluginContext,
|
||||
RenderPurpose, ShowToastRequest,
|
||||
CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs,
|
||||
CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest,
|
||||
CallWebsocketRequestActionArgs, CallWebsocketRequestActionRequest, CallWorkspaceActionArgs,
|
||||
CallWorkspaceActionRequest, Color, FilterResponse, GetFolderActionsResponse,
|
||||
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
|
||||
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse,
|
||||
GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, InternalEvent,
|
||||
InternalEventPayload, JsonPrimitive, PluginContext, RenderPurpose, ShowToastRequest,
|
||||
};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
@@ -51,9 +58,10 @@ use yaak_rpc_schema::{AppMetaData, EphemeralHttpResponse};
|
||||
use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
use yaak_templates::strip_json_comments::strip_json_comments;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args};
|
||||
use yaak_tls::find_client_certificate;
|
||||
|
||||
mod commands;
|
||||
mod encoding;
|
||||
mod error;
|
||||
mod feedback;
|
||||
@@ -212,6 +220,56 @@ async fn detect_cli_version_for_binary(program: &str) -> Option<String> {
|
||||
Some(parts.next().unwrap_or(line).to_string())
|
||||
}
|
||||
|
||||
async fn cmd_template_tokens_to_string<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
tokens: Tokens,
|
||||
) -> YaakResult<String> {
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
|
||||
RenderPurpose::Preview,
|
||||
);
|
||||
let new_tokens = transform_args(tokens, &cb)?;
|
||||
Ok(new_tokens.to_string())
|
||||
}
|
||||
|
||||
async fn cmd_render_template<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
template: &str,
|
||||
workspace_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
purpose: Option<RenderPurpose>,
|
||||
ignore_error: Option<bool>,
|
||||
) -> YaakResult<String> {
|
||||
let environment_chain =
|
||||
app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let result = render_template(
|
||||
template,
|
||||
environment_chain,
|
||||
&PluginTemplateCallback::new(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
|
||||
purpose.unwrap_or(RenderPurpose::Preview),
|
||||
),
|
||||
&RenderOptions {
|
||||
error_behavior: match ignore_error {
|
||||
Some(true) => RenderErrorBehavior::ReturnEmpty,
|
||||
_ => RenderErrorBehavior::Throw,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn cmd_send_feedback<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
feature: String,
|
||||
@@ -238,8 +296,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
) -> YaakResult<Vec<ServiceDefinition>> {
|
||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||
let (resolved_request, auth_context_id) =
|
||||
resolve_grpc_request(&window.db(), &unrendered_request)?;
|
||||
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
||||
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&unrendered_request.workspace_id,
|
||||
@@ -299,8 +356,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
) -> YaakResult<String> {
|
||||
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
|
||||
let (resolved_request, auth_context_id) =
|
||||
resolve_grpc_request(&window.db(), &unrendered_request)?;
|
||||
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&unrendered_request.workspace_id,
|
||||
unrendered_request.folder_id.as_deref(),
|
||||
@@ -1024,19 +1080,283 @@ async fn cmd_import_url<R: Runtime>(
|
||||
import_url(&window, url).await
|
||||
}
|
||||
|
||||
async fn cmd_http_request_actions<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetHttpRequestActionsResponse>> {
|
||||
Ok(plugin_manager.get_http_request_actions(&window.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_websocket_request_actions<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
Ok(plugin_manager.get_websocket_request_actions(&window.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_websocket_request_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
req: CallWebsocketRequestActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<()> {
|
||||
let websocket_request = window.db().get_websocket_request(&req.args.websocket_request.id)?;
|
||||
Ok(plugin_manager
|
||||
.call_websocket_request_action(
|
||||
&window.plugin_context(),
|
||||
CallWebsocketRequestActionRequest {
|
||||
args: CallWebsocketRequestActionArgs { websocket_request },
|
||||
..req
|
||||
},
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_workspace_actions<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetWorkspaceActionsResponse>> {
|
||||
Ok(plugin_manager.get_workspace_actions(&window.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_workspace_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
req: CallWorkspaceActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<()> {
|
||||
let workspace = window.db().get_workspace(&req.args.workspace.id)?;
|
||||
Ok(plugin_manager
|
||||
.call_workspace_action(
|
||||
&window.plugin_context(),
|
||||
CallWorkspaceActionRequest { args: CallWorkspaceActionArgs { workspace }, ..req },
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_folder_actions<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetFolderActionsResponse>> {
|
||||
Ok(plugin_manager.get_folder_actions(&window.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_folder_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
req: CallFolderActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<()> {
|
||||
let folder = window.db().get_folder(&req.args.folder.id)?;
|
||||
Ok(plugin_manager
|
||||
.call_folder_action(
|
||||
&window.plugin_context(),
|
||||
CallFolderActionRequest { args: CallFolderActionArgs { folder }, ..req },
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_grpc_request_actions<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetGrpcRequestActionsResponse>> {
|
||||
Ok(plugin_manager.get_grpc_request_actions(&window.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_template_function_summaries<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
let results = plugin_manager.get_template_function_summaries(&window.plugin_context()).await?;
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn cmd_template_function_config<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
function_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model: AnyModel,
|
||||
_environment_id: Option<&str>,
|
||||
) -> YaakResult<GetTemplateFunctionConfigResponse> {
|
||||
Ok(plugin_manager
|
||||
.get_template_function_config(&window.plugin_context(), function_name, values, model.id())
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_get_http_authentication_summaries<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
let results =
|
||||
plugin_manager.get_http_authentication_summaries(&window.plugin_context()).await?;
|
||||
Ok(results.into_iter().map(|(_, a)| a).collect())
|
||||
}
|
||||
|
||||
async fn cmd_get_http_authentication_config<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
encryption_manager: State<'_, EncryptionManager>,
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model: AnyModel,
|
||||
environment_id: Option<&str>,
|
||||
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
||||
let (workspace_id, folder_id) = match &model {
|
||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
||||
_ => return Err(GenericError("Unsupported model type for authentication config".into())),
|
||||
};
|
||||
|
||||
// Resolve environment chain and render the values for token lookup
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&workspace_id,
|
||||
folder_id.as_deref(),
|
||||
environment_id,
|
||||
)?;
|
||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager_arc,
|
||||
encryption_manager_arc,
|
||||
&window.plugin_context(),
|
||||
RenderPurpose::Preview,
|
||||
);
|
||||
|
||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
||||
let rendered_json =
|
||||
render_json_value(values_json, environment_chain, &cb, &RenderOptions::return_empty())
|
||||
.await?;
|
||||
|
||||
// Convert back to HashMap<String, JsonPrimitive>
|
||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
||||
|
||||
Ok(plugin_manager
|
||||
.get_http_authentication_config(
|
||||
&window.plugin_context(),
|
||||
auth_name,
|
||||
rendered_values,
|
||||
model.id(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_http_request_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
req: CallHttpRequestActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<()> {
|
||||
Ok(plugin_manager
|
||||
.call_http_request_action(
|
||||
&window.plugin_context(),
|
||||
CallHttpRequestActionRequest {
|
||||
args: CallHttpRequestActionArgs {
|
||||
http_request: resolve_http_request(&window, &req.args.http_request)?.0,
|
||||
..req.args
|
||||
},
|
||||
..req
|
||||
},
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_grpc_request_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
req: CallGrpcRequestActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<()> {
|
||||
Ok(plugin_manager
|
||||
.call_grpc_request_action(
|
||||
&window.plugin_context(),
|
||||
CallGrpcRequestActionRequest {
|
||||
args: CallGrpcRequestActionArgs {
|
||||
grpc_request: resolve_grpc_request(&window, &req.args.grpc_request)?.0,
|
||||
..req.args
|
||||
},
|
||||
..req
|
||||
},
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_http_authentication_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
encryption_manager: State<'_, EncryptionManager>,
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model: AnyModel,
|
||||
environment_id: Option<&str>,
|
||||
) -> YaakResult<()> {
|
||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
||||
let (workspace_id, folder_id) = match &model {
|
||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
||||
_ => return Err(GenericError("Unsupported model type for authentication action".into())),
|
||||
};
|
||||
|
||||
// Resolve environment chain and render the values
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&workspace_id,
|
||||
folder_id.as_deref(),
|
||||
environment_id,
|
||||
)?;
|
||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager_arc,
|
||||
encryption_manager_arc,
|
||||
&window.plugin_context(),
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
|
||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
||||
let rendered_json =
|
||||
render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?;
|
||||
|
||||
// Convert back to HashMap<String, JsonPrimitive>
|
||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
||||
|
||||
Ok(plugin_manager
|
||||
.call_http_authentication_action(
|
||||
&window.plugin_context(),
|
||||
auth_name,
|
||||
action_index,
|
||||
rendered_values,
|
||||
&model.id(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn cmd_curl_to_request<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
command: &str,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
workspace_id: &str,
|
||||
) -> YaakResult<HttpRequest> {
|
||||
let import_result = plugin_manager.import_data(&window.plugin_context(), command).await?;
|
||||
|
||||
Ok(import_result
|
||||
.resources
|
||||
.http_requests
|
||||
.get(0)
|
||||
.ok_or(GenericError("No curl command found".to_string()))
|
||||
.map(|r| {
|
||||
let mut request = r.clone();
|
||||
request.workspace_id = workspace_id.into();
|
||||
request.id = "".to_string();
|
||||
request
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Decodes base64 and writes the bytes to a file the user picked.
|
||||
///
|
||||
@@ -1132,6 +1452,17 @@ async fn cmd_send_http_request<R: Runtime>(
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn cmd_reload_plugins<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<Vec<(String, String)>> {
|
||||
let plugins = app_handle.db().list_plugins()?;
|
||||
let plugin_context =
|
||||
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
|
||||
let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
|
||||
Ok(errors)
|
||||
}
|
||||
|
||||
async fn cmd_new_child_window<R: Runtime>(
|
||||
parent_window: WebviewWindow<R>,
|
||||
@@ -1451,7 +1782,6 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
|
||||
let ev = match ev {
|
||||
Ok(Some(ev)) => ev,
|
||||
// Nothing to say, or the reply comes later from somewhere else.
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
warn!("Failed to handle plugin event: {e:?}");
|
||||
@@ -1464,10 +1794,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
timeout: Some(30000),
|
||||
}),
|
||||
);
|
||||
// Tell the plugin as well as the user. It is awaiting a
|
||||
// reply, and a toast it cannot see would leave it
|
||||
// waiting for one that never comes.
|
||||
InternalEventPayload::ErrorResponse(ErrorResponse { error: e.to_string() })
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
//! One import path for rendering, wherever the pieces actually live.
|
||||
//!
|
||||
//! The request renderers are engine code; the template renderers moved to
|
||||
//! `yaak-commands` when the template commands did. Callers in this crate do not
|
||||
//! need to track which is which.
|
||||
|
||||
use serde_json::Value;
|
||||
pub use yaak::render::{render_grpc_request, render_http_request};
|
||||
pub use yaak_commands::render::{render_json_value, render_template};
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||
|
||||
pub async fn render_template<T: TemplateCallback>(
|
||||
template: &str,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
parse_and_render(template, vars, cb, &opt).await
|
||||
}
|
||||
|
||||
pub async fn render_json_value<T: TemplateCallback>(
|
||||
value: Value,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<Value> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
render_json_value_raw(value, vars, cb, opt).await
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ use crate::updates::YaakUpdater;
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
use tauri::{Manager, Runtime, State, WebviewWindow};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use yaak_commands::{Host, PluginHost};
|
||||
@@ -42,9 +41,7 @@ use yaak_models::models::{
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::BatchUpsertResult;
|
||||
use yaak_plugins::events::{
|
||||
CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest,
|
||||
CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, FilterResponse, ImportResponse,
|
||||
JsonPrimitive, RenderPurpose, GetFolderActionsResponse, GetGrpcRequestActionsResponse,
|
||||
FilterResponse, GetFolderActionsResponse, GetGrpcRequestActionsResponse,
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
||||
GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse,
|
||||
GetTemplateFunctionSummaryResponse, GetThemesResponse, GetWebsocketRequestActionsResponse,
|
||||
@@ -53,13 +50,11 @@ use yaak_plugins::events::{
|
||||
use yaak_plugins::api::{PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::native_template_functions::encrypt_secure_template_function;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_rpc::RpcRouter;
|
||||
use yaak_rpc_schema::*;
|
||||
use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_sync::sync::SyncOp;
|
||||
use yaak_templates::TemplateCallback;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
use yaak_ws::WebsocketManager;
|
||||
|
||||
@@ -108,177 +103,27 @@ impl<R: Runtime> Host for ClientCtx<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> ClientCtx<R> {
|
||||
/// The plugin runtime this window talks to. Only the `PluginHost` impl
|
||||
/// below uses it; everything else goes through the trait.
|
||||
fn pm(&self) -> State<'_, PluginManager> {
|
||||
self.window.state::<PluginManager>()
|
||||
}
|
||||
}
|
||||
|
||||
/// The desktop answers all of these out of the `PluginManager` it already
|
||||
/// runs — the Node sidecar. Each is a delegation, which is the point: the
|
||||
/// operations are what the handlers need, and this is one host's way of
|
||||
/// providing them.
|
||||
impl<R: Runtime> PluginHost for ClientCtx<R> {
|
||||
async fn loaded_plugin_metadata(&self, directory: &str) -> Option<PluginMetadata> {
|
||||
let handle = self.pm().get_plugin_by_dir(directory).await?;
|
||||
let manager = self.window.state::<PluginManager>();
|
||||
let handle = manager.get_plugin_by_dir(directory).await?;
|
||||
Some(handle.info())
|
||||
}
|
||||
|
||||
async fn take_plugin_init_errors(&self) -> Vec<(String, String)> {
|
||||
self.pm().take_init_errors().await
|
||||
self.window.state::<PluginManager>().take_init_errors().await
|
||||
}
|
||||
|
||||
async fn resolve_plugins(&self, plugins: Vec<Plugin>) -> Vec<Plugin> {
|
||||
self.pm().resolve_plugins_for_runtime_from_db(plugins).await
|
||||
}
|
||||
|
||||
fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback {
|
||||
PluginTemplateCallback::new(
|
||||
Arc::new((*self.pm()).clone()),
|
||||
Arc::new(self.encryption_manager().clone()),
|
||||
&self.plugin_context(),
|
||||
purpose,
|
||||
)
|
||||
}
|
||||
|
||||
async fn template_function_summaries(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
Ok(self
|
||||
.window
|
||||
.state::<PluginManager>()
|
||||
.get_template_function_summaries(&self.plugin_context())
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn template_function_config(
|
||||
&self,
|
||||
function_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> yaak_commands::Result<GetTemplateFunctionConfigResponse> {
|
||||
Ok(self
|
||||
.window
|
||||
.state::<PluginManager>()
|
||||
.get_template_function_config(&self.plugin_context(), function_name, values, model_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn themes(&self) -> yaak_commands::Result<Vec<GetThemesResponse>> {
|
||||
Ok(self.pm().get_themes(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn http_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
Ok(self.pm().get_http_request_actions(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn websocket_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
Ok(self.pm().get_websocket_request_actions(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn grpc_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetGrpcRequestActionsResponse>> {
|
||||
Ok(self.pm().get_grpc_request_actions(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn workspace_actions(&self) -> yaak_commands::Result<Vec<GetWorkspaceActionsResponse>> {
|
||||
Ok(self.pm().get_workspace_actions(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn folder_actions(&self) -> yaak_commands::Result<Vec<GetFolderActionsResponse>> {
|
||||
Ok(self.pm().get_folder_actions(&self.plugin_context()).await?)
|
||||
}
|
||||
|
||||
async fn call_http_request_action(
|
||||
&self,
|
||||
req: CallHttpRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self.pm().call_http_request_action(&self.plugin_context(), req).await?)
|
||||
}
|
||||
|
||||
async fn call_grpc_request_action(
|
||||
&self,
|
||||
req: CallGrpcRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self.pm().call_grpc_request_action(&self.plugin_context(), req).await?)
|
||||
}
|
||||
|
||||
async fn call_websocket_request_action(
|
||||
&self,
|
||||
req: CallWebsocketRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self.pm().call_websocket_request_action(&self.plugin_context(), req).await?)
|
||||
}
|
||||
|
||||
async fn call_workspace_action(
|
||||
&self,
|
||||
req: CallWorkspaceActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self.pm().call_workspace_action(&self.plugin_context(), req).await?)
|
||||
}
|
||||
|
||||
async fn call_folder_action(
|
||||
&self,
|
||||
req: CallFolderActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self.pm().call_folder_action(&self.plugin_context(), req).await?)
|
||||
}
|
||||
|
||||
async fn http_authentication_summaries(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
let results = self.pm().get_http_authentication_summaries(&self.plugin_context()).await?;
|
||||
Ok(results.into_iter().map(|(_, a)| a).collect())
|
||||
}
|
||||
|
||||
async fn http_authentication_config(
|
||||
&self,
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> yaak_commands::Result<GetHttpAuthenticationConfigResponse> {
|
||||
Ok(self
|
||||
.pm()
|
||||
.get_http_authentication_config(&self.plugin_context(), auth_name, values, model_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn call_http_authentication_action(
|
||||
&self,
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Ok(self
|
||||
.pm()
|
||||
.call_http_authentication_action(
|
||||
&self.plugin_context(),
|
||||
auth_name,
|
||||
action_index,
|
||||
values,
|
||||
model_id,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn import_data(&self, content: &str) -> yaak_commands::Result<ImportResponse> {
|
||||
Ok(self.pm().import_data(&self.plugin_context(), content).await?)
|
||||
}
|
||||
|
||||
async fn reload_plugins(&self, plugins: Vec<Plugin>) -> Vec<(String, String)> {
|
||||
self.pm().initialize_all_plugins(plugins, &self.plugin_context()).await
|
||||
self.window.state::<PluginManager>().resolve_plugins_for_runtime_from_db(plugins).await
|
||||
}
|
||||
|
||||
async fn encrypt_secure_template(&self, template: &str) -> yaak_commands::Result<String> {
|
||||
let plugin_manager = Arc::new((*self.pm()).clone());
|
||||
let plugin_manager = Arc::new((*self.window.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new(self.encryption_manager().clone());
|
||||
Ok(encrypt_secure_template_function(
|
||||
plugin_manager,
|
||||
@@ -382,11 +227,11 @@ async fn cmd_metadata<R: Runtime>(ctx: ClientCtx<R>, _req: CmdMetadataReq) -> Re
|
||||
}
|
||||
|
||||
async fn cmd_template_tokens_to_string<R: Runtime>(ctx: ClientCtx<R>, req: CmdTemplateTokensToStringReq) -> Result<String> {
|
||||
Ok(yaak_commands::templates::cmd_template_tokens_to_string(ctx, req).await?)
|
||||
Ok(crate::cmd_template_tokens_to_string(ctx.window.clone(), ctx.window.app_handle().clone(), req.tokens).await?)
|
||||
}
|
||||
|
||||
async fn cmd_render_template<R: Runtime>(ctx: ClientCtx<R>, req: CmdRenderTemplateReq) -> Result<String> {
|
||||
Ok(yaak_commands::templates::cmd_render_template(ctx, req).await?)
|
||||
Ok(crate::cmd_render_template(ctx.window.clone(), ctx.window.app_handle().clone(), &req.template, &req.workspace_id, req.environment_id.as_deref(), req.purpose, req.ignore_error).await?)
|
||||
}
|
||||
|
||||
async fn cmd_send_feedback<R: Runtime>(ctx: ClientCtx<R>, req: CmdSendFeedbackReq) -> Result<()> {
|
||||
@@ -449,68 +294,68 @@ async fn cmd_import_url<R: Runtime>(ctx: ClientCtx<R>, req: CmdImportUrlReq) ->
|
||||
Ok(crate::cmd_import_url(ctx.window.clone(), &req.url).await?)
|
||||
}
|
||||
|
||||
async fn cmd_http_request_actions<R: Runtime>(ctx: ClientCtx<R>, req: CmdHttpRequestActionsReq) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
Ok(yaak_commands::actions::cmd_http_request_actions(ctx, req).await?)
|
||||
async fn cmd_http_request_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdHttpRequestActionsReq) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
Ok(crate::cmd_http_request_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_websocket_request_actions<R: Runtime>(ctx: ClientCtx<R>, req: CmdWebsocketRequestActionsReq) -> Result<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
Ok(yaak_commands::actions::cmd_websocket_request_actions(ctx, req).await?)
|
||||
async fn cmd_websocket_request_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdWebsocketRequestActionsReq) -> Result<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
Ok(crate::cmd_websocket_request_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_websocket_request_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallWebsocketRequestActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::actions::cmd_call_websocket_request_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_websocket_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_workspace_actions<R: Runtime>(ctx: ClientCtx<R>, req: CmdWorkspaceActionsReq) -> Result<Vec<GetWorkspaceActionsResponse>> {
|
||||
Ok(yaak_commands::actions::cmd_workspace_actions(ctx, req).await?)
|
||||
async fn cmd_workspace_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdWorkspaceActionsReq) -> Result<Vec<GetWorkspaceActionsResponse>> {
|
||||
Ok(crate::cmd_workspace_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_workspace_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallWorkspaceActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::actions::cmd_call_workspace_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_workspace_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_folder_actions<R: Runtime>(ctx: ClientCtx<R>, req: CmdFolderActionsReq) -> Result<Vec<GetFolderActionsResponse>> {
|
||||
Ok(yaak_commands::actions::cmd_folder_actions(ctx, req).await?)
|
||||
async fn cmd_folder_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdFolderActionsReq) -> Result<Vec<GetFolderActionsResponse>> {
|
||||
Ok(crate::cmd_folder_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_folder_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallFolderActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::actions::cmd_call_folder_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_folder_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_grpc_request_actions<R: Runtime>(ctx: ClientCtx<R>, req: CmdGrpcRequestActionsReq) -> Result<Vec<GetGrpcRequestActionsResponse>> {
|
||||
Ok(yaak_commands::actions::cmd_grpc_request_actions(ctx, req).await?)
|
||||
async fn cmd_grpc_request_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdGrpcRequestActionsReq) -> Result<Vec<GetGrpcRequestActionsResponse>> {
|
||||
Ok(crate::cmd_grpc_request_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_template_function_summaries<R: Runtime>(ctx: ClientCtx<R>, req: CmdTemplateFunctionSummariesReq) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
Ok(yaak_commands::templates::cmd_template_function_summaries(ctx, req).await?)
|
||||
async fn cmd_template_function_summaries<R: Runtime>(ctx: ClientCtx<R>, _req: CmdTemplateFunctionSummariesReq) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
Ok(crate::cmd_template_function_summaries(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_template_function_config<R: Runtime>(ctx: ClientCtx<R>, req: CmdTemplateFunctionConfigReq) -> Result<GetTemplateFunctionConfigResponse> {
|
||||
Ok(yaak_commands::templates::cmd_template_function_config(ctx, req).await?)
|
||||
Ok(crate::cmd_template_function_config(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>(), &req.function_name, req.values, req.model, req.environment_id.as_deref()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_get_http_authentication_summaries<R: Runtime>(ctx: ClientCtx<R>, req: CmdGetHttpAuthenticationSummariesReq) -> Result<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
Ok(yaak_commands::auth::cmd_get_http_authentication_summaries(ctx, req).await?)
|
||||
async fn cmd_get_http_authentication_summaries<R: Runtime>(ctx: ClientCtx<R>, _req: CmdGetHttpAuthenticationSummariesReq) -> Result<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
Ok(crate::cmd_get_http_authentication_summaries(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_get_http_authentication_config<R: Runtime>(ctx: ClientCtx<R>, req: CmdGetHttpAuthenticationConfigReq) -> Result<GetHttpAuthenticationConfigResponse> {
|
||||
Ok(yaak_commands::auth::cmd_get_http_authentication_config(ctx, req).await?)
|
||||
Ok(crate::cmd_get_http_authentication_config(ctx.window.clone(), ctx.window.app_handle().clone(), ctx.window.app_handle().state::<PluginManager>(), ctx.window.app_handle().state::<EncryptionManager>(), &req.auth_name, req.values, req.model, req.environment_id.as_deref()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_http_request_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallHttpRequestActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::actions::cmd_call_http_request_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_http_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_grpc_request_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallGrpcRequestActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::actions::cmd_call_grpc_request_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_grpc_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_call_http_authentication_action<R: Runtime>(ctx: ClientCtx<R>, req: CmdCallHttpAuthenticationActionReq) -> Result<()> {
|
||||
Ok(yaak_commands::auth::cmd_call_http_authentication_action(ctx, req).await?)
|
||||
Ok(crate::cmd_call_http_authentication_action(ctx.window.clone(), ctx.window.app_handle().clone(), ctx.window.app_handle().state::<PluginManager>(), ctx.window.app_handle().state::<EncryptionManager>(), &req.auth_name, req.action_index, req.values, req.model, req.environment_id.as_deref()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_curl_to_request<R: Runtime>(ctx: ClientCtx<R>, req: CmdCurlToRequestReq) -> Result<HttpRequest> {
|
||||
Ok(yaak_commands::actions::cmd_curl_to_request(ctx, req).await?)
|
||||
Ok(crate::cmd_curl_to_request(ctx.window.clone(), &req.command, ctx.window.app_handle().state::<PluginManager>(), &req.workspace_id).await?)
|
||||
}
|
||||
|
||||
async fn cmd_export_data<R: Runtime>(ctx: ClientCtx<R>, req: CmdExportDataReq) -> Result<()> {
|
||||
@@ -529,8 +374,8 @@ async fn cmd_send_http_request<R: Runtime>(ctx: ClientCtx<R>, req: CmdSendHttpRe
|
||||
Ok(crate::cmd_send_http_request(ctx.window.app_handle().clone(), ctx.window.clone(), req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), req.request_id).await?)
|
||||
}
|
||||
|
||||
async fn cmd_reload_plugins<R: Runtime>(ctx: ClientCtx<R>, req: CmdReloadPluginsReq) -> Result<Vec<(String, String)>> {
|
||||
Ok(yaak_commands::actions::cmd_reload_plugins(ctx, req).await?)
|
||||
async fn cmd_reload_plugins<R: Runtime>(ctx: ClientCtx<R>, _req: CmdReloadPluginsReq) -> Result<Vec<(String, String)>> {
|
||||
Ok(crate::cmd_reload_plugins(ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_plugin_info<R: Runtime>(ctx: ClientCtx<R>, req: CmdPluginInfoReq) -> Result<PluginMetadata> {
|
||||
@@ -573,8 +418,8 @@ async fn cmd_secure_template<R: Runtime>(ctx: ClientCtx<R>, req: CmdSecureTempla
|
||||
Ok(yaak_commands::encryption::cmd_secure_template(ctx, req).await?)
|
||||
}
|
||||
|
||||
async fn cmd_get_themes<R: Runtime>(ctx: ClientCtx<R>, req: CmdGetThemesReq) -> Result<Vec<GetThemesResponse>> {
|
||||
Ok(yaak_commands::templates::cmd_get_themes(ctx, req).await?)
|
||||
async fn cmd_get_themes<R: Runtime>(ctx: ClientCtx<R>, _req: CmdGetThemesReq) -> Result<Vec<GetThemesResponse>> {
|
||||
Ok(crate::commands::cmd_get_themes(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||
}
|
||||
|
||||
async fn cmd_enable_encryption<R: Runtime>(ctx: ClientCtx<R>, req: CmdEnableEncryptionReq) -> Result<()> {
|
||||
|
||||
@@ -18,7 +18,7 @@ use yaak_http::cookies::CookieStore;
|
||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||
use yaak_models::models::{
|
||||
HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent,
|
||||
WebsocketEventType,
|
||||
WebsocketEventType, WebsocketRequest,
|
||||
};
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
||||
@@ -27,7 +27,6 @@ use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_templates::strip_json_comments::maybe_strip_json_comments;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_tls::find_client_certificate;
|
||||
use yaak_commands::resolve::resolve_websocket_request;
|
||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||
|
||||
pub async fn cmd_ws_send<R: Runtime>(
|
||||
@@ -76,7 +75,7 @@ async fn send_websocket_message<R: Runtime>(
|
||||
environment_id,
|
||||
)?;
|
||||
let (resolved_request, _auth_context_id) =
|
||||
resolve_websocket_request(&window.db(), &unrendered_request)?;
|
||||
resolve_websocket_request(&window, &unrendered_request)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let request = render_websocket_request(
|
||||
@@ -155,7 +154,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
app_handle.db().resolve_settings_for_websocket_request(&unrendered_request)?;
|
||||
let settings = app_handle.db().get_settings();
|
||||
let (resolved_request, auth_context_id) =
|
||||
resolve_websocket_request(&window.db(), &unrendered_request)?;
|
||||
resolve_websocket_request(&window, &unrendered_request)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let request = render_websocket_request(
|
||||
@@ -455,6 +454,23 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
/// Resolve inherited authentication and headers for a websocket request
|
||||
fn resolve_websocket_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &WebsocketRequest,
|
||||
) -> Result<(WebsocketRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
window.db().resolve_auth_for_websocket_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
let headers = window.db().resolve_headers_for_websocket_request(request)?;
|
||||
new_request.headers = headers;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
|
||||
/// Convert WS URL to HTTP URL for cookie filtering
|
||||
/// WebSocket upgrade requests are HTTP requests initially, so HttpOnly cookies should apply
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt"] }
|
||||
yaak = { workspace = true }
|
||||
yaak-core = { workspace = true }
|
||||
yaak-crypto = { workspace = true }
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
//! The actions plugins contribute to the UI, and the calls that run them.
|
||||
//!
|
||||
//! Listing is a plain question for the plugin runtime. Calling is not: the
|
||||
//! frontend sends back the model it was showing, and a plugin must act on what
|
||||
//! that model *actually is* — re-read from the database, with inheritance
|
||||
//! resolved — not on a snapshot the UI has been holding. That re-reading is the
|
||||
//! work these handlers do.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::host::PluginHost;
|
||||
use crate::resolve::{resolve_grpc_request, resolve_http_request};
|
||||
use yaak_models::models::HttpRequest;
|
||||
use yaak_plugins::events::{
|
||||
CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs,
|
||||
CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest,
|
||||
CallWebsocketRequestActionArgs, CallWebsocketRequestActionRequest, CallWorkspaceActionArgs,
|
||||
CallWorkspaceActionRequest, GetFolderActionsResponse, GetGrpcRequestActionsResponse,
|
||||
GetHttpRequestActionsResponse, GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse,
|
||||
};
|
||||
use yaak_rpc_schema::*;
|
||||
|
||||
// -- Listing --
|
||||
|
||||
pub async fn cmd_http_request_actions<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdHttpRequestActionsReq,
|
||||
) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
host.http_request_actions().await
|
||||
}
|
||||
|
||||
pub async fn cmd_websocket_request_actions<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdWebsocketRequestActionsReq,
|
||||
) -> Result<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
host.websocket_request_actions().await
|
||||
}
|
||||
|
||||
pub async fn cmd_grpc_request_actions<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdGrpcRequestActionsReq,
|
||||
) -> Result<Vec<GetGrpcRequestActionsResponse>> {
|
||||
host.grpc_request_actions().await
|
||||
}
|
||||
|
||||
pub async fn cmd_workspace_actions<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdWorkspaceActionsReq,
|
||||
) -> Result<Vec<GetWorkspaceActionsResponse>> {
|
||||
host.workspace_actions().await
|
||||
}
|
||||
|
||||
pub async fn cmd_folder_actions<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdFolderActionsReq,
|
||||
) -> Result<Vec<GetFolderActionsResponse>> {
|
||||
host.folder_actions().await
|
||||
}
|
||||
|
||||
// -- Calling --
|
||||
|
||||
pub async fn cmd_call_http_request_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallHttpRequestActionReq,
|
||||
) -> Result<()> {
|
||||
let inner = req.req;
|
||||
let http_request = resolve_http_request(&host.db(), &inner.args.http_request)?.0;
|
||||
host.call_http_request_action(CallHttpRequestActionRequest {
|
||||
args: CallHttpRequestActionArgs { http_request },
|
||||
..inner
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cmd_call_grpc_request_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallGrpcRequestActionReq,
|
||||
) -> Result<()> {
|
||||
let inner = req.req;
|
||||
let grpc_request = resolve_grpc_request(&host.db(), &inner.args.grpc_request)?.0;
|
||||
host.call_grpc_request_action(CallGrpcRequestActionRequest {
|
||||
args: CallGrpcRequestActionArgs { grpc_request, ..inner.args },
|
||||
..inner
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cmd_call_websocket_request_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallWebsocketRequestActionReq,
|
||||
) -> Result<()> {
|
||||
let inner = req.req;
|
||||
let websocket_request = host.db().get_websocket_request(&inner.args.websocket_request.id)?;
|
||||
host.call_websocket_request_action(CallWebsocketRequestActionRequest {
|
||||
args: CallWebsocketRequestActionArgs { websocket_request },
|
||||
..inner
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cmd_call_workspace_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallWorkspaceActionReq,
|
||||
) -> Result<()> {
|
||||
let inner = req.req;
|
||||
let workspace = host.db().get_workspace(&inner.args.workspace.id)?;
|
||||
host.call_workspace_action(CallWorkspaceActionRequest {
|
||||
args: CallWorkspaceActionArgs { workspace },
|
||||
..inner
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cmd_call_folder_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallFolderActionReq,
|
||||
) -> Result<()> {
|
||||
let inner = req.req;
|
||||
let folder = host.db().get_folder(&inner.args.folder.id)?;
|
||||
host.call_folder_action(CallFolderActionRequest {
|
||||
args: CallFolderActionArgs { folder },
|
||||
..inner
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// -- Other things the plugin runtime does --
|
||||
|
||||
/// Turn a `curl` command line into an unsaved request, by handing it to the
|
||||
/// same importer plugins that read files.
|
||||
pub async fn cmd_curl_to_request<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCurlToRequestReq,
|
||||
) -> Result<HttpRequest> {
|
||||
let imported = host.import_data(&req.command).await?;
|
||||
|
||||
let request = imported
|
||||
.resources
|
||||
.http_requests
|
||||
.first()
|
||||
.ok_or_else(|| Error::Generic("No curl command found".to_string()))?;
|
||||
|
||||
// Belongs to the workspace the user is importing into, and is not saved
|
||||
// until they say so — hence the blank id.
|
||||
let mut request = request.clone();
|
||||
request.workspace_id = req.workspace_id;
|
||||
request.id = String::new();
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// Restart every plugin, returning whatever failed to come back up.
|
||||
pub async fn cmd_reload_plugins<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdReloadPluginsReq,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
let plugins = host.db().list_plugins()?;
|
||||
Ok(host.reload_plugins(plugins).await)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//! Authentication config forms and their actions.
|
||||
//!
|
||||
//! Both commands here do the same preparation: the frontend sends the model
|
||||
//! whose auth is being edited plus the values currently in the form, and those
|
||||
//! values may contain templates. They have to be rendered against the model's
|
||||
//! own environment chain before a plugin sees them, or an auth plugin receives
|
||||
//! `${[ api_key ]}` where it expected a key.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::host::PluginHost;
|
||||
use crate::render::render_json_value;
|
||||
use std::collections::HashMap;
|
||||
use yaak_models::models::AnyModel;
|
||||
use yaak_plugins::events::{
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, JsonPrimitive,
|
||||
RenderPurpose,
|
||||
};
|
||||
use yaak_rpc_schema::*;
|
||||
use yaak_templates::RenderOptions;
|
||||
|
||||
pub async fn cmd_get_http_authentication_summaries<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdGetHttpAuthenticationSummariesReq,
|
||||
) -> Result<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
host.http_authentication_summaries().await
|
||||
}
|
||||
|
||||
pub async fn cmd_get_http_authentication_config<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdGetHttpAuthenticationConfigReq,
|
||||
) -> Result<GetHttpAuthenticationConfigResponse> {
|
||||
// A config form is being displayed, so a template that cannot resolve
|
||||
// should show as blank rather than refuse to open the form.
|
||||
let values = render_auth_values(
|
||||
&host,
|
||||
&req.model,
|
||||
req.environment_id.as_deref(),
|
||||
req.values,
|
||||
RenderPurpose::Preview,
|
||||
&RenderOptions::return_empty(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
host.http_authentication_config(&req.auth_name, values, req.model.id()).await
|
||||
}
|
||||
|
||||
pub async fn cmd_call_http_authentication_action<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdCallHttpAuthenticationActionReq,
|
||||
) -> Result<()> {
|
||||
// An action actually uses these values, so an unresolvable template is an
|
||||
// error rather than an empty string that would silently authenticate wrong.
|
||||
let values = render_auth_values(
|
||||
&host,
|
||||
&req.model,
|
||||
req.environment_id.as_deref(),
|
||||
req.values,
|
||||
RenderPurpose::Send,
|
||||
&RenderOptions::throw(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
host.call_http_authentication_action(&req.auth_name, req.action_index, values, req.model.id())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Render the form's values against the environment chain the model sits in.
|
||||
///
|
||||
/// The chain depends on where the model lives — a request inherits through its
|
||||
/// folder, a workspace has only its own — so the model is what decides which
|
||||
/// variables are in scope.
|
||||
async fn render_auth_values<H: PluginHost>(
|
||||
host: &H,
|
||||
model: &AnyModel,
|
||||
environment_id: Option<&str>,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
purpose: RenderPurpose,
|
||||
options: &RenderOptions,
|
||||
) -> Result<HashMap<String, JsonPrimitive>> {
|
||||
let (workspace_id, folder_id) = match model {
|
||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
||||
other => {
|
||||
return Err(Error::Generic(format!(
|
||||
"Cannot resolve authentication for a {}",
|
||||
other.model()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let environment_chain =
|
||||
host.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
|
||||
|
||||
let cb = host.template_callback(purpose);
|
||||
let rendered =
|
||||
render_json_value(serde_json::to_value(&values)?, environment_chain, &cb, options).await?;
|
||||
|
||||
Ok(serde_json::from_value(rendered)?)
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
//! is anything only a desktop can do — open a native window, run the updater,
|
||||
//! show a native dialog — those handlers stay with the desktop.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use yaak_core::WorkspaceContext;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
@@ -22,17 +21,8 @@ use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::models::Plugin;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{
|
||||
CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest,
|
||||
CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, GetFolderActionsResponse,
|
||||
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
|
||||
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, ImportResponse, JsonPrimitive,
|
||||
PluginContext, RenderPurpose,
|
||||
};
|
||||
use yaak_plugins::events::PluginContext;
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
/// Only `Clone` is required here. `Send`/`Sync`/`'static` are deliberately
|
||||
/// *not*: a browser host is single-threaded and its connection pool is an
|
||||
@@ -116,101 +106,6 @@ pub trait PluginHost: Host {
|
||||
/// loaded. A host without a runtime can return them untouched.
|
||||
fn resolve_plugins(&self, plugins: Vec<Plugin>) -> impl Future<Output = Vec<Plugin>>;
|
||||
|
||||
/// The template functions this host can run, as a callback the renderer
|
||||
/// drives. This is the *only* thing the plugin runtime uniquely provides to
|
||||
/// a render — the variables come from the environment chain, which is an
|
||||
/// ordinary database read — so handing back the callback keeps the rest of
|
||||
/// rendering shared instead of pushing whole commands behind this trait.
|
||||
fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback;
|
||||
|
||||
/// Every template function the installed plugins expose, for the
|
||||
/// autocomplete menu.
|
||||
fn template_function_summaries(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetTemplateFunctionSummaryResponse>>>;
|
||||
|
||||
/// The form a template function wants to show for the given values.
|
||||
fn template_function_config(
|
||||
&self,
|
||||
function_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> impl Future<Output = crate::Result<GetTemplateFunctionConfigResponse>>;
|
||||
|
||||
/// Themes contributed by plugins.
|
||||
fn themes(&self) -> impl Future<Output = crate::Result<Vec<GetThemesResponse>>>;
|
||||
|
||||
// -- Actions plugins contribute to the UI --
|
||||
|
||||
fn http_request_actions(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetHttpRequestActionsResponse>>>;
|
||||
fn websocket_request_actions(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetWebsocketRequestActionsResponse>>>;
|
||||
fn grpc_request_actions(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetGrpcRequestActionsResponse>>>;
|
||||
fn workspace_actions(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetWorkspaceActionsResponse>>>;
|
||||
fn folder_actions(&self) -> impl Future<Output = crate::Result<Vec<GetFolderActionsResponse>>>;
|
||||
|
||||
/// Running an action. The request in each of these has already been
|
||||
/// re-read and had its inheritance resolved by the handler; a host must
|
||||
/// pass it through untouched.
|
||||
fn call_http_request_action(
|
||||
&self,
|
||||
req: CallHttpRequestActionRequest,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
fn call_grpc_request_action(
|
||||
&self,
|
||||
req: CallGrpcRequestActionRequest,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
fn call_websocket_request_action(
|
||||
&self,
|
||||
req: CallWebsocketRequestActionRequest,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
fn call_workspace_action(
|
||||
&self,
|
||||
req: CallWorkspaceActionRequest,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
fn call_folder_action(
|
||||
&self,
|
||||
req: CallFolderActionRequest,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
|
||||
// -- Authentication --
|
||||
|
||||
fn http_authentication_summaries(
|
||||
&self,
|
||||
) -> impl Future<Output = crate::Result<Vec<GetHttpAuthenticationSummaryResponse>>>;
|
||||
|
||||
/// The form an auth plugin wants to show. `values` arrive already rendered.
|
||||
fn http_authentication_config(
|
||||
&self,
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> impl Future<Output = crate::Result<GetHttpAuthenticationConfigResponse>>;
|
||||
|
||||
fn call_http_authentication_action(
|
||||
&self,
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model_id: &str,
|
||||
) -> impl Future<Output = crate::Result<()>>;
|
||||
|
||||
// -- The importers, and the runtime itself --
|
||||
|
||||
/// Hand arbitrary text to the importer plugins and take what they make of
|
||||
/// it. Used for files, URLs and pasted `curl` commands alike.
|
||||
fn import_data(&self, content: &str) -> impl Future<Output = crate::Result<ImportResponse>>;
|
||||
|
||||
/// Restart every plugin, returning `(plugin, error)` for those that failed.
|
||||
fn reload_plugins(&self, plugins: Vec<Plugin>) -> impl Future<Output = Vec<(String, String)>>;
|
||||
|
||||
/// Re-encrypt the `secure(...)` values in a template.
|
||||
///
|
||||
/// Whole operation rather than its pieces because the encryption is only
|
||||
|
||||
@@ -11,18 +11,13 @@
|
||||
//! host-specific types; the ones that stay behind are the ones only a desktop
|
||||
//! can serve (native windows, the updater, dialogs) or that still lean on it.
|
||||
|
||||
pub mod actions;
|
||||
pub mod auth;
|
||||
pub mod data;
|
||||
pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod host;
|
||||
pub mod models;
|
||||
pub mod plugins;
|
||||
pub mod render;
|
||||
pub mod resolve;
|
||||
pub mod responses;
|
||||
pub mod templates;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use host::{Host, PluginHost};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Rendering a template against an environment chain.
|
||||
//!
|
||||
//! The variables come from the chain, the functions come from the host's
|
||||
//! template callback. Neither of these knows which host it is running under —
|
||||
//! that is the whole point of taking the callback as a parameter.
|
||||
|
||||
use serde_json::Value;
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||
|
||||
pub async fn render_template<T: TemplateCallback>(
|
||||
template: &str,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
parse_and_render(template, vars, cb, opt).await
|
||||
}
|
||||
|
||||
pub async fn render_json_value<T: TemplateCallback>(
|
||||
value: Value,
|
||||
environment_chain: Vec<Environment>,
|
||||
cb: &T,
|
||||
opt: &RenderOptions,
|
||||
) -> yaak_templates::error::Result<Value> {
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
render_json_value_raw(value, vars, cb, opt).await
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//! Filling in what a request inherits from its folders and workspace.
|
||||
//!
|
||||
//! A request stored in the database records only what is set *on it*;
|
||||
//! authentication and headers can come from any ancestor. Anything that acts on
|
||||
//! a request as the user sees it — sending it, handing it to a plugin — has to
|
||||
//! resolve that chain first, which is why this is shared rather than living
|
||||
//! next to any one caller.
|
||||
|
||||
use crate::error::Result;
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
||||
|
||||
/// The request with inherited auth and headers filled in, plus the id of the
|
||||
/// model the authentication was inherited *from* — plugins key their token
|
||||
/// caches on it, so it must be the ancestor's id and not the request's.
|
||||
pub fn resolve_http_request(db: &ClientDb, request: &HttpRequest) -> Result<(HttpRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
db.resolve_auth_for_http_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
new_request.headers = db.resolve_headers_for_http_request(request)?;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
|
||||
pub fn resolve_grpc_request(db: &ClientDb, request: &GrpcRequest) -> Result<(GrpcRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
db.resolve_auth_for_grpc_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
new_request.metadata = db.resolve_metadata_for_grpc_request(request)?;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
|
||||
pub fn resolve_websocket_request(
|
||||
db: &ClientDb,
|
||||
request: &WebsocketRequest,
|
||||
) -> Result<(WebsocketRequest, String)> {
|
||||
let mut new_request = request.clone();
|
||||
|
||||
let (authentication_type, authentication, authentication_context_id) =
|
||||
db.resolve_auth_for_websocket_request(request)?;
|
||||
new_request.authentication_type = authentication_type;
|
||||
new_request.authentication = authentication;
|
||||
|
||||
new_request.headers = db.resolve_headers_for_websocket_request(request)?;
|
||||
|
||||
Ok((new_request, authentication_context_id))
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//! Templates, the functions plugins put in them, and themes.
|
||||
//!
|
||||
//! Everything here needs the plugin runtime, but only for the one thing it
|
||||
//! uniquely provides: running a template function. Resolving the environment
|
||||
//! chain and deciding what a render should do about errors are ordinary work
|
||||
//! and stay here, where every host gets them the same.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::host::PluginHost;
|
||||
use crate::render::render_template;
|
||||
use yaak_plugins::events::{
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
RenderPurpose,
|
||||
};
|
||||
use yaak_rpc_schema::*;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions, transform_args};
|
||||
|
||||
pub async fn cmd_render_template<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdRenderTemplateReq,
|
||||
) -> Result<String> {
|
||||
let environment_chain =
|
||||
host.db().resolve_environments(&req.workspace_id, None, req.environment_id.as_deref())?;
|
||||
let cb = host.template_callback(req.purpose.unwrap_or(RenderPurpose::Preview));
|
||||
let options = RenderOptions {
|
||||
// A preview that throws would show the user an error where they expect
|
||||
// to see the value so far, so callers rendering *into the UI* ask for
|
||||
// empties instead.
|
||||
error_behavior: match req.ignore_error {
|
||||
Some(true) => RenderErrorBehavior::ReturnEmpty,
|
||||
_ => RenderErrorBehavior::Throw,
|
||||
},
|
||||
};
|
||||
Ok(render_template(&req.template, environment_chain, &cb, &options).await?)
|
||||
}
|
||||
|
||||
/// Render only the *arguments* of a template's function calls, leaving the
|
||||
/// calls themselves intact. This is what turns a parsed template back into
|
||||
/// something displayable without evaluating it.
|
||||
pub async fn cmd_template_tokens_to_string<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdTemplateTokensToStringReq,
|
||||
) -> Result<String> {
|
||||
let cb = host.template_callback(RenderPurpose::Preview);
|
||||
Ok(transform_args(req.tokens, &cb)?.to_string())
|
||||
}
|
||||
|
||||
pub async fn cmd_template_function_summaries<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdTemplateFunctionSummariesReq,
|
||||
) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
host.template_function_summaries().await
|
||||
}
|
||||
|
||||
pub async fn cmd_template_function_config<H: PluginHost>(
|
||||
host: H,
|
||||
req: CmdTemplateFunctionConfigReq,
|
||||
) -> Result<GetTemplateFunctionConfigResponse> {
|
||||
host.template_function_config(&req.function_name, req.values, req.model.id()).await
|
||||
}
|
||||
|
||||
pub async fn cmd_get_themes<H: PluginHost>(
|
||||
host: H,
|
||||
_req: CmdGetThemesReq,
|
||||
) -> Result<Vec<GetThemesResponse>> {
|
||||
host.themes().await
|
||||
}
|
||||
@@ -8,39 +8,25 @@
|
||||
//! `PluginHost` too, without one, which is only possible because that trait
|
||||
//! names operations rather than handing back a manager.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
use yaak_commands::auth::cmd_get_http_authentication_config;
|
||||
use yaak_commands::models::{
|
||||
cmd_default_headers, cmd_get_workspace_meta, models_delete, models_upsert,
|
||||
models_workspace_models,
|
||||
};
|
||||
use yaak_commands::templates::cmd_render_template;
|
||||
use yaak_commands::{Host, PluginHost};
|
||||
use yaak_core::WorkspaceContext;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::models::{AnyModel, Environment, EnvironmentVariable, Plugin, Workspace};
|
||||
use yaak_models::models::{AnyModel, Plugin, Workspace};
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::{ModelPayload, UpdateSource};
|
||||
use yaak_plugins::events::{
|
||||
CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest,
|
||||
CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, GetFolderActionsResponse,
|
||||
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
|
||||
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
|
||||
GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse,
|
||||
GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, ImportResponse, JsonPrimitive,
|
||||
RenderPurpose,
|
||||
};
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_rpc_schema::{
|
||||
CmdDefaultHeadersReq, CmdGetWorkspaceMetaReq, CmdRenderTemplateReq, ModelsDeleteReq,
|
||||
ModelsUpsertReq, ModelsWorkspaceModelsReq,
|
||||
CmdDefaultHeadersReq, CmdGetWorkspaceMetaReq, ModelsDeleteReq, ModelsUpsertReq,
|
||||
ModelsWorkspaceModelsReq,
|
||||
};
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHost {
|
||||
@@ -169,9 +155,6 @@ async fn host_free_handlers_need_no_state() {
|
||||
#[derive(Clone)]
|
||||
struct SingleThreadedHost {
|
||||
inner: Rc<Inner>,
|
||||
/// The values the last auth-config call arrived with, so a test can check
|
||||
/// they were rendered before the host ever saw them.
|
||||
auth_values: Rc<RefCell<Option<HashMap<String, JsonPrimitive>>>>,
|
||||
}
|
||||
|
||||
impl Host for SingleThreadedHost {
|
||||
@@ -200,32 +183,6 @@ impl Host for SingleThreadedHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// A template callback with no plugins behind it: variables still resolve,
|
||||
/// function calls have nothing to run them. A browser host would put a Worker
|
||||
/// round-trip where this returns an error.
|
||||
struct NoTemplateFunctions;
|
||||
|
||||
impl TemplateCallback for NoTemplateFunctions {
|
||||
async fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
_args: HashMap<String, serde_json::Value>,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
Err(yaak_templates::error::Error::RenderError(format!(
|
||||
"no plugin runtime to run {fn_name}()"
|
||||
)))
|
||||
}
|
||||
|
||||
fn transform_arg(
|
||||
&self,
|
||||
_fn_name: &str,
|
||||
_arg_name: &str,
|
||||
arg_value: &str,
|
||||
) -> yaak_templates::error::Result<String> {
|
||||
Ok(arg_value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Answering plugin questions with no plugin runtime behind them. A browser
|
||||
/// host would put a `postMessage` round-trip to its Worker where these return
|
||||
/// constants; the shape of the trait is what makes either possible.
|
||||
@@ -247,136 +204,12 @@ impl PluginHost for SingleThreadedHost {
|
||||
async fn encrypt_secure_template(&self, _template: &str) -> yaak_commands::Result<String> {
|
||||
Err(yaak_commands::Error::Generic("no plugin runtime on this host".into()))
|
||||
}
|
||||
|
||||
fn template_callback(&self, _purpose: RenderPurpose) -> impl TemplateCallback {
|
||||
NoTemplateFunctions
|
||||
}
|
||||
|
||||
async fn template_function_summaries(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetTemplateFunctionSummaryResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn template_function_config(
|
||||
&self,
|
||||
function_name: &str,
|
||||
_values: HashMap<String, JsonPrimitive>,
|
||||
_model_id: &str,
|
||||
) -> yaak_commands::Result<GetTemplateFunctionConfigResponse> {
|
||||
Err(yaak_commands::Error::Generic(format!("no plugin provides {function_name}()")))
|
||||
}
|
||||
|
||||
async fn themes(&self) -> yaak_commands::Result<Vec<GetThemesResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
// No plugins, so nothing contributes actions and nothing can run one.
|
||||
|
||||
async fn http_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn websocket_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetWebsocketRequestActionsResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn grpc_request_actions(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetGrpcRequestActionsResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn workspace_actions(&self) -> yaak_commands::Result<Vec<GetWorkspaceActionsResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn folder_actions(&self) -> yaak_commands::Result<Vec<GetFolderActionsResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn call_http_request_action(
|
||||
&self,
|
||||
_req: CallHttpRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn call_grpc_request_action(
|
||||
&self,
|
||||
_req: CallGrpcRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn call_websocket_request_action(
|
||||
&self,
|
||||
_req: CallWebsocketRequestActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn call_workspace_action(
|
||||
&self,
|
||||
_req: CallWorkspaceActionRequest,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn call_folder_action(&self, _req: CallFolderActionRequest) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn http_authentication_summaries(
|
||||
&self,
|
||||
) -> yaak_commands::Result<Vec<GetHttpAuthenticationSummaryResponse>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn http_authentication_config(
|
||||
&self,
|
||||
_auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
_model_id: &str,
|
||||
) -> yaak_commands::Result<GetHttpAuthenticationConfigResponse> {
|
||||
*self.auth_values.borrow_mut() = Some(values);
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn call_http_authentication_action(
|
||||
&self,
|
||||
_auth_name: &str,
|
||||
_action_index: i32,
|
||||
_values: HashMap<String, JsonPrimitive>,
|
||||
_model_id: &str,
|
||||
) -> yaak_commands::Result<()> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn import_data(&self, _content: &str) -> yaak_commands::Result<ImportResponse> {
|
||||
Err(no_plugins())
|
||||
}
|
||||
|
||||
async fn reload_plugins(&self, _plugins: Vec<Plugin>) -> Vec<(String, String)> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn no_plugins() -> yaak_commands::Error {
|
||||
yaak_commands::Error::Generic("no plugin runtime on this host".into())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_single_threaded_host_can_implement_the_trait() {
|
||||
let TestHost { inner } = TestHost::new();
|
||||
let host = SingleThreadedHost {
|
||||
inner: Rc::new(Arc::into_inner(inner).expect("sole owner")),
|
||||
auth_values: Rc::new(RefCell::new(None)),
|
||||
};
|
||||
let host = SingleThreadedHost { inner: Rc::new(Arc::into_inner(inner).expect("sole owner")) };
|
||||
|
||||
let workspace = Workspace { name: "From one thread".to_string(), ..Default::default() };
|
||||
let id = models_upsert(host.clone(), ModelsUpsertReq { model: AnyModel::Workspace(workspace) })
|
||||
@@ -394,42 +227,6 @@ async fn a_single_threaded_host_can_implement_the_trait() {
|
||||
.expect("workspace models");
|
||||
assert!(json.contains(&id), "the workspace should be in its own bootstrap payload");
|
||||
|
||||
// Rendering, on a host whose template callback has no plugins behind it.
|
||||
// Resolving the environment chain is a database read and the render is
|
||||
// shared code; only the callback came from the host. Rendering a real
|
||||
// variable is what proves the chain was resolved rather than skipped.
|
||||
let environment = host
|
||||
.db()
|
||||
.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: id.clone(),
|
||||
name: "Test env".to_string(),
|
||||
variables: vec![EnvironmentVariable {
|
||||
enabled: true,
|
||||
name: "greeting".to_string(),
|
||||
value: "hello".to_string(),
|
||||
id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
.expect("seed environment");
|
||||
|
||||
let rendered = cmd_render_template(
|
||||
host.clone(),
|
||||
CmdRenderTemplateReq {
|
||||
template: "${[ greeting ]} world".to_string(),
|
||||
workspace_id: id.clone(),
|
||||
environment_id: Some(environment.id.clone()),
|
||||
purpose: None,
|
||||
ignore_error: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("render");
|
||||
assert_eq!(rendered, "hello world", "the environment chain should have been resolved");
|
||||
|
||||
// The delete path too, since it is the one that used to reach for a
|
||||
// blocking thread this host does not have.
|
||||
let workspace = host.db().get_workspace(&id).expect("get workspace");
|
||||
@@ -438,64 +235,3 @@ async fn a_single_threaded_host_can_implement_the_trait() {
|
||||
.expect("delete");
|
||||
assert_eq!(deleted, id);
|
||||
}
|
||||
|
||||
/// Auth form values may contain templates, and a plugin must never see one
|
||||
/// unrendered. The rendering happens in the shared handler, so this checks the
|
||||
/// host received a resolved value rather than `${[ ... ]}`.
|
||||
#[tokio::test]
|
||||
async fn auth_values_are_rendered_before_the_host_sees_them() {
|
||||
let TestHost { inner } = TestHost::new();
|
||||
let host = SingleThreadedHost {
|
||||
inner: Rc::new(Arc::into_inner(inner).expect("sole owner")),
|
||||
auth_values: Rc::new(RefCell::new(None)),
|
||||
};
|
||||
|
||||
let workspace = host
|
||||
.db()
|
||||
.upsert_workspace(
|
||||
&Workspace { name: "Auth".to_string(), ..Default::default() },
|
||||
&host.update_source(),
|
||||
)
|
||||
.expect("workspace");
|
||||
host.db()
|
||||
.upsert_environment(
|
||||
&Environment {
|
||||
workspace_id: workspace.id.clone(),
|
||||
name: "Env".to_string(),
|
||||
variables: vec![EnvironmentVariable {
|
||||
enabled: true,
|
||||
name: "token".to_string(),
|
||||
value: "s3cret".to_string(),
|
||||
id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&host.update_source(),
|
||||
)
|
||||
.expect("environment");
|
||||
let environment =
|
||||
host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0);
|
||||
|
||||
let mut values = HashMap::new();
|
||||
values.insert("password".to_string(), JsonPrimitive::String("${[ token ]}".to_string()));
|
||||
|
||||
// The host refuses the call itself — it has no plugins — but only after the
|
||||
// handler has rendered and handed over the values, which is what matters.
|
||||
let _ = cmd_get_http_authentication_config(
|
||||
host.clone(),
|
||||
yaak_rpc_schema::CmdGetHttpAuthenticationConfigReq {
|
||||
auth_name: "basic".to_string(),
|
||||
values,
|
||||
model: AnyModel::Workspace(workspace),
|
||||
environment_id: Some(environment.id),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let seen = host.auth_values.borrow().clone().expect("the host should have been called");
|
||||
assert!(
|
||||
matches!(seen.get("password"), Some(JsonPrimitive::String(v)) if v == "s3cret"),
|
||||
"the template should have been rendered before reaching the host, got {:?}",
|
||||
seen.get("password"),
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -225,6 +225,7 @@ export type HttpResponse = {
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
|
||||
@@ -1677,13 +1677,6 @@ pub struct HttpResponse {
|
||||
pub workspace_id: String,
|
||||
pub request_id: String,
|
||||
|
||||
/// Where the engine put the body, when it puts it in a file.
|
||||
///
|
||||
/// Not exported to TypeScript: a path is only meaningful to a host that
|
||||
/// has the filesystem it names, and bodies are moving off it. Read a body
|
||||
/// by response id instead — the frontend through
|
||||
/// `cmd_http_response_body_path`, plugins through `ctx.httpResponse.body`.
|
||||
#[ts(skip)]
|
||||
pub body_path: Option<String>,
|
||||
pub content_length: Option<i32>,
|
||||
pub content_length_compressed: Option<i32>,
|
||||
|
||||
+1
-7
@@ -426,16 +426,10 @@ export type GetHttpResponseBodyInfoRequest = { responseId: string, };
|
||||
|
||||
export type GetHttpResponseBodyInfoResponse = {
|
||||
/**
|
||||
* How many bytes are stored right now, which is not necessarily what the
|
||||
* How many bytes are actually stored, which is not necessarily what the
|
||||
* `Content-Length` header claimed. Zero when the response has no body.
|
||||
*/
|
||||
contentLength: number,
|
||||
/**
|
||||
* Whether the response has finished arriving. While it has not, the body
|
||||
* keeps growing past `content_length`, and a reader that wants all of it
|
||||
* asks again.
|
||||
*/
|
||||
complete: boolean,
|
||||
/**
|
||||
* The response's `Content-Type` header, verbatim, so the reader can pick a
|
||||
* charset.
|
||||
|
||||
+1
@@ -224,6 +224,7 @@ export type HttpResponse = {
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
|
||||
@@ -1443,16 +1443,11 @@ pub struct GetHttpResponseBodyInfoRequest {
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct GetHttpResponseBodyInfoResponse {
|
||||
/// How many bytes are stored right now, which is not necessarily what the
|
||||
/// How many bytes are actually stored, which is not necessarily what the
|
||||
/// `Content-Length` header claimed. Zero when the response has no body.
|
||||
#[ts(type = "number")]
|
||||
pub content_length: u64,
|
||||
|
||||
/// Whether the response has finished arriving. While it has not, the body
|
||||
/// keeps growing past `content_length`, and a reader that wants all of it
|
||||
/// asks again.
|
||||
pub complete: bool,
|
||||
|
||||
/// The response's `Content-Type` header, verbatim, so the reader can pick a
|
||||
/// charset.
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
Vendored
+3
-18
@@ -29,23 +29,8 @@ export function boot(): Promise<void>;
|
||||
* Run one command as `label` (the calling tab's identity, which stands in for
|
||||
* the desktop's window label on every write it makes).
|
||||
*
|
||||
* The payload shapes match the `Cmd*Req` types in `yaak-rpc-schema`, but are
|
||||
* declared locally and dispatched by name, which is the one place this host
|
||||
* does not share the desktop's guarantees: the desktop builds its router from
|
||||
* the schema, so every command has a handler by construction. Here a renamed
|
||||
* command would surface as a runtime "not a command this host answers".
|
||||
*
|
||||
* The fix is `yaak-commands` (the `Host` trait), not more machinery here —
|
||||
* its `models::*` handlers are already this file, typed. Three things have to
|
||||
* give before a wasm host can register them:
|
||||
*
|
||||
* 1. `Host: Send + Sync`, which a browser cannot satisfy: there is one thread
|
||||
* and the connection pool is an `Rc`.
|
||||
* 2. `models_delete` reaches for `spawn_blocking`; there is nothing to spawn
|
||||
* onto here.
|
||||
* 3. `yaak-commands` depends on `yaak` and `yaak-plugins`, which pull the HTTP
|
||||
* stack and the Node sidecar and do not build for wasm32.
|
||||
*
|
||||
* None of those are hard; they are just not this PR.
|
||||
* The payload shapes match the `Cmd*Req` types in `yaak-rpc-schema` — that
|
||||
* crate itself pulls the git, gRPC and plugin crates for their response types
|
||||
* and cannot come to wasm, so the handful needed here are declared locally.
|
||||
*/
|
||||
export function rpc(cmd: string, payload: any, label: string): any;
|
||||
|
||||
@@ -66,24 +66,9 @@ export function boot() {
|
||||
* Run one command as `label` (the calling tab's identity, which stands in for
|
||||
* the desktop's window label on every write it makes).
|
||||
*
|
||||
* The payload shapes match the `Cmd*Req` types in `yaak-rpc-schema`, but are
|
||||
* declared locally and dispatched by name, which is the one place this host
|
||||
* does not share the desktop's guarantees: the desktop builds its router from
|
||||
* the schema, so every command has a handler by construction. Here a renamed
|
||||
* command would surface as a runtime "not a command this host answers".
|
||||
*
|
||||
* The fix is `yaak-commands` (the `Host` trait), not more machinery here —
|
||||
* its `models::*` handlers are already this file, typed. Three things have to
|
||||
* give before a wasm host can register them:
|
||||
*
|
||||
* 1. `Host: Send + Sync`, which a browser cannot satisfy: there is one thread
|
||||
* and the connection pool is an `Rc`.
|
||||
* 2. `models_delete` reaches for `spawn_blocking`; there is nothing to spawn
|
||||
* onto here.
|
||||
* 3. `yaak-commands` depends on `yaak` and `yaak-plugins`, which pull the HTTP
|
||||
* stack and the Node sidecar and do not build for wasm32.
|
||||
*
|
||||
* None of those are hard; they are just not this PR.
|
||||
* The payload shapes match the `Cmd*Req` types in `yaak-rpc-schema` — that
|
||||
* crate itself pulls the git, gRPC and plugin crates for their response types
|
||||
* and cannot come to wasm, so the handful needed here are declared locally.
|
||||
* @param {string} cmd
|
||||
* @param {any} payload
|
||||
* @param {string} label
|
||||
@@ -689,7 +674,7 @@ export function __wbindgen_cast_0000000000000002(arg0, arg1) {
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000003(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 180, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h7e53e249a4dc4aa9);
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha579407f9663b071);
|
||||
return ret;
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000004(arg0, arg1) {
|
||||
@@ -746,8 +731,8 @@ function wasm_bindgen__convert__closures_____invoke__hf84d53817e0238b4(arg0, arg
|
||||
}
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h7e53e249a4dc4aa9(arg0, arg1, arg2) {
|
||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h7e53e249a4dc4aa9(arg0, arg1, arg2);
|
||||
function wasm_bindgen__convert__closures_____invoke__ha579407f9663b071(arg0, arg1, arg2) {
|
||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__ha579407f9663b071(arg0, arg1, arg2);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -17,7 +17,7 @@ export const rust_sqlite_wasm_realloc: (a: number, b: number) => number;
|
||||
export const sqlite3_os_end: () => number;
|
||||
export const sqlite3_os_init: () => number;
|
||||
export const wasm_bindgen__convert__closures_____invoke__hf84d53817e0238b4: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h7e53e249a4dc4aa9: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha579407f9663b071: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h2c72ca09e851b7f3: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h7e06bb925b918fbb: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h87640adb2bbfa2fc: (a: number, b: number) => void;
|
||||
|
||||
@@ -306,7 +306,6 @@ fn build_shared_reply(
|
||||
GetHttpResponseBodyInfoResponse {
|
||||
content_length: info.content_length,
|
||||
content_type: info.content_type,
|
||||
complete: info.complete,
|
||||
},
|
||||
),
|
||||
Err(err) => InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||
@@ -646,7 +645,6 @@ mod tests {
|
||||
Ok(ResponseBodyInfo {
|
||||
content_length: self.body.len() as u64,
|
||||
content_type: Some("text/plain; charset=utf-8".to_string()),
|
||||
complete: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
use crate::error::Result;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use yaak_models::models::HttpResponseState;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
|
||||
/// The most bytes one read will hand back, however much was asked for.
|
||||
@@ -29,9 +28,6 @@ pub struct ResponseBodyInfo {
|
||||
pub content_length: u64,
|
||||
/// The response's `Content-Type` header, verbatim.
|
||||
pub content_type: Option<String>,
|
||||
/// Whether the response has finished arriving, so `content_length` is
|
||||
/// final. A body still being written grows past it.
|
||||
pub complete: bool,
|
||||
}
|
||||
|
||||
/// Somewhere response bodies can be read from, a window at a time.
|
||||
@@ -82,12 +78,7 @@ impl ResponseBodyStore for FileResponseBodyStore<'_> {
|
||||
None => 0,
|
||||
};
|
||||
|
||||
Ok(ResponseBodyInfo {
|
||||
content_length,
|
||||
content_type,
|
||||
// Closed is the one terminal state: success, error, and cancel all end there.
|
||||
complete: matches!(response.state, HttpResponseState::Closed),
|
||||
})
|
||||
Ok(ResponseBodyInfo { content_length, content_type })
|
||||
}
|
||||
|
||||
fn read_chunk(&self, response_id: &str, offset: u64, length: u64) -> Result<Vec<u8>> {
|
||||
@@ -201,19 +192,6 @@ mod tests {
|
||||
assert!(store.read_chunk(&id, 0, 100).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_tracks_whether_the_response_has_closed() {
|
||||
let (qm, _tmp, id) = seed(Some(b"partial"));
|
||||
// Seeded responses default to Initialized: still arriving.
|
||||
assert!(!FileResponseBodyStore::new(&qm).info(&id).unwrap().complete);
|
||||
|
||||
let mut response = qm.connect().get_http_response(&id).unwrap();
|
||||
response.state = HttpResponseState::Closed;
|
||||
qm.connect().update_http_response_if_id(&response, &UpdateSource::Sync).unwrap();
|
||||
|
||||
assert!(FileResponseBodyStore::new(&qm).info(&id).unwrap().complete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_response_fails() {
|
||||
let (qm, _tmp, _id) = seed(Some(b"hi"));
|
||||
|
||||
Generated
+4099
-2665
File diff suppressed because it is too large
Load Diff
+6
-8
@@ -119,23 +119,21 @@
|
||||
"@tauri-apps/cli": "npm:@tauri-apps/cli-cef@3.0.0-alpha.6",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@yaakapp/cli": "0.5.1",
|
||||
"@yaakapp/cli": "latest",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.5.25",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tar": "^7.5.22",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.9",
|
||||
"vite-plus": "^0.2.9",
|
||||
"vitest": "^4.1.10",
|
||||
"yauzl": "^3.4.0"
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||
"vite-plus": "^0.2.1",
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"overrides": {
|
||||
"js-yaml": "^4.3.1",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.9"
|
||||
"js-yaml": "^4.1.1",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1"
|
||||
},
|
||||
"packageManager": "npm@11.11.1"
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ const ALL_CAPABILITIES: PlatformCapabilities = {
|
||||
localFiles: true,
|
||||
timeline: true,
|
||||
multiWindow: true,
|
||||
windowChrome: true,
|
||||
plugins: true,
|
||||
encryption: true,
|
||||
updater: true,
|
||||
|
||||
@@ -255,13 +255,6 @@ export interface PlatformCapabilities {
|
||||
timeline: boolean;
|
||||
/** More than one window or tab on the same data. */
|
||||
multiWindow: boolean;
|
||||
/**
|
||||
* The page is the window's titlebar: it draws the drag region and window
|
||||
* controls, and leaves room for macOS traffic lights. False when something
|
||||
* else owns the frame around the page (a browser tab), so none of that
|
||||
* chrome should be reserved or drawn.
|
||||
*/
|
||||
windowChrome: boolean;
|
||||
/** The plugin runtime. */
|
||||
plugins: boolean;
|
||||
/** Workspace encryption backed by a key the host keeps. */
|
||||
|
||||
@@ -129,16 +129,13 @@ Reported honestly, so callers gate on the question rather than on the host:
|
||||
|
||||
| True | False |
|
||||
| --- | --- |
|
||||
| `cookieJar` (the jar stores and edits here; only filling it needs the sender) | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `timeline`, `multiWindow`, `windowChrome`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` |
|
||||
| `cookieJar` (the jar stores and edits here; only filling it needs the sender) | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `timeline`, `multiWindow`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` |
|
||||
|
||||
`multiWindow: false` means the host cannot open a *second window* on demand —
|
||||
what `cmd_new_child_window` does for Settings and workspace switching. It is not
|
||||
a claim that nothing else is looking: other tabs may well be open on the same
|
||||
worker, and it pushes every write to all of them regardless.
|
||||
|
||||
`windowChrome: false` means the browser owns the frame around the page, so the
|
||||
header draws no window controls and reserves no room for macOS traffic lights.
|
||||
|
||||
## Multiple tabs
|
||||
|
||||
Each tab mints a label at load (`tab_xxxxxxxx`) and sends it with every command;
|
||||
|
||||
@@ -49,9 +49,6 @@ function capabilitiesFor(): PlatformCapabilities {
|
||||
// else is looking: other tabs may well be open on the same worker, and it
|
||||
// pushes every write to all of them regardless of this flag.
|
||||
multiWindow: false,
|
||||
// The browser draws the frame around the page. There are no traffic lights
|
||||
// to leave room for and no window controls to draw.
|
||||
windowChrome: false,
|
||||
plugins: false,
|
||||
encryption: false,
|
||||
updater: false,
|
||||
|
||||
+1
-7
@@ -426,16 +426,10 @@ export type GetHttpResponseBodyInfoRequest = { responseId: string, };
|
||||
|
||||
export type GetHttpResponseBodyInfoResponse = {
|
||||
/**
|
||||
* How many bytes are stored right now, which is not necessarily what the
|
||||
* How many bytes are actually stored, which is not necessarily what the
|
||||
* `Content-Length` header claimed. Zero when the response has no body.
|
||||
*/
|
||||
contentLength: number,
|
||||
/**
|
||||
* Whether the response has finished arriving. While it has not, the body
|
||||
* keeps growing past `content_length`, and a reader that wants all of it
|
||||
* asks again.
|
||||
*/
|
||||
complete: boolean,
|
||||
/**
|
||||
* The response's `Content-Type` header, verbatim, so the reader can pick a
|
||||
* charset.
|
||||
|
||||
@@ -224,6 +224,7 @@ export type HttpResponse = {
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
requestId: string;
|
||||
bodyPath: string | null;
|
||||
contentLength: number | null;
|
||||
contentLengthCompressed: number | null;
|
||||
elapsed: number;
|
||||
|
||||
@@ -79,21 +79,17 @@ export interface ReadHttpResponseBodyOptions {
|
||||
/**
|
||||
* A response body, read back from wherever the host stored it.
|
||||
*
|
||||
* The accessors are named after `fetch`'s and behave the same way against a
|
||||
* response that is still arriving: they wait for the rest, and one that never
|
||||
* finishes is never finished reading — `chunks()` is the way to consume that.
|
||||
* Unlike `fetch`, the body is not used up by reading it: the bytes are in
|
||||
* durable storage, so every accessor can be called as many times as you like,
|
||||
* in any order.
|
||||
* The accessors are named after `fetch`'s, but unlike `fetch` the body is not
|
||||
* used up by reading it: these bytes are in durable storage, so every accessor
|
||||
* can be called as many times as you like, in any order.
|
||||
*/
|
||||
export interface HttpResponseBody {
|
||||
/** The response these bytes belong to. */
|
||||
readonly responseId: string;
|
||||
|
||||
/**
|
||||
* How many bytes were stored when this body was opened, which is not
|
||||
* necessarily what the `Content-Length` header claimed. Zero when the
|
||||
* response has no body. Final only if `complete`.
|
||||
* How many bytes are stored, which is not necessarily what the
|
||||
* `Content-Length` header claimed. Zero when the response has no body.
|
||||
*/
|
||||
readonly contentLength: number;
|
||||
|
||||
@@ -101,30 +97,20 @@ export interface HttpResponseBody {
|
||||
readonly contentType: string | null;
|
||||
|
||||
/**
|
||||
* Whether the response had finished arriving when this body was opened.
|
||||
* When false, the accessors below will wait for the rest of it.
|
||||
*/
|
||||
readonly complete: boolean;
|
||||
|
||||
/**
|
||||
* The whole body decoded to a string, using the charset from `contentType`
|
||||
* and falling back to UTF-8. Waits for a response still arriving. Throws
|
||||
* once more than `maxBytes` has been read.
|
||||
* The body decoded to a string, using the charset from `contentType` and
|
||||
* falling back to UTF-8. Throws if the body is over `maxBytes`.
|
||||
*/
|
||||
text(options?: ReadHttpResponseBodyOptions): Promise<string>;
|
||||
|
||||
/** `text()`, parsed as JSON. */
|
||||
json<T = unknown>(options?: ReadHttpResponseBodyOptions): Promise<T>;
|
||||
|
||||
/** The whole body as raw bytes. Waits and throws as `text()` does. */
|
||||
/** The raw bytes. Throws if the body is over `maxBytes`. */
|
||||
arrayBuffer(options?: ReadHttpResponseBodyOptions): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* The raw bytes, a chunk at a time, so a body of any size can be read
|
||||
* without holding all of it at once. Not subject to `maxBytes`.
|
||||
*
|
||||
* Follows a response that is still arriving, yielding as it comes, and ends
|
||||
* when the response does. Break out of the loop to stop early.
|
||||
*/
|
||||
chunks(options?: Pick<ReadHttpResponseBodyOptions, "chunkSize">): AsyncIterable<Uint8Array>;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.13",
|
||||
"@types/ws": "^8.5.13",
|
||||
"esbuild": "^0.28.0"
|
||||
"@types/ws": "^8.5.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
HttpAuthenticationAction,
|
||||
HttpRequest,
|
||||
HttpRequestAction,
|
||||
HttpResponse,
|
||||
ImportResources,
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
@@ -54,21 +53,6 @@ import { EventChannel } from "./EventChannel";
|
||||
import { migrateTemplateFunctionSelectOptions } from "./migrations";
|
||||
import { createResponseBody, decodeBase64Chunk } from "./responseBody";
|
||||
|
||||
/**
|
||||
* A response as a plugin should see it.
|
||||
*
|
||||
* The host still puts `bodyPath` on the wire for its own callers, but it names
|
||||
* a file on the host's disk — meaningless to a plugin, absent once bodies move
|
||||
* off the filesystem, and impossible in a browser. Plugins address bodies by
|
||||
* response id, so drop it here rather than let one grow a dependency on it.
|
||||
*/
|
||||
function forPlugin(httpResponse: HttpResponse): HttpResponse {
|
||||
const { bodyPath: _bodyPath, ...rest } = httpResponse as HttpResponse & {
|
||||
bodyPath?: string | null;
|
||||
};
|
||||
return rest;
|
||||
}
|
||||
|
||||
export interface PluginWorkerData {
|
||||
bootRequest: BootRequest;
|
||||
pluginRefId: string;
|
||||
@@ -571,17 +555,13 @@ export class PluginInstance {
|
||||
return this.#sendPayload(context, { type: "empty_response" }, replyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the host and wait for its reply.
|
||||
*
|
||||
* A host that cannot answer replies with an error, which becomes a thrown
|
||||
* error here. The alternative is handing back a reply-shaped object with
|
||||
* none of the fields the caller destructures, and letting it fail somewhere
|
||||
* further along with no idea why.
|
||||
*/
|
||||
#sendForReply<T extends Omit<InternalEventPayload, "type">>(
|
||||
context: PluginContext,
|
||||
payload: InternalEventPayload,
|
||||
// Off by default because a reply-shaped object with none of the expected
|
||||
// fields is what every existing caller already copes with; turning it on
|
||||
// for a new call is how that stops spreading.
|
||||
{ throwOnError = false }: { throwOnError?: boolean } = {},
|
||||
): Promise<T> {
|
||||
// 1. Build event to send
|
||||
const eventToSend = this.#buildEventToSend(context, payload, null);
|
||||
@@ -592,9 +572,8 @@ export class PluginInstance {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
this.#appToPluginEvents.unlisten(cb); // Unlisten, now that we're done
|
||||
const { type: _, ...payload } = event.payload;
|
||||
if (event.payload.type === "error_response") {
|
||||
const { error } = payload as { error?: string };
|
||||
reject(new Error(error || `Host failed to handle ${eventToSend.payload.type}`));
|
||||
if (throwOnError && event.payload.type === "error_response") {
|
||||
reject(new Error(String((payload as { error?: string }).error ?? "Unknown error")));
|
||||
return;
|
||||
}
|
||||
resolve(payload as T);
|
||||
@@ -630,30 +609,24 @@ export class PluginInstance {
|
||||
}
|
||||
|
||||
#newCtx(context: PluginContext): Context {
|
||||
/** Read a body the host has stored, a chunk at a time, following it if it is still arriving. */
|
||||
/** Read a body the host has stored, a chunk at a time. */
|
||||
const storedBody = async (responseId: string) => {
|
||||
const bodyInfo = () =>
|
||||
this.#sendForReply<GetHttpResponseBodyInfoResponse>(context, {
|
||||
type: "get_http_response_body_info_request",
|
||||
responseId,
|
||||
});
|
||||
const info = await bodyInfo();
|
||||
const info = await this.#sendForReply<GetHttpResponseBodyInfoResponse>(
|
||||
context,
|
||||
{ type: "get_http_response_body_info_request", responseId },
|
||||
{ throwOnError: true },
|
||||
);
|
||||
|
||||
return createResponseBody(
|
||||
{
|
||||
responseId,
|
||||
contentLength: info.contentLength,
|
||||
contentType: info.contentType ?? null,
|
||||
complete: info.complete,
|
||||
},
|
||||
{ responseId, contentLength: info.contentLength, contentType: info.contentType ?? null },
|
||||
async (offset, length) => {
|
||||
const chunk = await this.#sendForReply<ReadHttpResponseBodyChunkResponse>(
|
||||
context,
|
||||
{ type: "read_http_response_body_chunk_request", responseId, offset, length },
|
||||
{ throwOnError: true },
|
||||
);
|
||||
return decodeBase64Chunk(chunk.data);
|
||||
},
|
||||
{ refresh: bodyInfo },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -808,7 +781,7 @@ export class PluginInstance {
|
||||
context,
|
||||
payload,
|
||||
);
|
||||
return httpResponses.map(forPlugin);
|
||||
return httpResponses;
|
||||
},
|
||||
body: ({ responseId }) => storedBody(responseId),
|
||||
},
|
||||
@@ -845,18 +818,21 @@ export class PluginInstance {
|
||||
const { httpResponse, body } = await this.#sendForReply<SendHttpRequestResponse>(
|
||||
context,
|
||||
payload,
|
||||
// A failed send has no response to hand back, and reading `.body`
|
||||
// off nothing would bury the host's reason for failing.
|
||||
{ throwOnError: true },
|
||||
);
|
||||
|
||||
// A send with no request behind it saves nothing, so the reply
|
||||
// carries the only copy of its body. A saved one is read back from
|
||||
// the host like any other. Callers get the same thing either way.
|
||||
if (body == null) {
|
||||
return { httpResponse: forPlugin(httpResponse), body: await storedBody(httpResponse.id) };
|
||||
return { httpResponse, body: await storedBody(httpResponse.id) };
|
||||
}
|
||||
|
||||
const bytes = decodeBase64Chunk(body);
|
||||
return {
|
||||
httpResponse: forPlugin(httpResponse),
|
||||
httpResponse,
|
||||
body: createResponseBody(
|
||||
{
|
||||
responseId: httpResponse.id,
|
||||
@@ -864,8 +840,6 @@ export class PluginInstance {
|
||||
contentType:
|
||||
httpResponse.headers.find((h) => h.name.toLowerCase() === "content-type")
|
||||
?.value ?? null,
|
||||
// The host waited for the whole send before replying.
|
||||
complete: true,
|
||||
},
|
||||
async (offset, length) => bytes.slice(offset, offset + length),
|
||||
),
|
||||
|
||||
@@ -13,9 +13,6 @@ const DEFAULT_CHUNK_SIZE = 1024 * 1024;
|
||||
*/
|
||||
const DEFAULT_MAX_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
/** How long to wait, having caught up with a body still arriving, before looking again. */
|
||||
const DEFAULT_POLL_INTERVAL_MS = 100;
|
||||
|
||||
/** Fetch one window of body bytes from the host. */
|
||||
export type ReadResponseBodyChunk = (offset: number, length: number) => Promise<Uint8Array>;
|
||||
|
||||
@@ -23,65 +20,26 @@ export interface ResponseBodyInfo {
|
||||
responseId: string;
|
||||
contentLength: number;
|
||||
contentType: string | null;
|
||||
/** Whether the response has finished arriving, so `contentLength` is final. */
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
/** What can change while a body is still arriving. */
|
||||
export type ResponseBodyProgress = Pick<ResponseBodyInfo, "contentLength" | "complete">;
|
||||
|
||||
export interface CreateResponseBodyOptions {
|
||||
/**
|
||||
* Ask the host where the body has got to. Needed only for a body that was
|
||||
* not complete when opened; a reader that has caught up calls this to learn
|
||||
* whether to wait for more or stop.
|
||||
*/
|
||||
refresh?: () => Promise<ResponseBodyProgress>;
|
||||
pollIntervalMs?: number;
|
||||
}
|
||||
|
||||
export function createResponseBody(
|
||||
info: ResponseBodyInfo,
|
||||
readChunk: ReadResponseBodyChunk,
|
||||
{ refresh, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS }: CreateResponseBodyOptions = {},
|
||||
): HttpResponseBody {
|
||||
const { responseId, contentLength, contentType, complete } = info;
|
||||
const { responseId, contentLength, contentType } = info;
|
||||
|
||||
/**
|
||||
* Yield the body from the start until it has all arrived.
|
||||
*
|
||||
* A complete body is read up to its known length and no further. One still
|
||||
* arriving is followed: on catching up, ask the host whether it has finished,
|
||||
* and if not, wait and look again. So this ends when the response does —
|
||||
* which for a stream that never closes means it doesn't, exactly as
|
||||
* iterating `fetch`'s body would not.
|
||||
*/
|
||||
async function* chunks(
|
||||
options?: Pick<ReadHttpResponseBodyOptions, "chunkSize">,
|
||||
): AsyncIterable<Uint8Array> {
|
||||
const chunkSize = Math.max(1, Math.floor(options?.chunkSize ?? DEFAULT_CHUNK_SIZE));
|
||||
let known = contentLength;
|
||||
let done = complete;
|
||||
let offset = 0;
|
||||
|
||||
while (true) {
|
||||
if (done && offset >= known) return;
|
||||
|
||||
const want = done ? Math.min(chunkSize, known - offset) : chunkSize;
|
||||
const chunk = await readChunk(offset, want);
|
||||
if (chunk.byteLength > 0) {
|
||||
yield chunk;
|
||||
offset += chunk.byteLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Caught up. A complete body that came up short simply ended sooner than
|
||||
// the host said; one still arriving needs asking about.
|
||||
if (done || refresh == null) return;
|
||||
({ contentLength: known, complete: done } = await refresh());
|
||||
if (offset < known) continue;
|
||||
if (done) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
// Bounded by the length the host reported, but a short read still ends it:
|
||||
// the body may have been rewritten between the two calls.
|
||||
while (offset < contentLength) {
|
||||
const chunk = await readChunk(offset, Math.min(chunkSize, contentLength - offset));
|
||||
if (chunk.byteLength === 0) return;
|
||||
yield chunk;
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +70,6 @@ export function createResponseBody(
|
||||
responseId,
|
||||
contentLength,
|
||||
contentType,
|
||||
complete,
|
||||
chunks,
|
||||
async arrayBuffer(options) {
|
||||
const bytes = await readAll("arrayBuffer", options);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { createResponseBody, decodeBase64Chunk } from "../src/responseBody";
|
||||
|
||||
/** A finished body, in a store that records every window it was asked for. */
|
||||
/** A store of bytes that records every window it was asked for. */
|
||||
function fakeBody(bytes: Uint8Array, contentType: string | null) {
|
||||
const reads: Array<[number, number]> = [];
|
||||
const body = createResponseBody(
|
||||
{ responseId: "rs_test", contentLength: bytes.byteLength, contentType, complete: true },
|
||||
{ responseId: "rs_test", contentLength: bytes.byteLength, contentType },
|
||||
async (offset, length) => {
|
||||
reads.push([offset, length]);
|
||||
return bytes.slice(offset, offset + length);
|
||||
@@ -14,39 +14,6 @@ function fakeBody(bytes: Uint8Array, contentType: string | null) {
|
||||
return { body, reads };
|
||||
}
|
||||
|
||||
/**
|
||||
* A body still arriving: it grows by one script step every time the reader
|
||||
* asks the host where it has got to, and completes on the last step.
|
||||
*/
|
||||
function streamingBody(steps: string[], contentType = "text/plain") {
|
||||
let stored = new Uint8Array();
|
||||
let step = 0;
|
||||
let refreshes = 0;
|
||||
const advance = () => {
|
||||
if (step < steps.length) {
|
||||
const next = utf8(steps[step]!);
|
||||
const grown = new Uint8Array(stored.byteLength + next.byteLength);
|
||||
grown.set(stored);
|
||||
grown.set(next, stored.byteLength);
|
||||
stored = grown;
|
||||
step++;
|
||||
}
|
||||
return { contentLength: stored.byteLength, complete: step >= steps.length };
|
||||
};
|
||||
const body = createResponseBody(
|
||||
{ responseId: "rs_live", contentLength: 0, contentType, complete: false },
|
||||
async (offset, length) => stored.slice(offset, offset + length),
|
||||
{
|
||||
refresh: async () => {
|
||||
refreshes++;
|
||||
return advance();
|
||||
},
|
||||
pollIntervalMs: 1,
|
||||
},
|
||||
);
|
||||
return { body, refreshCount: () => refreshes };
|
||||
}
|
||||
|
||||
function utf8(text: string) {
|
||||
return new TextEncoder().encode(text);
|
||||
}
|
||||
@@ -119,7 +86,7 @@ describe("response body", () => {
|
||||
test("stops early when the host runs out of bytes sooner than it claimed", async () => {
|
||||
// contentLength says 100; the store only ever hands back 10.
|
||||
const body = createResponseBody(
|
||||
{ responseId: "rs_test", contentLength: 100, contentType: "text/plain", complete: true },
|
||||
{ responseId: "rs_test", contentLength: 100, contentType: "text/plain" },
|
||||
async (offset) => (offset === 0 ? utf8("0123456789") : new Uint8Array()),
|
||||
);
|
||||
|
||||
@@ -142,54 +109,6 @@ describe("response body", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a response still arriving", () => {
|
||||
test("chunks() follows it until it finishes", async () => {
|
||||
const { body, refreshCount } = streamingBody(["data: 1\n", "data: 2\n", "data: 3\n"]);
|
||||
expect(body.complete).toBe(false);
|
||||
|
||||
const seen: string[] = [];
|
||||
for await (const chunk of body.chunks()) {
|
||||
seen.push(new TextDecoder().decode(chunk));
|
||||
}
|
||||
|
||||
expect(seen.join("")).toEqual("data: 1\ndata: 2\ndata: 3\n");
|
||||
// Asked once per catch-up, and stopped as soon as the host said it was done.
|
||||
expect(refreshCount()).toEqual(3);
|
||||
});
|
||||
|
||||
test("text() waits for the rest rather than returning a prefix", async () => {
|
||||
const { body } = streamingBody(['{"token":', '"abc"}']);
|
||||
expect(await body.json()).toEqual({ token: "abc" });
|
||||
});
|
||||
|
||||
test("keeps waiting through a stretch with nothing new", async () => {
|
||||
// Two refreshes report no growth before the body finally moves.
|
||||
const { body } = streamingBody(["", "", "late"]);
|
||||
expect(await body.text()).toEqual("late");
|
||||
});
|
||||
|
||||
test("still refuses to buffer past maxBytes as it streams", async () => {
|
||||
const { body } = streamingBody(["x".repeat(40), "x".repeat(40), "x".repeat(40)]);
|
||||
await expect(body.text({ maxBytes: 100 })).rejects.toThrow(/chunks\(\)/);
|
||||
});
|
||||
|
||||
test("a finished body never asks the host again", async () => {
|
||||
let refreshes = 0;
|
||||
const body = createResponseBody(
|
||||
{ responseId: "rs_done", contentLength: 5, contentType: null, complete: true },
|
||||
async (offset, length) => utf8("hello").slice(offset, offset + length),
|
||||
{
|
||||
refresh: async () => {
|
||||
refreshes++;
|
||||
return { contentLength: 5, complete: true };
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(await body.text()).toEqual("hello");
|
||||
expect(refreshes).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeBase64Chunk", () => {
|
||||
test("round-trips arbitrary bytes", () => {
|
||||
const bytes = new Uint8Array([0, 1, 127, 128, 254, 255]);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useCapability } from "@yaakapp-internal/platform";
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -32,10 +31,6 @@ export function HeaderSize({
|
||||
interfaceScale,
|
||||
}: HeaderSizeProps) {
|
||||
const isFullscreen = useIsFullscreen();
|
||||
// The header only doubles as the titlebar when the host hands the page the
|
||||
// window frame (Tauri) and the user hasn't opted for the native one. In a
|
||||
// browser tab the frame is the browser's: no controls, no traffic lights.
|
||||
const drawsWindowChrome = useCapability("windowChrome") && !useNativeTitlebar;
|
||||
const finalStyle = useMemo<CSSProperties>(() => {
|
||||
const s = { ...style };
|
||||
|
||||
@@ -43,8 +38,8 @@ export function HeaderSize({
|
||||
if (size === "md") s.minHeight = HEADER_SIZE_MD;
|
||||
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
|
||||
|
||||
if (!drawsWindowChrome) {
|
||||
// No style updates when something else draws the titlebar
|
||||
if (useNativeTitlebar) {
|
||||
// No style updates when using native titlebar
|
||||
} else if (osType === "macos") {
|
||||
if (!isFullscreen) {
|
||||
// Add large padding for window controls
|
||||
@@ -62,7 +57,7 @@ export function HeaderSize({
|
||||
interfaceScale,
|
||||
size,
|
||||
style,
|
||||
drawsWindowChrome,
|
||||
useNativeTitlebar,
|
||||
osType,
|
||||
]);
|
||||
|
||||
@@ -87,7 +82,7 @@ export function HeaderSize({
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!hideControls && drawsWindowChrome && (
|
||||
{!hideControls && !useNativeTitlebar && (
|
||||
<WindowControls
|
||||
onlyX={onlyXWindowControl}
|
||||
osType={osType}
|
||||
|
||||
@@ -24,12 +24,10 @@ describe("auth-ntlm", () => {
|
||||
|
||||
test("uses NTLM challenge when Negotiate and NTLM headers are separate", async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
httpResponse: {
|
||||
headers: [
|
||||
{ name: "WWW-Authenticate", value: "Negotiate" },
|
||||
{ name: "WWW-Authenticate", value: "NTLM TlRMTVNTUAACAAAAAA==" },
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{ name: "WWW-Authenticate", value: "Negotiate" },
|
||||
{ name: "WWW-Authenticate", value: "NTLM TlRMTVNTUAACAAAAAA==" },
|
||||
],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
@@ -50,9 +48,7 @@ describe("auth-ntlm", () => {
|
||||
|
||||
test("uses NTLM challenge when auth schemes are comma-separated in one header", async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
httpResponse: {
|
||||
headers: [{ name: "www-authenticate", value: "Negotiate, NTLM TlRMTVNTUAACAAAAAA==" }],
|
||||
},
|
||||
headers: [{ name: "www-authenticate", value: "Negotiate, NTLM TlRMTVNTUAACAAAAAA==" }],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
@@ -72,7 +68,7 @@ describe("auth-ntlm", () => {
|
||||
|
||||
test("throws a clear error when NTLM challenge is missing", async () => {
|
||||
const send = vi.fn().mockResolvedValue({
|
||||
httpResponse: { headers: [{ name: "WWW-Authenticate", value: "Negotiate" }] },
|
||||
headers: [{ name: "WWW-Authenticate", value: "Negotiate" }],
|
||||
});
|
||||
const ctx = { httpRequest: { send } } as unknown as Context;
|
||||
|
||||
|
||||
@@ -64,9 +64,7 @@ export async function fetchAccessToken(
|
||||
throw new Error(`Failed to fetch access token: ${resp.error}`);
|
||||
}
|
||||
|
||||
// A token request is sent ad-hoc, with no id, so nothing saves the response
|
||||
// and this body is the only copy of it. An empty one parses to {} below,
|
||||
// which is what reading a missing file used to give.
|
||||
// Empty when the response had no body, which parses to {} below.
|
||||
const body = await responseBody.text();
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
|
||||
@@ -84,8 +84,7 @@ export async function getOrRefreshAccessToken(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sent ad-hoc, so this body came back with the response rather than being
|
||||
// saved anywhere to read later.
|
||||
// Empty when the response had no body, which parses to {} below.
|
||||
const body = await responseBody.text();
|
||||
|
||||
console.log("[oauth2] Got refresh token response", resp.status);
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"test": "vp test --run tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"openapi-to-postmanv2": "^5.8.0",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/openapi-to-postmanv2": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
# Real-World OpenAPI Fixtures
|
||||
|
||||
These fixtures were copied from the public APIs.guru OpenAPI directory:
|
||||
|
||||
- `apis-guru.yaml`: https://api.apis.guru/v2/specs/apis.guru/2.2.0/openapi.yaml
|
||||
- `httpbin.yaml`: https://api.apis.guru/v2/specs/httpbin.org/0.9.2/openapi.yaml
|
||||
- `nasa-apod.yaml`: https://api.apis.guru/v2/specs/nasa.gov/apod/1.0.0/openapi.yaml
|
||||
- `xkcd.yaml`: https://api.apis.guru/v2/specs/xkcd.com/1.0.0/openapi.yaml
|
||||
@@ -1,399 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: https://api.apis.guru/v2
|
||||
info:
|
||||
contact:
|
||||
email: mike.ralphson@gmail.com
|
||||
name: APIs.guru
|
||||
url: https://APIs.guru
|
||||
description: |
|
||||
Wikipedia for Web APIs. Repository of API definitions in OpenAPI format.
|
||||
**Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA).
|
||||
Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)
|
||||
license:
|
||||
name: CC0 1.0
|
||||
url: https://github.com/APIs-guru/openapi-directory#licenses
|
||||
title: APIs.guru
|
||||
version: 2.2.0
|
||||
x-apisguru-categories:
|
||||
- open_data
|
||||
- developer_tools
|
||||
x-logo:
|
||||
url: https://api.apis.guru/v2/cache/logo/https_apis.guru_branding_logo_vertical.svg
|
||||
x-origin:
|
||||
- format: openapi
|
||||
url: https://api.apis.guru/v2/openapi.yaml
|
||||
version: "3.0"
|
||||
x-providerName: apis.guru
|
||||
x-tags:
|
||||
- API
|
||||
- Catalog
|
||||
- Directory
|
||||
- REST
|
||||
- Swagger
|
||||
- OpenAPI
|
||||
externalDocs:
|
||||
url: https://github.com/APIs-guru/openapi-directory/blob/master/API.md
|
||||
security: []
|
||||
tags:
|
||||
- description: Actions relating to APIs in the collection
|
||||
name: APIs
|
||||
paths:
|
||||
/list.json:
|
||||
get:
|
||||
description: |
|
||||
List all APIs in the directory.
|
||||
Returns links to the OpenAPI definitions for each API in the directory.
|
||||
If API exist in multiple versions `preferred` one is explicitly marked.
|
||||
Some basic info from the OpenAPI definition is cached inside each object.
|
||||
This allows you to generate some simple views without needing to fetch the OpenAPI definition for each API.
|
||||
operationId: listAPIs
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/APIs"
|
||||
description: OK
|
||||
summary: List all APIs
|
||||
tags:
|
||||
- APIs
|
||||
/metrics.json:
|
||||
get:
|
||||
description: |
|
||||
Some basic metrics for the entire directory.
|
||||
Just stunning numbers to put on a front page and are intended purely for WoW effect :)
|
||||
operationId: getMetrics
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Metrics"
|
||||
description: OK
|
||||
summary: Get basic metrics
|
||||
tags:
|
||||
- APIs
|
||||
/providers.json:
|
||||
get:
|
||||
description: |
|
||||
List all the providers in the directory
|
||||
operationId: getProviders
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
minLength: 1
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
type: object
|
||||
description: OK
|
||||
summary: List all providers
|
||||
tags:
|
||||
- APIs
|
||||
"/specs/{provider}/{api}.json":
|
||||
get:
|
||||
description: Returns the API entry for one specific version of an API where there is no serviceName.
|
||||
operationId: getAPI
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/provider"
|
||||
- $ref: "#/components/parameters/api"
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/API"
|
||||
description: OK
|
||||
summary: Retrieve one version of a particular API
|
||||
tags:
|
||||
- APIs
|
||||
"/specs/{provider}/{service}/{api}.json":
|
||||
get:
|
||||
description: Returns the API entry for one specific version of an API where there is a serviceName.
|
||||
operationId: getServiceAPI
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/provider"
|
||||
- in: path
|
||||
name: service
|
||||
required: true
|
||||
schema:
|
||||
example: graph
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
- $ref: "#/components/parameters/api"
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/API"
|
||||
description: OK
|
||||
summary: Retrieve one version of a particular API with a serviceName.
|
||||
tags:
|
||||
- APIs
|
||||
"/{provider}.json":
|
||||
get:
|
||||
description: |
|
||||
List all APIs in the directory for a particular providerName
|
||||
Returns links to the individual API entry for each API.
|
||||
operationId: getProvider
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/provider"
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/APIs"
|
||||
description: OK
|
||||
summary: List all APIs for a particular provider
|
||||
tags:
|
||||
- APIs
|
||||
"/{provider}/services.json":
|
||||
get:
|
||||
description: |
|
||||
List all serviceNames in the directory for a particular providerName
|
||||
operationId: getServices
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/provider"
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
minLength: 0
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
type: object
|
||||
description: OK
|
||||
summary: List all serviceNames for a particular provider
|
||||
tags:
|
||||
- APIs
|
||||
components:
|
||||
parameters:
|
||||
api:
|
||||
in: path
|
||||
name: api
|
||||
required: true
|
||||
schema:
|
||||
example: 2.1.0
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
provider:
|
||||
in: path
|
||||
name: provider
|
||||
required: true
|
||||
schema:
|
||||
example: apis.guru
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
schemas:
|
||||
API:
|
||||
additionalProperties: false
|
||||
description: Meta information about API
|
||||
properties:
|
||||
added:
|
||||
description: Timestamp when the API was first added to the directory
|
||||
format: date-time
|
||||
type: string
|
||||
preferred:
|
||||
description: Recommended version
|
||||
type: string
|
||||
versions:
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/ApiVersion"
|
||||
description: List of supported versions of the API
|
||||
minProperties: 1
|
||||
type: object
|
||||
required:
|
||||
- added
|
||||
- preferred
|
||||
- versions
|
||||
type: object
|
||||
APIs:
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/API"
|
||||
description: |
|
||||
List of API details.
|
||||
It is a JSON object with API IDs(`<provider>[:<service>]`) as keys.
|
||||
example:
|
||||
googleapis.com:drive:
|
||||
added: 2015-02-22T20:00:45.000Z
|
||||
preferred: v3
|
||||
versions:
|
||||
v2:
|
||||
added: 2015-02-22T20:00:45.000Z
|
||||
info:
|
||||
title: Drive
|
||||
version: v2
|
||||
x-apiClientRegistration:
|
||||
url: https://console.developers.google.com
|
||||
x-logo:
|
||||
url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png
|
||||
x-origin:
|
||||
format: google
|
||||
url: https://www.googleapis.com/discovery/v1/apis/drive/v2/rest
|
||||
version: v1
|
||||
x-preferred: false
|
||||
x-providerName: googleapis.com
|
||||
x-serviceName: drive
|
||||
swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.json
|
||||
swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.yaml
|
||||
updated: 2016-06-17T00:21:44.000Z
|
||||
v3:
|
||||
added: 2015-12-12T00:25:13.000Z
|
||||
info:
|
||||
title: Drive
|
||||
version: v3
|
||||
x-apiClientRegistration:
|
||||
url: https://console.developers.google.com
|
||||
x-logo:
|
||||
url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png
|
||||
x-origin:
|
||||
format: google
|
||||
url: https://www.googleapis.com/discovery/v1/apis/drive/v3/rest
|
||||
version: v1
|
||||
x-preferred: true
|
||||
x-providerName: googleapis.com
|
||||
x-serviceName: drive
|
||||
swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.json
|
||||
swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.yaml
|
||||
updated: 2016-06-17T00:21:44.000Z
|
||||
minProperties: 1
|
||||
type: object
|
||||
ApiVersion:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
added:
|
||||
description: Timestamp when the version was added
|
||||
format: date-time
|
||||
type: string
|
||||
externalDocs:
|
||||
description: Copy of `externalDocs` section from OpenAPI definition
|
||||
minProperties: 1
|
||||
type: object
|
||||
info:
|
||||
description: Copy of `info` section from OpenAPI definition
|
||||
minProperties: 1
|
||||
type: object
|
||||
link:
|
||||
description: Link to the individual API entry for this API
|
||||
format: url
|
||||
type: string
|
||||
openapiVer:
|
||||
description: The value of the `openapi` or `swagger` property of the source definition
|
||||
type: string
|
||||
swaggerUrl:
|
||||
description: URL to OpenAPI definition in JSON format
|
||||
format: url
|
||||
type: string
|
||||
swaggerYamlUrl:
|
||||
description: URL to OpenAPI definition in YAML format
|
||||
format: url
|
||||
type: string
|
||||
updated:
|
||||
description: Timestamp when the version was updated
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- added
|
||||
- updated
|
||||
- swaggerUrl
|
||||
- swaggerYamlUrl
|
||||
- info
|
||||
- openapiVer
|
||||
type: object
|
||||
Metrics:
|
||||
additionalProperties: false
|
||||
description: List of basic metrics
|
||||
example:
|
||||
datasets: []
|
||||
fixedPct: 22
|
||||
fixes: 81119
|
||||
invalid: 598
|
||||
issues: 28
|
||||
numAPIs: 2501
|
||||
numDrivers: 10
|
||||
numEndpoints: 106448
|
||||
numProviders: 659
|
||||
numSpecs: 3329
|
||||
stars: 2429
|
||||
thisWeek:
|
||||
added: 45
|
||||
updated: 171
|
||||
unofficial: 25
|
||||
unreachable: 123
|
||||
properties:
|
||||
datasets:
|
||||
description: Data used for charting etc
|
||||
items: {}
|
||||
type: array
|
||||
fixedPct:
|
||||
description: Percentage of all APIs where auto fixes have been applied
|
||||
type: integer
|
||||
fixes:
|
||||
description: Total number of fixes applied across all APIs
|
||||
type: integer
|
||||
invalid:
|
||||
description: Number of newly invalid APIs
|
||||
type: integer
|
||||
issues:
|
||||
description: Open GitHub issues on our main repo
|
||||
type: integer
|
||||
numAPIs:
|
||||
description: Number of unique APIs
|
||||
minimum: 1
|
||||
type: integer
|
||||
numDrivers:
|
||||
description: Number of methods of API retrieval
|
||||
type: integer
|
||||
numEndpoints:
|
||||
description: Total number of endpoints inside all definitions
|
||||
minimum: 1
|
||||
type: integer
|
||||
numProviders:
|
||||
description: Number of API providers in directory
|
||||
type: integer
|
||||
numSpecs:
|
||||
description: Number of API definitions including different versions of the same API
|
||||
minimum: 1
|
||||
type: integer
|
||||
stars:
|
||||
description: GitHub stars for our main repo
|
||||
type: integer
|
||||
thisWeek:
|
||||
description: Summary totals for the last 7 days
|
||||
properties:
|
||||
added:
|
||||
description: APIs added in the last week
|
||||
type: integer
|
||||
updated:
|
||||
description: APIs updated in the last week
|
||||
type: integer
|
||||
type: object
|
||||
unofficial:
|
||||
description: Number of unofficial APIs
|
||||
type: integer
|
||||
unreachable:
|
||||
description: Number of unreachable (4XX,5XX status) APIs
|
||||
type: integer
|
||||
required:
|
||||
- numSpecs
|
||||
- numAPIs
|
||||
- numEndpoints
|
||||
type: object
|
||||
x-optic-standard: "@febf8ac6-ee67-4565-b45a-5c85a469dca7/Fz6KU3_wMIO5iJ6_VUZ30"
|
||||
x-optic-url: https://app.useoptic.com/organizations/febf8ac6-ee67-4565-b45a-5c85a469dca7/apis/_0fKWqUvhs9ssYNkq1k-c
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: https://api.nasa.gov/planetary
|
||||
- url: http://api.nasa.gov/planetary
|
||||
info:
|
||||
contact:
|
||||
email: evan.t.yates@nasa.gov
|
||||
description: This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
title: APOD
|
||||
version: 1.0.0
|
||||
x-apisguru-categories:
|
||||
- media
|
||||
- open_data
|
||||
x-origin:
|
||||
- format: swagger
|
||||
url: https://raw.githubusercontent.com/nasa/api-docs/gh-pages/assets/json/APOD
|
||||
version: "2.0"
|
||||
x-providerName: nasa.gov
|
||||
x-serviceName: apod
|
||||
x-logo:
|
||||
url: https://api.apis.guru/v2/cache/logo/https_apis.guru_assets_images_no-logo.svg
|
||||
tags:
|
||||
- description: An example tag
|
||||
externalDocs:
|
||||
description: Here's a link
|
||||
url: https://example.com
|
||||
name: request tag
|
||||
paths:
|
||||
/apod:
|
||||
get:
|
||||
description: Returns the picture of the day
|
||||
parameters:
|
||||
- description: The date of the APOD image to retrieve
|
||||
in: query
|
||||
name: date
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- description: Retrieve the URL for the high resolution image
|
||||
in: query
|
||||
name: hd
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
items:
|
||||
x-thing: ok
|
||||
type: array
|
||||
description: successful operation
|
||||
"400":
|
||||
description: Date must be between Jun 16, 1995 and Mar 28, 2019.
|
||||
security:
|
||||
- api_key: []
|
||||
summary: Returns images
|
||||
tags:
|
||||
- request tag
|
||||
components:
|
||||
securitySchemes:
|
||||
api_key:
|
||||
in: query
|
||||
name: api_key
|
||||
type: apiKey
|
||||
@@ -1,78 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: http://xkcd.com/
|
||||
info:
|
||||
description: Webcomic of romance, sarcasm, math, and language.
|
||||
title: XKCD
|
||||
version: 1.0.0
|
||||
x-apisguru-categories:
|
||||
- media
|
||||
x-logo:
|
||||
url: https://api.apis.guru/v2/cache/logo/http_imgs.xkcd.com_static_terrible_small_logo.png
|
||||
x-origin:
|
||||
- format: openapi
|
||||
url: https://raw.githubusercontent.com/APIs-guru/unofficial_openapi_specs/master/xkcd.com/1.0.0/openapi.yaml
|
||||
version: "3.0"
|
||||
x-providerName: xkcd.com
|
||||
x-tags:
|
||||
- humor
|
||||
- comics
|
||||
x-unofficialSpec: true
|
||||
externalDocs:
|
||||
url: https://xkcd.com/json.html
|
||||
paths:
|
||||
/info.0.json:
|
||||
get:
|
||||
description: |
|
||||
Fetch current comic and metadata.
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
"*/*":
|
||||
schema:
|
||||
$ref: "#/components/schemas/comic"
|
||||
description: OK
|
||||
"/{comicId}/info.0.json":
|
||||
get:
|
||||
description: |
|
||||
Fetch comics and metadata by comic id.
|
||||
parameters:
|
||||
- in: path
|
||||
name: comicId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
"*/*":
|
||||
schema:
|
||||
$ref: "#/components/schemas/comic"
|
||||
description: OK
|
||||
components:
|
||||
schemas:
|
||||
comic:
|
||||
properties:
|
||||
alt:
|
||||
type: string
|
||||
day:
|
||||
type: string
|
||||
img:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
month:
|
||||
type: string
|
||||
news:
|
||||
type: string
|
||||
num:
|
||||
type: number
|
||||
safe_title:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
transcript:
|
||||
type: string
|
||||
year:
|
||||
type: string
|
||||
type: object
|
||||
@@ -5,13 +5,7 @@ import { convertOpenApi } from "../src";
|
||||
|
||||
describe("importer-openapi", () => {
|
||||
const p = path.join(__dirname, "fixtures");
|
||||
const fixtures = fs.readdirSync(p).filter((fixture) => {
|
||||
return fs.statSync(path.join(p, fixture)).isFile();
|
||||
});
|
||||
const realWorldFixturesPath = path.join(p, "real-world");
|
||||
const realWorldFixtures = fs
|
||||
.readdirSync(realWorldFixturesPath)
|
||||
.filter((fixture) => fixture.endsWith(".yaml"));
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
test("Maps operation description to request description", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
@@ -31,195 +25,7 @@ describe("importer-openapi", () => {
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
description: expect.stringContaining("Lijst van klanten"),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports requests directly from OpenAPI details", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Native Import Test", version: "1.0.0" },
|
||||
servers: [
|
||||
{ url: "https://api.example.com/{version}", variables: { version: { default: "v1" } } },
|
||||
],
|
||||
tags: [{ name: "accounts", description: "Account operations" }],
|
||||
paths: {
|
||||
"/accounts/{accountId}/members": {
|
||||
parameters: [
|
||||
{
|
||||
name: "accountId",
|
||||
in: "path",
|
||||
required: true,
|
||||
description: "Account identifier",
|
||||
schema: { type: "string", example: "acct_123" },
|
||||
},
|
||||
],
|
||||
post: {
|
||||
tags: ["accounts"],
|
||||
summary: "Create member",
|
||||
operationId: "createMember",
|
||||
parameters: [
|
||||
{
|
||||
name: "include",
|
||||
in: "query",
|
||||
description: "Related resources to include",
|
||||
schema: { type: "string", enum: ["roles"] },
|
||||
},
|
||||
{
|
||||
name: "X-Trace-Id",
|
||||
in: "header",
|
||||
schema: { type: "string", example: "trace-123" },
|
||||
},
|
||||
],
|
||||
security: [{ tokenAuth: [] }],
|
||||
requestBody: {
|
||||
description: "Member payload",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/MemberInput" },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"201": { description: "Created" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
tokenAuth: { type: "http", scheme: "bearer" },
|
||||
},
|
||||
schemas: {
|
||||
MemberInput: {
|
||||
type: "object",
|
||||
required: ["email"],
|
||||
properties: {
|
||||
email: { type: "string", example: "me@example.com" },
|
||||
admin: { type: "boolean", default: false },
|
||||
primaryContact: { $ref: "#/components/schemas/Contact" },
|
||||
secondaryContact: { $ref: "#/components/schemas/Contact" },
|
||||
},
|
||||
},
|
||||
Contact: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", example: "Taylor" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.folders).toEqual([
|
||||
expect.objectContaining({ name: "accounts", description: "Account operations" }),
|
||||
]);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }],
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Create member",
|
||||
method: "POST",
|
||||
url: "${[baseUrl]}/accounts/:accountId/members",
|
||||
authenticationType: "bearer",
|
||||
authentication: { token: "", prefix: "Bearer" },
|
||||
bodyType: "application/json",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{
|
||||
email: "me@example.com",
|
||||
admin: false,
|
||||
primaryContact: { name: "Taylor" },
|
||||
secondaryContact: { name: "Taylor" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
headers: expect.arrayContaining([
|
||||
{ enabled: false, name: "X-Trace-Id", value: "trace-123" },
|
||||
{ enabled: true, name: "Content-Type", value: "application/json" },
|
||||
]),
|
||||
urlParameters: [
|
||||
{ enabled: true, name: ":accountId", value: "acct_123" },
|
||||
{ enabled: false, name: "include", value: "roles" },
|
||||
],
|
||||
description: expect.stringContaining("Operation ID: createMember"),
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests[0]?.description).toContain("Member payload");
|
||||
expect(imported?.resources.httpRequests[0]?.description).toContain("201: Created");
|
||||
});
|
||||
|
||||
test("Handles large schemas without the Postman converter path", async () => {
|
||||
const paths: Record<string, unknown> = {};
|
||||
for (let i = 0; i < 500; i++) {
|
||||
paths[`/zones/{zoneId}/resources/${i}`] = {
|
||||
get: {
|
||||
tags: ["zones"],
|
||||
summary: `Read resource ${i}`,
|
||||
parameters: [
|
||||
{ name: "zoneId", in: "path", required: true, schema: { type: "string" } },
|
||||
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
description: "OK",
|
||||
content: {
|
||||
"application/json": { schema: { $ref: "#/components/schemas/Resource" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.1.0",
|
||||
info: { title: "Large API", version: "1.0.0" },
|
||||
servers: [{ url: "https://api.example.com/client/v4" }],
|
||||
tags: [{ name: "zones" }],
|
||||
paths,
|
||||
components: {
|
||||
schemas: {
|
||||
Resource: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
name: { type: "string" },
|
||||
metadata: { $ref: "#/components/schemas/Metadata" },
|
||||
},
|
||||
},
|
||||
Metadata: {
|
||||
type: "object",
|
||||
properties: {
|
||||
createdOn: { type: "string", format: "date-time" },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests.length).toBe(500);
|
||||
expect(imported?.resources.httpRequests[499]).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "Read resource 499",
|
||||
url: "${[baseUrl]}/zones/:zoneId/resources/499",
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
variables: [{ name: "baseUrl", value: "https://api.example.com/client/v4" }],
|
||||
description: "Lijst van klanten",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
@@ -229,340 +35,6 @@ describe("importer-openapi", () => {
|
||||
expect(imported).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Prefers operation and path servers over the spec base URL", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Servers Test", version: "1.0.0" },
|
||||
servers: [{ url: "https://root.example.com" }],
|
||||
paths: {
|
||||
"/root": { get: { responses: {} } },
|
||||
"/path-level": {
|
||||
servers: [{ url: "https://path.example.com" }],
|
||||
get: { responses: {} },
|
||||
},
|
||||
"/operation-level": {
|
||||
servers: [{ url: "https://path.example.com" }],
|
||||
get: { servers: [{ url: "https://operation.example.com" }], responses: {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests.map((r) => r.url)).toEqual([
|
||||
"${[baseUrl]}/root",
|
||||
"https://path.example.com/path-level",
|
||||
"https://operation.example.com/operation-level",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports OpenAPI 3 OAuth2 flows", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "OAuth Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/a": { get: { security: [{ oauth: ["read", "write"] }], responses: {} } },
|
||||
"/b": { get: { security: [{ implicitOauth: [] }], responses: {} } },
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
oauth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
clientCredentials: { tokenUrl: "https://example.com/token", scopes: {} },
|
||||
},
|
||||
},
|
||||
implicitOauth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
implicit: { authorizationUrl: "https://example.com/authorize", scopes: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "client_credentials",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
headerPrefix: "Bearer",
|
||||
scope: "read write",
|
||||
accessTokenUrl: "https://example.com/token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.httpRequests[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "implicit",
|
||||
clientId: "",
|
||||
headerPrefix: "Bearer",
|
||||
authorizationUrl: "https://example.com/authorize",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Imports Swagger 2 OAuth2 flows and produces", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
swagger: "2.0",
|
||||
info: { title: "Swagger OAuth Test", version: "1.0.0" },
|
||||
host: "example.com",
|
||||
produces: ["application/json"],
|
||||
paths: { "/a": { get: { security: [{ oauth: ["admin"] }], responses: {} } } },
|
||||
securityDefinitions: {
|
||||
oauth: {
|
||||
type: "oauth2",
|
||||
flow: "accessCode",
|
||||
authorizationUrl: "https://example.com/authorize",
|
||||
tokenUrl: "https://example.com/token",
|
||||
scopes: { admin: "Admin access" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "authorization_code",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
headerPrefix: "Bearer",
|
||||
scope: "admin",
|
||||
authorizationUrl: "https://example.com/authorize",
|
||||
accessTokenUrl: "https://example.com/token",
|
||||
},
|
||||
headers: [{ enabled: true, name: "Accept", value: "application/json" }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Names operations that only carry a description", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Naming Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/a": { get: { description: "Fetch the current comic.\nMore detail here.\n" } },
|
||||
"/b": { get: { description: `${"x".repeat(101)}` } },
|
||||
"/c": { get: { summary: "Explicit summary", description: "Ignored" } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests.map((r) => r.name)).toEqual([
|
||||
"Fetch the current comic.",
|
||||
// Too long to read as a name, so the route is clearer
|
||||
"GET /b",
|
||||
"Explicit summary",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Disambiguates requests that would share a name", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Duplicate Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/anything": {
|
||||
get: { summary: "Returns anything" },
|
||||
post: { summary: "Returns anything" },
|
||||
},
|
||||
"/unique": { get: { summary: "Stands alone" } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests.map((r) => r.name)).toEqual([
|
||||
"Returns anything (GET /anything)",
|
||||
"Returns anything (POST /anything)",
|
||||
"Stands alone",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Flags deprecated operations", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Deprecated Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/old": { get: { summary: "Old", deprecated: true, description: "Use /new instead." } },
|
||||
"/new": { get: { summary: "New" } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.description).toBe(
|
||||
"Deprecated.\n\nUse /new instead.",
|
||||
);
|
||||
expect(imported?.resources.httpRequests[1]?.description).toBe("New");
|
||||
});
|
||||
|
||||
test("Derives an Accept header from OpenAPI 3 responses", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Accept Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/prefers-json": {
|
||||
get: {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "ok",
|
||||
content: { "application/xml": {}, "application/json": {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Only failures describe content, so there is nothing to accept
|
||||
"/errors-only": {
|
||||
get: {
|
||||
responses: { "500": { description: "nope", content: { "application/json": {} } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
|
||||
{ enabled: true, name: "Accept", value: "application/json" },
|
||||
]);
|
||||
expect(imported?.resources.httpRequests[1]?.headers).toEqual([]);
|
||||
});
|
||||
|
||||
test("Lets an operation override a path-level parameter", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "Override Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/a": {
|
||||
parameters: [
|
||||
{ name: "page", in: "query", required: false, schema: { example: "path-level" } },
|
||||
{ name: "keep", in: "query", required: true, schema: { example: "untouched" } },
|
||||
],
|
||||
get: {
|
||||
parameters: [
|
||||
// Same name and location as above, so it replaces rather than adds
|
||||
{ name: "page", in: "query", required: true, schema: { example: "operation" } },
|
||||
// Same name but a different location, so it is its own parameter
|
||||
{ name: "page", in: "header", schema: { example: "header-level" } },
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
|
||||
{ enabled: true, name: "page", value: "operation" },
|
||||
{ enabled: true, name: "keep", value: "untouched" },
|
||||
]);
|
||||
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
|
||||
{ enabled: false, name: "page", value: "header-level" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Prefers operation-level consumes for Swagger bodies", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
swagger: "2.0",
|
||||
info: { title: "Consumes Test", version: "1.0.0" },
|
||||
host: "example.com",
|
||||
consumes: ["application/json"],
|
||||
paths: {
|
||||
"/a": {
|
||||
post: {
|
||||
consumes: ["application/xml"],
|
||||
parameters: [{ name: "body", in: "body", schema: { type: "object" } }],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
bodyType: "application/xml",
|
||||
headers: expect.arrayContaining([
|
||||
{ enabled: true, name: "Content-Type", value: "application/xml" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Imports Swagger 2 basic auth and cookie API keys", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
swagger: "2.0",
|
||||
info: { title: "Auth Test", version: "1.0.0" },
|
||||
host: "example.com",
|
||||
paths: {
|
||||
"/a": { get: { security: [{ basicAuth: [] }], responses: {} } },
|
||||
"/b": { get: { security: [{ cookieKey: [] }], responses: {} } },
|
||||
},
|
||||
securityDefinitions: {
|
||||
basicAuth: { type: "basic" },
|
||||
cookieKey: { type: "apiKey", in: "cookie", name: "session" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "basic",
|
||||
authentication: { username: "", password: "" },
|
||||
}),
|
||||
);
|
||||
// The auth plugin has no cookie location, so it becomes the Cookie header
|
||||
expect(imported?.resources.httpRequests[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "apikey",
|
||||
authentication: { location: "header", key: "Cookie", value: "session=" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Reports references that point outside the document", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.0",
|
||||
info: { title: "External Ref Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/a": {
|
||||
post: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: { $ref: "./shared.yaml#/components/schemas/Foo" } },
|
||||
},
|
||||
},
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
"/b": { get: { responses: {} } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.description).toContain(
|
||||
"./shared.yaml#/components/schemas/Foo",
|
||||
);
|
||||
// The report is per-operation, so an unrelated request stays clean
|
||||
expect(imported?.resources.httpRequests[1]?.description).toBeUndefined();
|
||||
});
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
test(`Imports ${fixture}`, async () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), "utf-8");
|
||||
@@ -574,15 +46,7 @@ describe("importer-openapi", () => {
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests.length).toBe(19);
|
||||
expect(imported?.resources.folders.map((f) => f.name)).toEqual(["pet", "store", "user"]);
|
||||
});
|
||||
}
|
||||
|
||||
for (const fixture of realWorldFixtures) {
|
||||
test(`Snapshots real-world fixture ${fixture}`, async () => {
|
||||
const contents = fs.readFileSync(path.join(realWorldFixturesPath, fixture), "utf-8");
|
||||
const imported = await convertOpenApi(contents);
|
||||
expect(imported).toMatchSnapshot();
|
||||
expect(imported?.resources.folders.length).toBe(7);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const tar = require("tar");
|
||||
const yauzl = require("yauzl");
|
||||
|
||||
// Resolve an archive entry against destDir, refusing anything that escapes it.
|
||||
function safeJoin(destDir, entryName) {
|
||||
const root = path.resolve(destDir);
|
||||
const resolved = path.resolve(root, entryName);
|
||||
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
||||
throw new Error(`Archive entry escapes destination directory: ${entryName}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function extractZip(filePath, destDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.open(filePath, { lazyEntries: true }, (err, zip) => {
|
||||
if (err) return reject(err);
|
||||
zip.on("error", reject);
|
||||
zip.on("end", resolve);
|
||||
zip.on("entry", (entry) => {
|
||||
let dst;
|
||||
try {
|
||||
dst = safeJoin(destDir, entry.fileName);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
|
||||
// Unix mode lives in the high 16 bits of the external attributes
|
||||
const rawMode = (entry.externalFileAttributes >>> 16) & 0xffff;
|
||||
const isSymlink = (rawMode & 0o170000) === 0o120000;
|
||||
if (isSymlink) {
|
||||
return reject(new Error(`Refusing to extract symlink from archive: ${entry.fileName}`));
|
||||
}
|
||||
|
||||
if (entry.fileName.endsWith("/")) {
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
return zip.readEntry();
|
||||
}
|
||||
|
||||
zip.openReadStream(entry, (err2, stream) => {
|
||||
if (err2) return reject(err2);
|
||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
||||
const out = fs.createWriteStream(dst);
|
||||
stream.on("error", reject);
|
||||
out.on("error", reject);
|
||||
out.on("close", () => {
|
||||
const mode = rawMode & 0o7777;
|
||||
if (mode !== 0) fs.chmodSync(dst, mode);
|
||||
zip.readEntry();
|
||||
});
|
||||
stream.pipe(out);
|
||||
});
|
||||
});
|
||||
zip.readEntry();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a `.zip` or `.tar.gz` archive into destDir, preserving file modes.
|
||||
* Entries that would land outside destDir are rejected.
|
||||
*/
|
||||
async function extractArchive(filePath, destDir) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
if (filePath.endsWith(".zip")) {
|
||||
await extractZip(filePath, destDir);
|
||||
} else if (filePath.endsWith(".tar.gz") || filePath.endsWith(".tgz")) {
|
||||
// oxlint-disable-next-line await-thenable -- tar.x() returns a promise when `file` is set
|
||||
await tar.x({ file: filePath, cwd: destDir });
|
||||
} else {
|
||||
throw new Error(`Unsupported archive format: ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { extractArchive };
|
||||
@@ -17,10 +17,7 @@ import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Git supplies three arguments when invoking this as a post-checkout hook. When run directly,
|
||||
// assume the caller wants to configure the current worktree instead of requiring placeholder refs.
|
||||
const isManualRun = process.argv.length === 2;
|
||||
const isBranchCheckout = isManualRun || process.argv[4] === "1";
|
||||
const isBranchCheckout = process.argv[4] === "1";
|
||||
|
||||
if (!isBranchCheckout) {
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const path = require("node:path");
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const decompress = require("decompress");
|
||||
const Downloader = require("nodejs-file-downloader");
|
||||
const { extractArchive } = require("./extract-archive.cjs");
|
||||
const { rmSync, cpSync, mkdirSync, existsSync } = require("node:fs");
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
@@ -92,7 +92,7 @@ rmSync(tmpDir, { recursive: true, force: true });
|
||||
console.log("SHA256 verified:", actualHash);
|
||||
|
||||
// Decompress to the same directory
|
||||
await extractArchive(filePath, tmpDir);
|
||||
await decompress(filePath, tmpDir, {});
|
||||
|
||||
// Copy binary
|
||||
const binSrc = path.join(tmpDir, SRC_BIN_MAP[key]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const decompress = require("decompress");
|
||||
const Downloader = require("nodejs-file-downloader");
|
||||
const { extractArchive } = require("./extract-archive.cjs");
|
||||
const path = require("node:path");
|
||||
const { rmSync, mkdirSync, cpSync, existsSync, statSync, chmodSync } = require("node:fs");
|
||||
const { execSync } = require("node:child_process");
|
||||
@@ -86,7 +86,7 @@ mkdirSync(dstDir, { recursive: true });
|
||||
console.log("SHA256 verified:", actualHash);
|
||||
|
||||
// Decompress to the same directory
|
||||
await extractArchive(filePath, tmpDir);
|
||||
await decompress(filePath, tmpDir, {});
|
||||
|
||||
// Copy binary
|
||||
cpSync(binSrc, binDst);
|
||||
|
||||
Reference in New Issue
Block a user