+
+
+
+ patchModel(workspaceMeta, { settingSyncDir: filePath })}
+ />
+
+
+
+
+
-
-
- patchModel(workspaceMeta, { settingSyncDir: filePath })}
- />
-
-
-
);
}
+
+WorkspaceSettingsDialog.show = (workspaceId: string, tab?: WorkspaceSettingsTab) => {
+ showDialog({
+ id: "workspace-settings",
+ size: "lg",
+ className: "h-[calc(100vh-5rem)] !max-h-[50rem]",
+ noPadding: true,
+ render: ({ hide }) => (
+
+ ),
+ });
+};
diff --git a/apps/yaak-client/components/core/Checkbox.tsx b/apps/yaak-client/components/core/Checkbox.tsx
index 6b10c56b..59258918 100644
--- a/apps/yaak-client/components/core/Checkbox.tsx
+++ b/apps/yaak-client/components/core/Checkbox.tsx
@@ -13,6 +13,7 @@ export interface CheckboxProps {
hideLabel?: boolean;
fullWidth?: boolean;
help?: ReactNode;
+ size?: "sm" | "md";
}
export function Checkbox({
@@ -25,6 +26,7 @@ export function Checkbox({
hideLabel,
fullWidth,
help,
+ size = "sm",
}: CheckboxProps) {
return (
diff --git a/apps/yaak-client/components/core/KeyValueRow.tsx b/apps/yaak-client/components/core/KeyValueRow.tsx
index 91aee687..f7b4dbd9 100644
--- a/apps/yaak-client/components/core/KeyValueRow.tsx
+++ b/apps/yaak-client/components/core/KeyValueRow.tsx
@@ -1,16 +1,24 @@
import classNames from "classnames";
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
+import { CopyIconButton } from "../CopyIconButton";
interface Props {
children:
| ReactElement
>
| (ReactElement> | null)[];
+ selectable?: boolean;
}
-export function KeyValueRows({ children }: Props) {
+export function KeyValueRows({ children, selectable }: Props) {
const childArray = Array.isArray(children) ? children.filter(Boolean) : [children];
return (
-
+
{childArray.map((child, i) => (
// oxlint-disable-next-line react/no-array-index-key
@@ -28,6 +36,8 @@ interface KeyValueRowProps {
leftSlot?: ReactNode;
labelClassName?: string;
labelColor?: "secondary" | "primary" | "info";
+ enableCopy?: boolean;
+ copyText?: string;
}
export function KeyValueRow({
@@ -37,7 +47,24 @@ export function KeyValueRow({
leftSlot,
labelColor = "secondary",
labelClassName,
+ enableCopy,
+ copyText,
}: KeyValueRowProps) {
+ const textToCopy =
+ copyText ??
+ (typeof children === "string" || typeof children === "number" ? `${children}` : null);
+ const resolvedRightSlot =
+ rightSlot ??
+ (enableCopy && textToCopy != null ? (
+
+ ) : null);
+
return (
<>
{leftSlot ?? }
{children}
- {rightSlot ? {rightSlot}
: }
+ {resolvedRightSlot ? (
+ {resolvedRightSlot}
+ ) : (
+
+ )}
>
diff --git a/apps/yaak-client/components/core/PlainInput.tsx b/apps/yaak-client/components/core/PlainInput.tsx
index 95cbaec8..4361bba3 100644
--- a/apps/yaak-client/components/core/PlainInput.tsx
+++ b/apps/yaak-client/components/core/PlainInput.tsx
@@ -43,6 +43,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
className,
containerClassName,
defaultValue,
+ disabled,
forceUpdateKey: forceUpdateKeyFromAbove,
help,
hideLabel,
@@ -163,7 +164,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
"relative w-full rounded-md text",
"border",
"overflow-hidden",
- focused ? "border-border-focus" : "border-border-subtle",
+ focused && !disabled ? "border-border-focus" : "border-border-subtle",
+ disabled && "border-dotted",
hasChanged && "has-[:invalid]:border-danger", // For built-in HTML validation
size === "md" && "min-h-md",
size === "sm" && "min-h-sm",
@@ -198,12 +200,13 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
// oxlint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus}
defaultValue={defaultValue ?? undefined}
+ disabled={disabled}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
onChange={(e) => handleChange(e.target.value)}
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
- className={classNames(commonClassName, "h-full")}
+ className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
onFocus={handleFocus}
onBlur={handleBlur}
required={required}
diff --git a/apps/yaak-client/components/core/Select.tsx b/apps/yaak-client/components/core/Select.tsx
index 555a4517..424365e5 100644
--- a/apps/yaak-client/components/core/Select.tsx
+++ b/apps/yaak-client/components/core/Select.tsx
@@ -109,7 +109,15 @@ export function Select({
) : (
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
// light mode
-
+
+ o.type === "separator" || o.value !== defaultValue
+ ? o
+ : { ...o, label: <>{o.label} (default)> },
+ )}
+ >
diff --git a/apps/yaak-client/components/core/SettingRow.tsx b/apps/yaak-client/components/core/SettingRow.tsx
new file mode 100644
index 00000000..eb24556c
--- /dev/null
+++ b/apps/yaak-client/components/core/SettingRow.tsx
@@ -0,0 +1,510 @@
+import type { AnyModel } from "@yaakapp-internal/models";
+import { patchModel } from "@yaakapp-internal/models";
+import classNames from "classnames";
+import type { ReactNode } from "react";
+import { CopyIconButton } from "../CopyIconButton";
+import { Checkbox } from "./Checkbox";
+import { IconButton, type IconButtonProps } from "./IconButton";
+import { PlainInput } from "./PlainInput";
+import type { RadioDropdownItem } from "./RadioDropdown";
+import { Select } from "./Select";
+import { SelectFile } from "../SelectFile";
+
+type ModelKeyOfValue = {
+ [K in keyof T]-?: T[K] extends V ? K : never;
+}[keyof T];
+
+type SettingRowBaseProps = {
+ className?: string;
+ controlClassName?: string;
+ description?: ReactNode;
+ disabled?: boolean;
+ title: ReactNode;
+};
+
+export function SettingsList({ children, className }: { children: ReactNode; className?: string }) {
+ return {children}
;
+}
+
+export function SettingsSection({
+ children,
+ className,
+ description,
+ title,
+}: {
+ children: ReactNode;
+ className?: string;
+ description?: ReactNode;
+ title: ReactNode | null;
+}) {
+ const showHeader = title != null || description != null;
+
+ return (
+
+ {showHeader && (
+
+ {title != null &&
{title}
}
+ {description != null &&
{description}
}
+
+ )}
+ {children}
+
+ );
+}
+
+export function SettingRow({
+ children,
+ className,
+ controlClassName,
+ description,
+ disabled,
+ title,
+}: {
+ children: ReactNode;
+} & SettingRowBaseProps) {
+ return (
+
+
+
{title}
+ {description != null && (
+
{description}
+ )}
+
+
+ {children}
+
+
+ );
+}
+
+export function SettingValue({
+ actions,
+ className,
+ copyText,
+ enableCopy = true,
+ value,
+}: {
+ actions?: SettingValueAction[];
+ className?: string;
+ copyText?: string;
+ enableCopy?: boolean;
+ value: ReactNode;
+}) {
+ const textValue = typeof value === "string" || typeof value === "number" ? `${value}` : null;
+ const textToCopy = copyText ?? textValue;
+
+ return (
+ <>
+
+ {value}
+
+ {actions?.map((action) => (
+
+ ))}
+ {enableCopy && textToCopy != null && (
+
+ )}
+ >
+ );
+}
+
+type SettingValueAction = {
+ icon: IconButtonProps["icon"];
+ onClick: () => void;
+ title: string;
+};
+
+export function SettingRowBoolean({
+ checked,
+ checkboxSize = "md",
+ onChange,
+ title,
+ ...props
+}: {
+ checked: boolean;
+ checkboxSize?: "sm" | "md";
+ onChange: (checked: boolean) => void;
+} & SettingRowBaseProps) {
+ return (
+
+
+
+ );
+}
+
+export function ModelSettingRowBoolean>({
+ model,
+ modelKey,
+ ...props
+}: {
+ model: M;
+ modelKey: K;
+} & Omit[0], "checked" | "onChange">) {
+ return (
+ patchModel(model, { [modelKey]: value } as Partial)}
+ {...props}
+ />
+ );
+}
+
+export function SettingRowNumber({
+ inputClassName,
+ inputWidthClassName = "!w-48",
+ name,
+ onChange,
+ placeholder,
+ required,
+ title,
+ type = "number",
+ validate,
+ value,
+ ...props
+}: {
+ inputClassName?: string;
+ inputWidthClassName?: string;
+ name: string;
+ onChange: (value: number) => void;
+ placeholder?: string;
+ required?: boolean;
+ type?: "number";
+ validate?: (value: string) => boolean;
+ value: number;
+} & SettingRowBaseProps) {
+ return (
+
+ onChange(Number.parseInt(value, 10) || 0)}
+ type={type}
+ className={inputClassName}
+ containerClassName={inputWidthClassName}
+ disabled={props.disabled}
+ />
+
+ );
+}
+
+export function ModelSettingRowNumber>({
+ model,
+ modelKey,
+ name = String(modelKey),
+ ...props
+}: {
+ model: M;
+ modelKey: K;
+ name?: string;
+} & Omit[0], "name" | "onChange" | "value">) {
+ return (
+ patchModel(model, { [modelKey]: value } as Partial)}
+ {...props}
+ />
+ );
+}
+
+export function SettingRowText({
+ inputClassName,
+ inputWidthClassName = "!w-80",
+ name,
+ onChange,
+ placeholder,
+ required,
+ title,
+ type = "text",
+ value,
+ ...props
+}: {
+ inputClassName?: string;
+ inputWidthClassName?: string;
+ name: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ required?: boolean;
+ type?: "text" | "password";
+ value: string;
+} & SettingRowBaseProps) {
+ return (
+
+
+
+ );
+}
+
+export function ModelSettingRowText>({
+ model,
+ modelKey,
+ name = String(modelKey),
+ ...props
+}: {
+ model: M;
+ modelKey: K;
+ name?: string;
+} & Omit[0], "name" | "onChange" | "value">) {
+ return (
+ patchModel(model, { [modelKey]: value } as Partial)}
+ {...props}
+ />
+ );
+}
+
+export function SettingRowFile({
+ buttonClassName,
+ controlClassName = "min-w-0 max-w-[min(32rem,45vw)]",
+ directory,
+ filePath,
+ nameOverride,
+ noun,
+ onChange,
+ size = "xs",
+ title,
+ ...props
+}: {
+ buttonClassName?: string;
+ directory?: boolean;
+ filePath: string | null;
+ nameOverride?: string | null;
+ noun?: string;
+ onChange: (filePath: string | null) => void | Promise;
+ size?: Parameters[0]["size"];
+} & SettingRowBaseProps) {
+ return (
+
+ onChange(filePath)}
+ />
+
+ );
+}
+
+export function SettingRowDirectory({
+ noun = "Directory",
+ ...props
+}: Omit[0], "directory">) {
+ return ;
+}
+
+export function SettingRowSelect({
+ defaultValue,
+ name,
+ onChange,
+ options,
+ selectClassName = "!w-48",
+ title,
+ value,
+ ...props
+}: {
+ defaultValue?: T;
+ name: string;
+ onChange: (value: T) => void;
+ options: RadioDropdownItem[];
+ selectClassName?: string;
+ value: T;
+} & SettingRowBaseProps) {
+ return (
+
+
+
+ );
+}
+
+export function SettingSelectControl({
+ defaultValue,
+ disabled,
+ label,
+ name,
+ onChange,
+ options,
+ selectClassName = "!w-48",
+ value,
+}: {
+ defaultValue?: T;
+ disabled?: boolean;
+ label: string;
+ name: string;
+ onChange: (value: T) => void;
+ options: RadioDropdownItem[];
+ selectClassName?: string;
+ value: T;
+}) {
+ return (
+
+ );
+}
+
+export function ModelSettingSelectControl<
+ M extends AnyModel,
+ K extends ModelKeyOfValue,
+ V extends M[K] & string,
+>({
+ model,
+ modelKey,
+ name = String(modelKey),
+ ...props
+}: {
+ model: M;
+ modelKey: K;
+ name?: string;
+} & Omit>[0], "name" | "onChange" | "value">) {
+ return (
+ patchModel(model, { [modelKey]: value } as Partial)}
+ {...props}
+ />
+ );
+}
+
+export function ModelSettingRowSelect<
+ M extends AnyModel,
+ K extends ModelKeyOfValue,
+ V extends M[K] & string,
+>({
+ model,
+ modelKey,
+ name = String(modelKey),
+ ...props
+}: {
+ model: M;
+ modelKey: K;
+ name?: string;
+} & Omit>[0], "name" | "onChange" | "value">) {
+ return (
+ patchModel(model, { [modelKey]: value } as Partial)}
+ {...props}
+ />
+ );
+}
+
+export function SettingOverrideRow({
+ children,
+ className,
+ controlClassName,
+ description,
+ disabled,
+ onResetOverride,
+ overridden,
+ resetTitle = "Reset override",
+ title,
+}: {
+ children: ReactNode;
+ className?: string;
+ controlClassName?: string;
+ description?: ReactNode;
+ disabled?: boolean;
+ onResetOverride: () => void;
+ overridden: boolean;
+ resetTitle?: string;
+ title: ReactNode;
+}) {
+ return (
+
+ {title}
+ {overridden && (
+
+ )}
+
+ }
+ >
+ {children}
+
+ );
+}
diff --git a/apps/yaak-client/components/git/GitDropdown.tsx b/apps/yaak-client/components/git/GitDropdown.tsx
index a88661dc..607320e2 100644
--- a/apps/yaak-client/components/git/GitDropdown.tsx
+++ b/apps/yaak-client/components/git/GitDropdown.tsx
@@ -596,7 +596,7 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
color: "success",
label: "Open Workspace Settings",
leftSlot: ,
- onSelect: () => openWorkspaceSettings("data"),
+ onSelect: () => openWorkspaceSettings("settings"),
},
{ type: "separator" },
{
diff --git a/apps/yaak-client/hooks/useAuthTab.tsx b/apps/yaak-client/hooks/useAuthTab.tsx
index 7ae05ef2..91e7c99d 100644
--- a/apps/yaak-client/hooks/useAuthTab.tsx
+++ b/apps/yaak-client/hooks/useAuthTab.tsx
@@ -43,7 +43,7 @@ export function useAuthTab(tabValue: T, model: AuthenticatedMo
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
?.shortLabel ?? "UNKNOWN"}
diff --git a/crates-cli/yaak-cli/src/plugin_events.rs b/crates-cli/yaak-cli/src/plugin_events.rs
index bdd37e7e..70bc2adc 100644
--- a/crates-cli/yaak-cli/src/plugin_events.rs
+++ b/crates-cli/yaak-cli/src/plugin_events.rs
@@ -473,7 +473,7 @@ async fn build_plugin_reply(
let names = cookie_jar
.cookies
.into_iter()
- .filter_map(|c| parse_cookie_name_value(&c.raw_cookie).map(|(name, _)| name))
+ .map(|c| c.name)
.collect();
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
@@ -531,12 +531,6 @@ async fn render_json_value_for_cli(
render_json_value_raw(value, vars, cb, opt).await
}
-fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
- let first_part = raw_cookie.split(';').next()?.trim();
- let (name, value) = first_part.split_once('=')?;
- Some((name.trim().to_string(), value.to_string()))
-}
-
fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
clipboard.set_text(text.to_string()).map_err(|e| e.to_string())
diff --git a/crates-tauri/yaak-app-client/src/plugin_events.rs b/crates-tauri/yaak-app-client/src/plugin_events.rs
index 4b14dd90..98a89117 100644
--- a/crates-tauri/yaak-app-client/src/plugin_events.rs
+++ b/crates-tauri/yaak-app-client/src/plugin_events.rs
@@ -8,7 +8,6 @@ use crate::{
workspace_from_window,
};
use chrono::Utc;
-use cookie::Cookie;
use log::error;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
@@ -409,11 +408,7 @@ async fn handle_host_plugin_request(
let window = get_window_from_plugin_context(app_handle, plugin_context)?;
let names = match cookie_jar_from_window(&window) {
None => Vec::new(),
- Some(j) => j
- .cookies
- .into_iter()
- .filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
- .collect(),
+ Some(j) => j.cookies.into_iter().map(|c| c.name).collect(),
};
Ok(Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
names,
diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs
index 96b6f438..5ac1d913 100644
--- a/crates-tauri/yaak-app-client/src/ws_ext.rs
+++ b/crates-tauri/yaak-app-client/src/ws_ext.rs
@@ -134,7 +134,8 @@ pub async fn cmd_ws_connect(
unrendered_request.folder_id.as_deref(),
environment_id,
)?;
- let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
+ let resolved_settings =
+ 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, &unrendered_request)?;
@@ -248,7 +249,7 @@ pub async fn cmd_ws_connect(
}
// Add cookies to WS HTTP Upgrade
- if let Some(id) = cookie_jar_id {
+ if let (true, Some(id)) = (resolved_settings.send_cookies, cookie_jar_id) {
let cookie_jar = app_handle.db().get_cookie_jar(&id)?;
let store = CookieStore::from_cookies(cookie_jar.cookies);
@@ -289,7 +290,7 @@ pub async fn cmd_ws_connect(
url.as_str(),
headers,
receive_tx,
- workspace.setting_validate_certificates,
+ resolved_settings.validate_certificates,
client_cert,
)
.await
diff --git a/crates/yaak-git/bindings/gen_models.ts b/crates/yaak-git/bindings/gen_models.ts
index 866a91e4..e9c76a50 100644
--- a/crates/yaak-git/bindings/gen_models.ts
+++ b/crates/yaak-git/bindings/gen_models.ts
@@ -1,45 +1,168 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type DnsOverride = { hostname: string, ipv4: Array, ipv6: Array, enabled?: boolean, };
+export type DnsOverride = {
+ hostname: string;
+ ipv4: Array;
+ ipv6: Array;
+ enabled?: boolean;
+};
-export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
-/**
- * Variables defined in this environment scope.
- * Child environments override parent variables by name.
- */
-variables: Array, color: string | null, sortPriority: number, };
+export type Environment = {
+ model: "environment";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ name: string;
+ public: boolean;
+ parentModel: string;
+ parentId: string | null;
+ /**
+ * Variables defined in this environment scope.
+ * Child environments override parent variables by name.
+ */
+ variables: Array;
+ color: string | null;
+ sortPriority: number;
+};
-export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
+export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
-export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, };
+export type Folder = {
+ model: "folder";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ sortPriority: number;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number,
-/**
- * Server URL (http for plaintext or https for secure)
- */
-url: string, };
+export type GrpcRequest = {
+ model: "grpc_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authenticationType: string | null;
+ authentication: Record;
+ description: string;
+ message: string;
+ metadata: Array;
+ method: string | null;
+ name: string;
+ service: string | null;
+ sortPriority: number;
+ /**
+ * Server URL (http for plaintext or https for secure)
+ */
+ url: string;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
-export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type HttpRequest = {
+ model: "http_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ body: Record;
+ bodyType: string | null;
+ description: string;
+ headers: Array;
+ method: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
+export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
-export type HttpUrlParameter = { enabled?: boolean,
-/**
- * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
- * Other entries are appended as query parameters
- */
-name: string, value: string, id?: string, };
+export type HttpUrlParameter = {
+ enabled?: boolean;
+ /**
+ * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
+ * Other entries are appended as query parameters
+ */
+ name: string;
+ value: string;
+ id?: string;
+};
-export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
+export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
-export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type InheritedIntSetting = { enabled?: boolean; value: number };
-export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array, };
+export type SyncModel =
+ | ({ type: "workspace" } & Workspace)
+ | ({ type: "environment" } & Environment)
+ | ({ type: "folder" } & Folder)
+ | ({ type: "http_request" } & HttpRequest)
+ | ({ type: "grpc_request" } & GrpcRequest)
+ | ({ type: "websocket_request" } & WebsocketRequest);
+
+export type WebsocketRequest = {
+ model: "websocket_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ message: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
+
+export type Workspace = {
+ model: "workspace";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ encryptionKeyChallenge: string | null;
+ settingValidateCertificates: boolean;
+ settingFollowRedirects: boolean;
+ settingRequestTimeout: number;
+ settingDnsOverrides: Array;
+ settingSendCookies: boolean;
+ settingStoreCookies: boolean;
+};
diff --git a/crates/yaak-http/src/cookies.rs b/crates/yaak-http/src/cookies.rs
index fa4d91f6..efdddc6f 100644
--- a/crates/yaak-http/src/cookies.rs
+++ b/crates/yaak-http/src/cookies.rs
@@ -7,7 +7,7 @@ use log::debug;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use url::Url;
-use yaak_models::models::{Cookie, CookieDomain, CookieExpires};
+use yaak_models::models::{Cookie, CookieDomain, CookieExpires, CookieSameSite};
/// A thread-safe cookie store that can be shared across requests
#[derive(Debug, Clone)]
@@ -45,10 +45,7 @@ impl CookieStore {
let matching_cookies: Vec<_> = cookies
.iter()
.filter(|cookie| self.cookie_matches(cookie, url, &now))
- .filter_map(|cookie| {
- // Parse the raw cookie to get name=value
- parse_cookie_name_value(&cookie.raw_cookie)
- })
+ .map(|cookie| (cookie.name.clone(), cookie.value.clone()))
.collect();
if matching_cookies.is_empty() {
@@ -72,13 +69,7 @@ impl CookieStore {
if let Some(cookie) = parse_set_cookie(header_value, url) {
// Remove any existing cookie with the same name and domain
cookies.retain(|existing| !cookies_match(existing, &cookie));
- debug!(
- "Storing cookie: {} for domain {:?}",
- parse_cookie_name_value(&cookie.raw_cookie)
- .map(|(n, _)| n)
- .unwrap_or_else(|| "unknown".to_string()),
- cookie.domain
- );
+ debug!("Storing cookie: {} for domain {:?}", cookie.name, cookie.domain);
cookies.push(cookie);
}
}
@@ -117,10 +108,9 @@ impl CookieStore {
}
// Check path
- let (cookie_path, _) = &cookie.path;
let url_path = url.path();
- path_matches(url_path, cookie_path)
+ path_matches(url_path, &cookie.path)
}
}
@@ -133,8 +123,7 @@ pub fn get_cookie_value_from_jar(
let domain = domain.and_then(normalize_cookie_domain_filter);
cookies.into_iter().find_map(|cookie| {
- let (cookie_name, value) = parse_cookie_name_value(&cookie.raw_cookie)?;
- if cookie_name != name {
+ if cookie.name != name {
return None;
}
@@ -144,11 +133,12 @@ pub fn get_cookie_value_from_jar(
}
}
- Some(value)
+ Some(cookie.value)
})
}
/// Parse name=value from a cookie string (raw_cookie format)
+#[cfg(test)]
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
// The raw_cookie typically looks like "name=value" or "name=value; attr1; attr2=..."
let first_part = raw_cookie.split(';').next()?;
@@ -177,8 +167,6 @@ fn cookie_domain_matches_filter(cookie_domain: &CookieDomain, domain: &str) -> b
fn parse_set_cookie(header_value: &str, request_url: &Url) -> Option {
let parsed = cookie::Cookie::parse(header_value).ok()?;
- let raw_cookie = format!("{}={}", parsed.name(), parsed.value());
-
// Determine domain
let domain = if let Some(domain_attr) = parsed.domain() {
// Domain attribute present - this is a suffix match
@@ -216,14 +204,28 @@ fn parse_set_cookie(header_value: &str, request_url: &Url) -> Option {
// Determine path
let path = if let Some(path_attr) = parsed.path() {
- (path_attr.to_string(), true)
+ path_attr.to_string()
} else {
// Default path is the directory of the request URI
- let default_path = default_cookie_path(request_url.path());
- (default_path, false)
+ default_cookie_path(request_url.path())
};
- Some(Cookie { raw_cookie, domain, expires, path })
+ let same_site = parsed.same_site().map(|same_site| match same_site {
+ cookie::SameSite::Strict => CookieSameSite::Strict,
+ cookie::SameSite::Lax => CookieSameSite::Lax,
+ cookie::SameSite::None => CookieSameSite::None,
+ });
+
+ Some(Cookie {
+ name: parsed.name().to_string(),
+ value: parsed.value().to_string(),
+ domain,
+ expires,
+ path,
+ secure: parsed.secure().unwrap_or(false),
+ http_only: parsed.http_only().unwrap_or(false),
+ same_site,
+ })
}
/// Get the default cookie path from a request path (RFC 6265 Section 5.1.4)
@@ -261,10 +263,7 @@ fn path_matches(request_path: &str, cookie_path: &str) -> bool {
/// Check if two cookies match (same name and domain)
fn cookies_match(a: &Cookie, b: &Cookie) -> bool {
- let name_a = parse_cookie_name_value(&a.raw_cookie).map(|(n, _)| n);
- let name_b = parse_cookie_name_value(&b.raw_cookie).map(|(n, _)| n);
-
- if name_a != name_b {
+ if a.name != b.name {
return false;
}
@@ -317,11 +316,16 @@ mod tests {
use super::*;
fn cookie(raw_cookie: &str, domain: CookieDomain) -> Cookie {
+ let (name, value) = parse_cookie_name_value(raw_cookie).unwrap();
Cookie {
- raw_cookie: raw_cookie.to_string(),
+ name,
+ value,
domain,
expires: CookieExpires::SessionEnd,
- path: ("/".to_string(), false),
+ path: "/".to_string(),
+ secure: false,
+ http_only: false,
+ same_site: None,
}
}
diff --git a/crates/yaak-http/src/transaction.rs b/crates/yaak-http/src/transaction.rs
index e7f135ae..67019693 100644
--- a/crates/yaak-http/src/transaction.rs
+++ b/crates/yaak-http/src/transaction.rs
@@ -12,22 +12,58 @@ pub struct HttpTransaction {
sender: S,
max_redirects: usize,
cookie_store: Option,
+ send_cookies: bool,
+ store_cookies: bool,
}
impl HttpTransaction {
/// Create a new transaction with default settings
pub fn new(sender: S) -> Self {
- Self { sender, max_redirects: 10, cookie_store: None }
+ Self {
+ sender,
+ max_redirects: 10,
+ cookie_store: None,
+ send_cookies: false,
+ store_cookies: false,
+ }
}
/// Create a new transaction with custom max redirects
pub fn with_max_redirects(sender: S, max_redirects: usize) -> Self {
- Self { sender, max_redirects, cookie_store: None }
+ Self {
+ sender,
+ max_redirects,
+ cookie_store: None,
+ send_cookies: false,
+ store_cookies: false,
+ }
}
/// Create a new transaction with a cookie store
pub fn with_cookie_store(sender: S, cookie_store: CookieStore) -> Self {
- Self { sender, max_redirects: 10, cookie_store: Some(cookie_store) }
+ Self {
+ sender,
+ max_redirects: 10,
+ cookie_store: Some(cookie_store),
+ send_cookies: true,
+ store_cookies: true,
+ }
+ }
+
+ /// Create a new transaction with a cookie store and explicit send/store behavior
+ pub fn with_cookie_behavior(
+ sender: S,
+ cookie_store: CookieStore,
+ send_cookies: bool,
+ store_cookies: bool,
+ ) -> Self {
+ Self {
+ sender,
+ max_redirects: 10,
+ cookie_store: Some(cookie_store),
+ send_cookies,
+ store_cookies,
+ }
}
/// Create a new transaction with custom max redirects and a cookie store
@@ -36,7 +72,13 @@ impl HttpTransaction {
max_redirects: usize,
cookie_store: Option,
) -> Self {
- Self { sender, max_redirects, cookie_store }
+ Self {
+ sender,
+ max_redirects,
+ send_cookies: cookie_store.is_some(),
+ store_cookies: cookie_store.is_some(),
+ cookie_store,
+ }
}
/// Execute the request with cancellation support.
@@ -66,9 +108,11 @@ impl HttpTransaction {
}
// Inject cookies into headers if we have a cookie store
- let headers_with_cookies = if let Some(cookie_store) = &self.cookie_store {
+ let headers_with_cookies = if self.send_cookies {
let mut headers = current_headers.clone();
- if let Ok(url) = Url::parse(¤t_url) {
+ if let (Some(cookie_store), Ok(url)) =
+ (&self.cookie_store, Url::parse(¤t_url))
+ {
if let Some(cookie_header) = cookie_store.get_cookie_header(&url) {
debug!("Injecting Cookie header: {}", cookie_header);
// Check if there's already a Cookie header and merge if so
@@ -115,8 +159,10 @@ impl HttpTransaction {
};
// Parse Set-Cookie headers and store cookies
- if let Some(cookie_store) = &self.cookie_store {
- if let Ok(url) = Url::parse(¤t_url) {
+ if self.store_cookies {
+ if let (Some(cookie_store), Ok(url)) =
+ (&self.cookie_store, Url::parse(¤t_url))
+ {
let set_cookie_headers: Vec = response
.headers
.iter()
@@ -579,10 +625,14 @@ mod tests {
// Create a cookie store with a test cookie
let cookie = Cookie {
- raw_cookie: "session=abc123".to_string(),
+ name: "session".to_string(),
+ value: "abc123".to_string(),
domain: CookieDomain::HostOnly("example.com".to_string()),
expires: CookieExpires::SessionEnd,
- path: ("/".to_string(), false),
+ path: "/".to_string(),
+ secure: false,
+ http_only: false,
+ same_site: None,
};
let cookie_store = CookieStore::from_cookies(vec![cookie]);
@@ -602,6 +652,67 @@ mod tests {
assert!(result.is_ok());
}
+ #[tokio::test]
+ async fn test_cookie_injection_can_be_disabled() {
+ struct CookieRejectingSender;
+
+ #[async_trait]
+ impl HttpSender for CookieRejectingSender {
+ async fn send(
+ &self,
+ request: SendableHttpRequest,
+ _event_tx: mpsc::Sender,
+ ) -> Result {
+ let cookie_header =
+ request.headers.iter().find(|(k, _)| k.eq_ignore_ascii_case("cookie"));
+ assert!(cookie_header.is_none(), "Cookie header should not be present");
+
+ let body_stream: Pin> =
+ Box::pin(std::io::Cursor::new(vec![]));
+ Ok(HttpResponse::new(
+ 200,
+ None,
+ Vec::new(),
+ Vec::new(),
+ None,
+ "https://example.com".to_string(),
+ None,
+ Some("HTTP/1.1".to_string()),
+ body_stream,
+ ContentEncoding::Identity,
+ ))
+ }
+ }
+
+ use yaak_models::models::{Cookie, CookieDomain, CookieExpires};
+
+ let cookie = Cookie {
+ name: "session".to_string(),
+ value: "abc123".to_string(),
+ domain: CookieDomain::HostOnly("example.com".to_string()),
+ expires: CookieExpires::SessionEnd,
+ path: "/".to_string(),
+ secure: false,
+ http_only: false,
+ same_site: None,
+ };
+ let cookie_store = CookieStore::from_cookies(vec![cookie]);
+ let transaction =
+ HttpTransaction::with_cookie_behavior(CookieRejectingSender, cookie_store, false, true);
+
+ let request = SendableHttpRequest {
+ url: "https://example.com/api".to_string(),
+ method: "GET".to_string(),
+ headers: vec![],
+ ..Default::default()
+ };
+
+ let (_tx, rx) = tokio::sync::watch::channel(false);
+ let (event_tx, _event_rx) = mpsc::channel(100);
+ let result = transaction.execute_with_cancellation(request, rx, event_tx).await;
+ assert!(result.is_ok());
+ }
+
#[tokio::test]
async fn test_set_cookie_parsing() {
// Create a cookie store
@@ -655,7 +766,62 @@ mod tests {
// Verify the cookie was stored
let cookies = cookie_store.get_all_cookies();
assert_eq!(cookies.len(), 1);
- assert!(cookies[0].raw_cookie.contains("session=xyz789"));
+ assert_eq!(cookies[0].name, "session");
+ assert_eq!(cookies[0].value, "xyz789");
+ }
+
+ #[tokio::test]
+ async fn test_set_cookie_storage_can_be_disabled() {
+ let cookie_store = CookieStore::new();
+
+ struct SetCookieSender;
+
+ #[async_trait]
+ impl HttpSender for SetCookieSender {
+ async fn send(
+ &self,
+ _request: SendableHttpRequest,
+ _event_tx: mpsc::Sender,
+ ) -> Result {
+ let headers =
+ vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
+
+ let body_stream: Pin> =
+ Box::pin(std::io::Cursor::new(vec![]));
+ Ok(HttpResponse::new(
+ 200,
+ None,
+ headers,
+ Vec::new(),
+ None,
+ "https://example.com".to_string(),
+ None,
+ Some("HTTP/1.1".to_string()),
+ body_stream,
+ ContentEncoding::Identity,
+ ))
+ }
+ }
+
+ let transaction = HttpTransaction::with_cookie_behavior(
+ SetCookieSender,
+ cookie_store.clone(),
+ true,
+ false,
+ );
+
+ let request = SendableHttpRequest {
+ url: "https://example.com/login".to_string(),
+ method: "POST".to_string(),
+ headers: vec![],
+ ..Default::default()
+ };
+
+ let (_tx, rx) = tokio::sync::watch::channel(false);
+ let (event_tx, _event_rx) = mpsc::channel(100);
+ let result = transaction.execute_with_cancellation(request, rx, event_tx).await;
+ assert!(result.is_ok());
+ assert!(cookie_store.get_all_cookies().is_empty());
}
#[tokio::test]
@@ -719,17 +885,15 @@ mod tests {
let cookies = cookie_store.get_all_cookies();
assert_eq!(cookies.len(), 3, "All three Set-Cookie headers should be parsed and stored");
- let cookie_values: Vec<&str> = cookies.iter().map(|c| c.raw_cookie.as_str()).collect();
+ let cookie_values: Vec<_> =
+ cookies.iter().map(|c| format!("{}={}", c.name, c.value)).collect();
assert!(
- cookie_values.iter().any(|c| c.contains("session=abc123")),
+ cookie_values.iter().any(|c| c == "session=abc123"),
"session cookie should be stored"
);
+ assert!(cookie_values.iter().any(|c| c == "user_id=42"), "user_id cookie should be stored");
assert!(
- cookie_values.iter().any(|c| c.contains("user_id=42")),
- "user_id cookie should be stored"
- );
- assert!(
- cookie_values.iter().any(|c| c.contains("preferences=dark")),
+ cookie_values.iter().any(|c| c == "preferences=dark"),
"preferences cookie should be stored"
);
}
diff --git a/crates/yaak-models/bindings/gen_models.ts b/crates/yaak-models/bindings/gen_models.ts
index 19a202bd..359f4895 100644
--- a/crates/yaak-models/bindings/gen_models.ts
+++ b/crates/yaak-models/bindings/gen_models.ts
@@ -1,121 +1,507 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ModelChangeEvent } from "./ModelChangeEvent";
-export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
+export type AnyModel =
+ | CookieJar
+ | Environment
+ | Folder
+ | GraphQlIntrospection
+ | GrpcConnection
+ | GrpcEvent
+ | GrpcRequest
+ | HttpRequest
+ | HttpResponse
+ | HttpResponseEvent
+ | KeyValue
+ | Plugin
+ | Settings
+ | SyncState
+ | WebsocketConnection
+ | WebsocketEvent
+ | WebsocketRequest
+ | Workspace
+ | WorkspaceMeta;
-export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
+export type ClientCertificate = {
+ host: string;
+ port: number | null;
+ crtFile: string | null;
+ keyFile: string | null;
+ pfxFile: string | null;
+ passphrase: string | null;
+ enabled?: boolean;
+};
-export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
+export type Cookie = {
+ name: string;
+ value: string;
+ domain: CookieDomain;
+ expires: CookieExpires;
+ path: string;
+ secure: boolean;
+ httpOnly: boolean;
+ sameSite: CookieSameSite | null;
+};
-export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
+export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty";
-export type CookieExpires = { "AtUtc": string } | "SessionEnd";
+export type CookieExpires = { AtUtc: string } | "SessionEnd";
-export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array, name: string, };
+export type CookieJar = {
+ model: "cookie_jar";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ cookies: Array;
+ name: string;
+};
-export type DnsOverride = { hostname: string, ipv4: Array, ipv6: Array, enabled?: boolean, };
+export type CookieSameSite = "Strict" | "Lax" | "None";
+
+export type DnsOverride = {
+ hostname: string;
+ ipv4: Array;
+ ipv6: Array;
+ enabled?: boolean;
+};
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
-export type EncryptedKey = { encryptedKey: string, };
+export type EncryptedKey = { encryptedKey: string };
-export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
-/**
- * Variables defined in this environment scope.
- * Child environments override parent variables by name.
- */
-variables: Array, color: string | null, sortPriority: number, };
+export type Environment = {
+ model: "environment";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ name: string;
+ public: boolean;
+ parentModel: string;
+ parentId: string | null;
+ /**
+ * Variables defined in this environment scope.
+ * Child environments override parent variables by name.
+ */
+ variables: Array;
+ color: string | null;
+ sortPriority: number;
+};
-export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
+export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
-export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, };
+export type Folder = {
+ model: "folder";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ sortPriority: number;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, };
+export type GraphQlIntrospection = {
+ model: "graphql_introspection";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ requestId: string;
+ content: string | null;
+};
-export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
+export type GrpcConnection = {
+ model: "grpc_connection";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ requestId: string;
+ elapsed: number;
+ error: string | null;
+ method: string;
+ service: string;
+ status: number;
+ state: GrpcConnectionState;
+ trailers: { [key in string]?: string };
+ url: string;
+};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
-export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
+export type GrpcEvent = {
+ model: "grpc_event";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ requestId: string;
+ connectionId: string;
+ content: string;
+ error: string | null;
+ eventType: GrpcEventType;
+ metadata: { [key in string]?: string };
+ status: number | null;
+};
-export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
+export type GrpcEventType =
+ | "info"
+ | "error"
+ | "client_message"
+ | "server_message"
+ | "connection_start"
+ | "connection_end";
-export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number,
-/**
- * Server URL (http for plaintext or https for secure)
- */
-url: string, };
+export type GrpcRequest = {
+ model: "grpc_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authenticationType: string | null;
+ authentication: Record;
+ description: string;
+ message: string;
+ metadata: Array;
+ method: string | null;
+ name: string;
+ service: string | null;
+ sortPriority: number;
+ /**
+ * Server URL (http for plaintext or https for secure)
+ */
+ url: string;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
-export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type HttpRequest = {
+ model: "http_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ body: Record;
+ bodyType: string | null;
+ description: string;
+ headers: Array;
+ method: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
+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, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array, 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;
+ remoteAddr: string | null;
+ requestContentLength: number | null;
+ requestHeaders: Array;
+ 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, };
+export type HttpResponseEvent = {
+ model: "http_response_event";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ responseId: string;
+ event: HttpResponseEventData;
+};
/**
* Serializable representation of HTTP response events for DB storage.
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
-export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array, duration: bigint, overridden: boolean, };
+export type HttpResponseEventData =
+ | { type: "setting"; name: string; value: string }
+ | { type: "info"; message: string }
+ | {
+ type: "redirect";
+ url: string;
+ status: number;
+ behavior: string;
+ dropped_body: boolean;
+ dropped_headers: Array;
+ }
+ | {
+ type: "send_url";
+ method: string;
+ scheme: string;
+ username: string;
+ password: string;
+ host: string;
+ port: number;
+ path: string;
+ query: string;
+ fragment: string;
+ }
+ | { type: "receive_url"; version: string; status: string }
+ | { type: "header_up"; name: string; value: string }
+ | { type: "header_down"; name: string; value: string }
+ | { type: "chunk_sent"; bytes: number }
+ | { type: "chunk_received"; bytes: number }
+ | {
+ type: "dns_resolved";
+ hostname: string;
+ addresses: Array;
+ duration: bigint;
+ overridden: boolean;
+ };
-export type HttpResponseHeader = { name: string, value: string, };
+export type HttpResponseHeader = { name: string; value: string };
export type HttpResponseState = "initialized" | "connected" | "closed";
-export type HttpUrlParameter = { enabled?: boolean,
-/**
- * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
- * Other entries are appended as query parameters
- */
-name: string, value: string, id?: string, };
+export type HttpUrlParameter = {
+ enabled?: boolean;
+ /**
+ * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
+ * Other entries are appended as query parameters
+ */
+ name: string;
+ value: string;
+ id?: string;
+};
-export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
+export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
-export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
+export type InheritedIntSetting = { enabled?: boolean; value: number };
-export type ParentAuthentication = { authentication: Record, authenticationType: string | null, };
+export type KeyValue = {
+ model: "key_value";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ key: string;
+ namespace: string;
+ value: string;
+};
-export type ParentHeaders = { headers: Array, };
+export type ModelPayload = {
+ model: AnyModel;
+ updateSource: UpdateSource;
+ change: ModelChangeEvent;
+};
-export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
+export type ParentAuthentication = {
+ authentication: Record;
+ authenticationType: string | null;
+};
-export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
+export type ParentHeaders = { headers: Array };
+
+export type Plugin = {
+ model: "plugin";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ checkedAt: string | null;
+ directory: string;
+ enabled: boolean;
+ url: string | null;
+ source: PluginSource;
+};
+
+export type PluginKeyValue = {
+ model: "plugin_key_value";
+ createdAt: string;
+ updatedAt: string;
+ pluginName: string;
+ key: string;
+ value: string;
+};
export type PluginSource = "bundled" | "filesystem" | "registry";
-export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
+export type ProxySetting =
+ | {
+ type: "enabled";
+ http: string;
+ https: string;
+ auth: ProxySettingAuth | null;
+ bypass: string;
+ disabled: boolean;
+ }
+ | { type: "disabled" };
-export type ProxySettingAuth = { user: string, password: string, };
+export type ProxySettingAuth = { user: string; password: string };
-export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, hotkeys: { [key in string]?: Array }, };
+export type Settings = {
+ model: "settings";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ appearance: string;
+ clientCertificates: Array;
+ coloredMethods: boolean;
+ editorFont: string | null;
+ editorFontSize: number;
+ editorKeymap: EditorKeymap;
+ editorSoftWrap: boolean;
+ hideWindowControls: boolean;
+ useNativeTitlebar: boolean;
+ interfaceFont: string | null;
+ interfaceFontSize: number;
+ interfaceScale: number;
+ openWorkspaceNewWindow: boolean | null;
+ proxy: ProxySetting | null;
+ themeDark: string;
+ themeLight: string;
+ updateChannel: string;
+ hideLicenseBadge: boolean;
+ autoupdate: boolean;
+ autoDownloadUpdates: boolean;
+ checkNotifications: boolean;
+ hotkeys: { [key in string]?: Array };
+};
-export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
+export type SyncState = {
+ model: "sync_state";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ flushedAt: string;
+ modelId: string;
+ checksum: string;
+ relPath: string;
+ syncDir: string;
+};
-export type UpdateSource = { "type": "background" } | { "type": "import" } | { "type": "plugin" } | { "type": "sync" } | { "type": "window", label: string, };
+export type UpdateSource =
+ | { type: "background" }
+ | { type: "import" }
+ | { type: "plugin" }
+ | { type: "sync" }
+ | { type: "window"; label: string };
-export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array, state: WebsocketConnectionState, status: number, url: string, };
+export type WebsocketConnection = {
+ model: "websocket_connection";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ requestId: string;
+ elapsed: number;
+ error: string | null;
+ headers: Array;
+ state: WebsocketConnectionState;
+ status: number;
+ url: string;
+};
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
-export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array, messageType: WebsocketEventType, };
+export type WebsocketEvent = {
+ model: "websocket_event";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ requestId: string;
+ connectionId: string;
+ isServer: boolean;
+ message: Array;
+ messageType: WebsocketEventType;
+};
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketMessageType = "text" | "binary";
-export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type WebsocketRequest = {
+ model: "websocket_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ message: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
-export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array, };
+export type Workspace = {
+ model: "workspace";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ encryptionKeyChallenge: string | null;
+ settingValidateCertificates: boolean;
+ settingFollowRedirects: boolean;
+ settingRequestTimeout: number;
+ settingDnsOverrides: Array;
+ settingSendCookies: boolean;
+ settingStoreCookies: boolean;
+};
-export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
+export type WorkspaceMeta = {
+ model: "workspace_meta";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ encryptionKey: EncryptedKey | null;
+ settingSyncDir: string | null;
+};
diff --git a/crates/yaak-models/migrations/20260302000000_inherited-request-settings.sql b/crates/yaak-models/migrations/20260302000000_inherited-request-settings.sql
new file mode 100644
index 00000000..84a9a8e3
--- /dev/null
+++ b/crates/yaak-models/migrations/20260302000000_inherited-request-settings.sql
@@ -0,0 +1,20 @@
+ALTER TABLE workspaces ADD COLUMN setting_send_cookies BOOLEAN DEFAULT TRUE NOT NULL;
+ALTER TABLE workspaces ADD COLUMN setting_store_cookies BOOLEAN DEFAULT TRUE NOT NULL;
+
+ALTER TABLE folders ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE folders ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE folders ADD COLUMN setting_request_timeout TEXT DEFAULT '{"enabled":false,"value":0}' NOT NULL;
+ALTER TABLE folders ADD COLUMN setting_validate_certificates TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE folders ADD COLUMN setting_follow_redirects TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+
+ALTER TABLE http_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE http_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE http_requests ADD COLUMN setting_request_timeout TEXT DEFAULT '{"enabled":false,"value":0}' NOT NULL;
+ALTER TABLE http_requests ADD COLUMN setting_validate_certificates TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE http_requests ADD COLUMN setting_follow_redirects TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+
+ALTER TABLE websocket_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE websocket_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+
+ALTER TABLE grpc_requests ADD COLUMN setting_send_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
+ALTER TABLE grpc_requests ADD COLUMN setting_store_cookies TEXT DEFAULT '{"enabled":false,"value":true}' NOT NULL;
diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs
index 1b47ba79..3050c725 100644
--- a/crates/yaak-models/src/models.rs
+++ b/crates/yaak-models/src/models.rs
@@ -1,7 +1,9 @@
use crate::error::Result;
use crate::models::HttpRequestIden::{
Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers,
- Method, Name, SortPriority, UpdatedAt, Url, UrlParameters, WorkspaceId,
+ Method, Name, SettingFollowRedirects, SettingRequestTimeout, SettingSendCookies,
+ SettingStoreCookies, SettingValidateCertificates, SortPriority, UpdatedAt, Url, UrlParameters,
+ WorkspaceId,
};
use crate::util::generate_prefixed_id;
use chrono::{NaiveDateTime, Utc};
@@ -90,6 +92,61 @@ pub struct DnsOverride {
pub enabled: bool,
}
+#[derive(Debug, Clone, PartialEq)]
+pub struct ResolvedHttpRequestSettings {
+ pub validate_certificates: bool,
+ pub follow_redirects: bool,
+ pub request_timeout: i32,
+ pub send_cookies: bool,
+ pub store_cookies: bool,
+}
+
+impl Default for ResolvedHttpRequestSettings {
+ fn default() -> Self {
+ Self {
+ validate_certificates: true,
+ follow_redirects: true,
+ request_timeout: 0,
+ send_cookies: true,
+ store_cookies: true,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
+#[serde(default, rename_all = "camelCase")]
+#[ts(export, export_to = "gen_models.ts")]
+pub struct InheritedBoolSetting {
+ #[serde(default)]
+ #[ts(optional, as = "Option")]
+ pub enabled: bool,
+ #[serde(default = "default_true")]
+ pub value: bool,
+}
+
+impl Default for InheritedBoolSetting {
+ fn default() -> Self {
+ Self { enabled: false, value: true }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
+#[serde(default, rename_all = "camelCase")]
+#[ts(export, export_to = "gen_models.ts")]
+pub struct InheritedIntSetting {
+ #[serde(default)]
+ #[ts(optional, as = "Option")]
+ pub enabled: bool,
+ #[serde(default)]
+ pub value: i32,
+}
+
+impl Default for InheritedIntSetting {
+ fn default() -> Self {
+ Self { enabled: false, value: 0 }
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_models.ts")]
@@ -322,6 +379,10 @@ pub struct Workspace {
pub setting_request_timeout: i32,
#[serde(default)]
pub setting_dns_overrides: Vec,
+ #[serde(default = "default_true")]
+ pub setting_send_cookies: bool,
+ #[serde(default = "default_true")]
+ pub setting_store_cookies: bool,
}
impl UpsertModelInfo for Workspace {
@@ -363,6 +424,8 @@ impl UpsertModelInfo for Workspace {
(SettingRequestTimeout, self.setting_request_timeout.into()),
(SettingValidateCertificates, self.setting_validate_certificates.into()),
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
+ (SettingSendCookies, self.setting_send_cookies.into()),
+ (SettingStoreCookies, self.setting_store_cookies.into()),
])
}
@@ -380,6 +443,8 @@ impl UpsertModelInfo for Workspace {
WorkspaceIden::SettingRequestTimeout,
WorkspaceIden::SettingValidateCertificates,
WorkspaceIden::SettingDnsOverrides,
+ WorkspaceIden::SettingSendCookies,
+ WorkspaceIden::SettingStoreCookies,
]
}
@@ -405,6 +470,8 @@ impl UpsertModelInfo for Workspace {
setting_request_timeout: row.get("setting_request_timeout")?,
setting_validate_certificates: row.get("setting_validate_certificates")?,
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
+ setting_send_cookies: row.get("setting_send_cookies")?,
+ setting_store_cookies: row.get("setting_store_cookies")?,
})
}
}
@@ -509,11 +576,127 @@ pub enum CookieExpires {
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "gen_models.ts")]
+pub enum CookieSameSite {
+ Strict,
+ Lax,
+ None,
+}
+
+#[derive(Debug, Clone, Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "gen_models.ts")]
pub struct Cookie {
- pub raw_cookie: String,
+ pub name: String,
+ pub value: String,
pub domain: CookieDomain,
pub expires: CookieExpires,
- pub path: (String, bool),
+ pub path: String,
+ pub secure: bool,
+ pub http_only: bool,
+ pub same_site: Option,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct CookieFields {
+ name: String,
+ value: String,
+ domain: CookieDomain,
+ expires: CookieExpires,
+ path: String,
+ #[serde(default)]
+ secure: bool,
+ #[serde(default)]
+ http_only: bool,
+ #[serde(default)]
+ same_site: Option,
+}
+
+#[derive(Deserialize)]
+struct LegacyCookie {
+ raw_cookie: String,
+ domain: CookieDomain,
+ expires: CookieExpires,
+ path: (String, bool),
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum CookieCompat {
+ New(CookieFields),
+ Legacy(LegacyCookie),
+}
+
+impl<'de> Deserialize<'de> for Cookie {
+ fn deserialize(deserializer: D) -> std::result::Result
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(match CookieCompat::deserialize(deserializer)? {
+ CookieCompat::New(cookie) => Self {
+ name: cookie.name,
+ value: cookie.value,
+ domain: cookie.domain,
+ expires: cookie.expires,
+ path: cookie.path,
+ secure: cookie.secure,
+ http_only: cookie.http_only,
+ same_site: cookie.same_site,
+ },
+ CookieCompat::Legacy(cookie) => {
+ let (name, value, secure, http_only, same_site) =
+ parse_legacy_cookie_parts(&cookie.raw_cookie);
+ Self {
+ name,
+ value,
+ domain: cookie.domain,
+ expires: cookie.expires,
+ path: cookie.path.0,
+ secure,
+ http_only,
+ same_site,
+ }
+ }
+ })
+ }
+}
+
+fn parse_legacy_cookie_parts(
+ raw_cookie: &str,
+) -> (String, String, bool, bool, Option) {
+ let mut parts = raw_cookie.split(';').map(str::trim);
+ let (name, value) = parts
+ .next()
+ .and_then(|part| {
+ let mut nv = part.splitn(2, '=');
+ Some((nv.next()?.trim().to_string(), nv.next().unwrap_or("").trim().to_string()))
+ })
+ .unwrap_or_default();
+
+ let mut secure = false;
+ let mut http_only = false;
+ let mut same_site = None;
+
+ for part in parts {
+ let mut attr = part.splitn(2, '=');
+ let key = attr.next().unwrap_or("").trim().to_lowercase();
+ let value = attr.next().unwrap_or("").trim().to_lowercase();
+ match key.as_str() {
+ "secure" => secure = true,
+ "httponly" => http_only = true,
+ "samesite" => {
+ same_site = match value.as_str() {
+ "strict" => Some(CookieSameSite::Strict),
+ "lax" => Some(CookieSameSite::Lax),
+ "none" => Some(CookieSameSite::None),
+ _ => same_site,
+ };
+ }
+ _ => {}
+ }
+ }
+
+ (name, value, secure, http_only, same_site)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
@@ -751,6 +934,11 @@ pub struct Folder {
pub headers: Vec,
pub name: String,
pub sort_priority: f64,
+ pub setting_send_cookies: InheritedBoolSetting,
+ pub setting_store_cookies: InheritedBoolSetting,
+ pub setting_validate_certificates: InheritedBoolSetting,
+ pub setting_follow_redirects: InheritedBoolSetting,
+ pub setting_request_timeout: InheritedIntSetting,
}
impl UpsertModelInfo for Folder {
@@ -790,6 +978,14 @@ impl UpsertModelInfo for Folder {
(Description, self.description.into()),
(Name, self.name.trim().into()),
(SortPriority, self.sort_priority.into()),
+ (SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
+ (SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
+ (
+ SettingValidateCertificates,
+ serde_json::to_string(&self.setting_validate_certificates)?.into(),
+ ),
+ (SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
+ (SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
])
}
@@ -803,6 +999,11 @@ impl UpsertModelInfo for Folder {
FolderIden::Description,
FolderIden::FolderId,
FolderIden::SortPriority,
+ FolderIden::SettingSendCookies,
+ FolderIden::SettingStoreCookies,
+ FolderIden::SettingValidateCertificates,
+ FolderIden::SettingFollowRedirects,
+ FolderIden::SettingRequestTimeout,
]
}
@@ -812,6 +1013,11 @@ impl UpsertModelInfo for Folder {
{
let headers: String = row.get("headers")?;
let authentication: String = row.get("authentication")?;
+ let setting_send_cookies: String = row.get("setting_send_cookies")?;
+ let setting_store_cookies: String = row.get("setting_store_cookies")?;
+ let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
+ let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
+ let setting_request_timeout: String = row.get("setting_request_timeout")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -825,6 +1031,14 @@ impl UpsertModelInfo for Folder {
headers: serde_json::from_str(&headers).unwrap_or_default(),
authentication_type: row.get("authentication_type")?,
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
+ setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
+ setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
+ setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
+ .unwrap_or_default(),
+ setting_follow_redirects: serde_json::from_str(&setting_follow_redirects)
+ .unwrap_or_default(),
+ setting_request_timeout: serde_json::from_str(&setting_request_timeout)
+ .unwrap_or_default(),
})
}
}
@@ -885,6 +1099,11 @@ pub struct HttpRequest {
pub url: String,
/// URL parameters used for both path placeholders (`:id`) and query string entries.
pub url_parameters: Vec,
+ pub setting_send_cookies: InheritedBoolSetting,
+ pub setting_store_cookies: InheritedBoolSetting,
+ pub setting_validate_certificates: InheritedBoolSetting,
+ pub setting_follow_redirects: InheritedBoolSetting,
+ pub setting_request_timeout: InheritedIntSetting,
}
impl UpsertModelInfo for HttpRequest {
@@ -928,6 +1147,14 @@ impl UpsertModelInfo for HttpRequest {
(AuthenticationType, self.authentication_type.into()),
(Headers, serde_json::to_string(&self.headers)?.into()),
(SortPriority, self.sort_priority.into()),
+ (SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
+ (SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
+ (
+ SettingValidateCertificates,
+ serde_json::to_string(&self.setting_validate_certificates)?.into(),
+ ),
+ (SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
+ (SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
])
}
@@ -947,6 +1174,11 @@ impl UpsertModelInfo for HttpRequest {
Url,
UrlParameters,
SortPriority,
+ SettingSendCookies,
+ SettingStoreCookies,
+ SettingValidateCertificates,
+ SettingFollowRedirects,
+ SettingRequestTimeout,
]
}
@@ -955,6 +1187,11 @@ impl UpsertModelInfo for HttpRequest {
let body: String = row.get("body")?;
let authentication: String = row.get("authentication")?;
let headers: String = row.get("headers")?;
+ let setting_send_cookies: String = row.get("setting_send_cookies")?;
+ let setting_store_cookies: String = row.get("setting_store_cookies")?;
+ let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
+ let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
+ let setting_request_timeout: String = row.get("setting_request_timeout")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -973,6 +1210,14 @@ impl UpsertModelInfo for HttpRequest {
sort_priority: row.get("sort_priority")?,
url: row.get("url")?,
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
+ setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
+ setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
+ setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
+ .unwrap_or_default(),
+ setting_follow_redirects: serde_json::from_str(&setting_follow_redirects)
+ .unwrap_or_default(),
+ setting_request_timeout: serde_json::from_str(&setting_request_timeout)
+ .unwrap_or_default(),
})
}
}
@@ -1127,6 +1372,8 @@ pub struct WebsocketRequest {
pub url: String,
/// URL parameters used for both path placeholders (`:id`) and query string entries.
pub url_parameters: Vec,
+ pub setting_send_cookies: InheritedBoolSetting,
+ pub setting_store_cookies: InheritedBoolSetting,
}
impl UpsertModelInfo for WebsocketRequest {
@@ -1169,6 +1416,8 @@ impl UpsertModelInfo for WebsocketRequest {
(SortPriority, self.sort_priority.into()),
(Url, self.url.into()),
(UrlParameters, serde_json::to_string(&self.url_parameters)?.into()),
+ (SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
+ (SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
])
}
@@ -1186,6 +1435,8 @@ impl UpsertModelInfo for WebsocketRequest {
WebsocketRequestIden::SortPriority,
WebsocketRequestIden::Url,
WebsocketRequestIden::UrlParameters,
+ WebsocketRequestIden::SettingSendCookies,
+ WebsocketRequestIden::SettingStoreCookies,
]
}
@@ -1196,6 +1447,8 @@ impl UpsertModelInfo for WebsocketRequest {
let url_parameters: String = row.get("url_parameters")?;
let authentication: String = row.get("authentication")?;
let headers: String = row.get("headers")?;
+ let setting_send_cookies: String = row.get("setting_send_cookies")?;
+ let setting_store_cookies: String = row.get("setting_store_cookies")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -1212,6 +1465,8 @@ impl UpsertModelInfo for WebsocketRequest {
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
folder_id: row.get("folder_id")?,
name: row.get("name")?,
+ setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
+ setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
})
}
}
@@ -1742,6 +1997,8 @@ pub struct GrpcRequest {
pub sort_priority: f64,
/// Server URL (http for plaintext or https for secure)
pub url: String,
+ pub setting_send_cookies: InheritedBoolSetting,
+ pub setting_store_cookies: InheritedBoolSetting,
}
impl UpsertModelInfo for GrpcRequest {
@@ -1785,6 +2042,8 @@ impl UpsertModelInfo for GrpcRequest {
(AuthenticationType, self.authentication_type.into()),
(Authentication, serde_json::to_string(&self.authentication)?.into()),
(Metadata, serde_json::to_string(&self.metadata)?.into()),
+ (SettingSendCookies, serde_json::to_string(&self.setting_send_cookies)?.into()),
+ (SettingStoreCookies, serde_json::to_string(&self.setting_store_cookies)?.into()),
])
}
@@ -1803,6 +2062,8 @@ impl UpsertModelInfo for GrpcRequest {
GrpcRequestIden::AuthenticationType,
GrpcRequestIden::Authentication,
GrpcRequestIden::Metadata,
+ GrpcRequestIden::SettingSendCookies,
+ GrpcRequestIden::SettingStoreCookies,
]
}
@@ -1812,6 +2073,8 @@ impl UpsertModelInfo for GrpcRequest {
{
let authentication: String = row.get("authentication")?;
let metadata: String = row.get("metadata")?;
+ let setting_send_cookies: String = row.get("setting_send_cookies")?;
+ let setting_store_cookies: String = row.get("setting_store_cookies")?;
Ok(Self {
id: row.get("id")?,
model: row.get("model")?,
@@ -1829,6 +2092,8 @@ impl UpsertModelInfo for GrpcRequest {
url: row.get("url")?,
sort_priority: row.get("sort_priority")?,
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
+ setting_send_cookies: serde_json::from_str(&setting_send_cookies).unwrap_or_default(),
+ setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
})
}
}
diff --git a/crates/yaak-models/src/queries/folders.rs b/crates/yaak-models/src/queries/folders.rs
index 702cae21..75ece683 100644
--- a/crates/yaak-models/src/queries/folders.rs
+++ b/crates/yaak-models/src/queries/folders.rs
@@ -3,7 +3,8 @@ use crate::connection_or_tx::ConnectionOrTx;
use crate::error::Result;
use crate::models::{
Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest,
- HttpRequestHeader, HttpRequestIden, WebsocketRequest, WebsocketRequestIden,
+ HttpRequestHeader, HttpRequestIden, ResolvedHttpRequestSettings, WebsocketRequest,
+ WebsocketRequestIden,
};
use crate::util::UpdateSource;
use serde_json::Value;
@@ -141,4 +142,45 @@ impl<'a> ClientDb<'a> {
Ok(headers)
}
+
+ pub fn resolve_settings_for_folder(
+ &self,
+ folder: &Folder,
+ ) -> Result {
+ let parent = if let Some(folder_id) = folder.folder_id.clone() {
+ let parent_folder = self.get_folder(&folder_id)?;
+ self.resolve_settings_for_folder(&parent_folder)?
+ } else {
+ let workspace = self.get_workspace(&folder.workspace_id)?;
+ self.resolve_settings_for_workspace(&workspace)
+ };
+
+ Ok(ResolvedHttpRequestSettings {
+ validate_certificates: if folder.setting_validate_certificates.enabled {
+ folder.setting_validate_certificates.value
+ } else {
+ parent.validate_certificates
+ },
+ follow_redirects: if folder.setting_follow_redirects.enabled {
+ folder.setting_follow_redirects.value
+ } else {
+ parent.follow_redirects
+ },
+ request_timeout: if folder.setting_request_timeout.enabled {
+ folder.setting_request_timeout.value
+ } else {
+ parent.request_timeout
+ },
+ send_cookies: if folder.setting_send_cookies.enabled {
+ folder.setting_send_cookies.value
+ } else {
+ parent.send_cookies
+ },
+ store_cookies: if folder.setting_store_cookies.enabled {
+ folder.setting_store_cookies.value
+ } else {
+ parent.store_cookies
+ },
+ })
+ }
}
diff --git a/crates/yaak-models/src/queries/http_requests.rs b/crates/yaak-models/src/queries/http_requests.rs
index 46286f4f..22e337eb 100644
--- a/crates/yaak-models/src/queries/http_requests.rs
+++ b/crates/yaak-models/src/queries/http_requests.rs
@@ -1,7 +1,10 @@
use super::dedupe_headers;
use crate::client_db::ClientDb;
use crate::error::Result;
-use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
+use crate::models::{
+ Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden,
+ ResolvedHttpRequestSettings,
+};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
@@ -91,6 +94,47 @@ impl<'a> ClientDb<'a> {
Ok(dedupe_headers(headers))
}
+ pub fn resolve_settings_for_http_request(
+ &self,
+ http_request: &HttpRequest,
+ ) -> Result {
+ let parent = if let Some(folder_id) = http_request.folder_id.clone() {
+ let folder = self.get_folder(&folder_id)?;
+ self.resolve_settings_for_folder(&folder)?
+ } else {
+ let workspace = self.get_workspace(&http_request.workspace_id)?;
+ self.resolve_settings_for_workspace(&workspace)
+ };
+
+ Ok(ResolvedHttpRequestSettings {
+ validate_certificates: if http_request.setting_validate_certificates.enabled {
+ http_request.setting_validate_certificates.value
+ } else {
+ parent.validate_certificates
+ },
+ follow_redirects: if http_request.setting_follow_redirects.enabled {
+ http_request.setting_follow_redirects.value
+ } else {
+ parent.follow_redirects
+ },
+ request_timeout: if http_request.setting_request_timeout.enabled {
+ http_request.setting_request_timeout.value
+ } else {
+ parent.request_timeout
+ },
+ send_cookies: if http_request.setting_send_cookies.enabled {
+ http_request.setting_send_cookies.value
+ } else {
+ parent.send_cookies
+ },
+ store_cookies: if http_request.setting_store_cookies.enabled {
+ http_request.setting_store_cookies.value
+ } else {
+ parent.store_cookies
+ },
+ })
+ }
+
pub fn list_http_requests_for_folder_recursive(
&self,
folder_id: &str,
diff --git a/crates/yaak-models/src/queries/websocket_requests.rs b/crates/yaak-models/src/queries/websocket_requests.rs
index 1e449d5a..598100a3 100644
--- a/crates/yaak-models/src/queries/websocket_requests.rs
+++ b/crates/yaak-models/src/queries/websocket_requests.rs
@@ -2,7 +2,8 @@ use super::dedupe_headers;
use crate::client_db::ClientDb;
use crate::error::Result;
use crate::models::{
- Folder, FolderIden, HttpRequestHeader, WebsocketRequest, WebsocketRequestIden,
+ Folder, FolderIden, HttpRequestHeader, ResolvedHttpRequestSettings, WebsocketRequest,
+ WebsocketRequestIden,
};
use crate::util::UpdateSource;
use serde_json::Value;
@@ -116,4 +117,31 @@ impl<'a> ClientDb<'a> {
Ok(dedupe_headers(headers))
}
+
+ pub fn resolve_settings_for_websocket_request(
+ &self,
+ websocket_request: &WebsocketRequest,
+ ) -> Result {
+ let parent = if let Some(folder_id) = websocket_request.folder_id.clone() {
+ let folder = self.get_folder(&folder_id)?;
+ self.resolve_settings_for_folder(&folder)?
+ } else {
+ let workspace = self.get_workspace(&websocket_request.workspace_id)?;
+ self.resolve_settings_for_workspace(&workspace)
+ };
+
+ Ok(ResolvedHttpRequestSettings {
+ send_cookies: if websocket_request.setting_send_cookies.enabled {
+ websocket_request.setting_send_cookies.value
+ } else {
+ parent.send_cookies
+ },
+ store_cookies: if websocket_request.setting_store_cookies.enabled {
+ websocket_request.setting_store_cookies.value
+ } else {
+ parent.store_cookies
+ },
+ ..parent
+ })
+ }
}
diff --git a/crates/yaak-models/src/queries/workspaces.rs b/crates/yaak-models/src/queries/workspaces.rs
index ecb8a3d1..118483bb 100644
--- a/crates/yaak-models/src/queries/workspaces.rs
+++ b/crates/yaak-models/src/queries/workspaces.rs
@@ -2,7 +2,7 @@ use crate::client_db::ClientDb;
use crate::error::Result;
use crate::models::{
EnvironmentIden, FolderIden, GrpcRequestIden, HttpRequestHeader, HttpRequestIden,
- WebsocketRequestIden, Workspace, WorkspaceIden,
+ ResolvedHttpRequestSettings, WebsocketRequestIden, Workspace, WorkspaceIden,
};
use crate::util::UpdateSource;
use serde_json::Value;
@@ -84,6 +84,19 @@ impl<'a> ClientDb<'a> {
headers.extend(workspace.headers.clone());
headers
}
+
+ pub fn resolve_settings_for_workspace(
+ &self,
+ workspace: &Workspace,
+ ) -> ResolvedHttpRequestSettings {
+ ResolvedHttpRequestSettings {
+ validate_certificates: workspace.setting_validate_certificates,
+ follow_redirects: workspace.setting_follow_redirects,
+ request_timeout: workspace.setting_request_timeout,
+ send_cookies: workspace.setting_send_cookies,
+ store_cookies: workspace.setting_store_cookies,
+ }
+ }
}
/// Global default headers that are always sent with requests unless overridden.
diff --git a/crates/yaak-sync/bindings/gen_models.ts b/crates/yaak-sync/bindings/gen_models.ts
index acb48761..73924741 100644
--- a/crates/yaak-sync/bindings/gen_models.ts
+++ b/crates/yaak-sync/bindings/gen_models.ts
@@ -1,47 +1,181 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type DnsOverride = { hostname: string, ipv4: Array, ipv6: Array, enabled?: boolean, };
+export type DnsOverride = {
+ hostname: string;
+ ipv4: Array;
+ ipv6: Array;
+ enabled?: boolean;
+};
-export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
-/**
- * Variables defined in this environment scope.
- * Child environments override parent variables by name.
- */
-variables: Array, color: string | null, sortPriority: number, };
+export type Environment = {
+ model: "environment";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ name: string;
+ public: boolean;
+ parentModel: string;
+ parentId: string | null;
+ /**
+ * Variables defined in this environment scope.
+ * Child environments override parent variables by name.
+ */
+ variables: Array;
+ color: string | null;
+ sortPriority: number;
+};
-export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
+export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string };
-export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, };
+export type Folder = {
+ model: "folder";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ sortPriority: number;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number,
-/**
- * Server URL (http for plaintext or https for secure)
- */
-url: string, };
+export type GrpcRequest = {
+ model: "grpc_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authenticationType: string | null;
+ authentication: Record;
+ description: string;
+ message: string;
+ metadata: Array;
+ method: string | null;
+ name: string;
+ service: string | null;
+ sortPriority: number;
+ /**
+ * Server URL (http for plaintext or https for secure)
+ */
+ url: string;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
-export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type HttpRequest = {
+ model: "http_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ body: Record;
+ bodyType: string | null;
+ description: string;
+ headers: Array;
+ method: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+ settingValidateCertificates: InheritedBoolSetting;
+ settingFollowRedirects: InheritedBoolSetting;
+ settingRequestTimeout: InheritedIntSetting;
+};
-export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
+export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string };
-export type HttpUrlParameter = { enabled?: boolean,
-/**
- * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
- * Other entries are appended as query parameters
- */
-name: string, value: string, id?: string, };
+export type HttpUrlParameter = {
+ enabled?: boolean;
+ /**
+ * Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
+ * Other entries are appended as query parameters
+ */
+ name: string;
+ value: string;
+ id?: string;
+};
-export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest;
+export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
-export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
+export type InheritedIntSetting = { enabled?: boolean; value: number };
-export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string,
-/**
- * URL parameters used for both path placeholders (`:id`) and query string entries.
- */
-urlParameters: Array, };
+export type SyncModel =
+ | ({ type: "workspace" } & Workspace)
+ | ({ type: "environment" } & Environment)
+ | ({ type: "folder" } & Folder)
+ | ({ type: "http_request" } & HttpRequest)
+ | ({ type: "grpc_request" } & GrpcRequest)
+ | ({ type: "websocket_request" } & WebsocketRequest);
-export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array, };
+export type SyncState = {
+ model: "sync_state";
+ id: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ flushedAt: string;
+ modelId: string;
+ checksum: string;
+ relPath: string;
+ syncDir: string;
+};
+
+export type WebsocketRequest = {
+ model: "websocket_request";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ workspaceId: string;
+ folderId: string | null;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ message: string;
+ name: string;
+ sortPriority: number;
+ url: string;
+ /**
+ * URL parameters used for both path placeholders (`:id`) and query string entries.
+ */
+ urlParameters: Array;
+ settingSendCookies: InheritedBoolSetting;
+ settingStoreCookies: InheritedBoolSetting;
+};
+
+export type Workspace = {
+ model: "workspace";
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ authentication: Record;
+ authenticationType: string | null;
+ description: string;
+ headers: Array;
+ name: string;
+ encryptionKeyChallenge: string | null;
+ settingValidateCertificates: boolean;
+ settingFollowRedirects: boolean;
+ settingRequestTimeout: number;
+ settingDnsOverrides: Array;
+ settingSendCookies: boolean;
+ settingStoreCookies: boolean;
+};
diff --git a/crates/yaak/src/send.rs b/crates/yaak/src/send.rs
index 0db6021f..f1f2a6e1 100644
--- a/crates/yaak/src/send.rs
+++ b/crates/yaak/src/send.rs
@@ -115,10 +115,17 @@ pub trait SendRequestExecutor: Send + Sync {
&self,
sendable_request: SendableHttpRequest,
event_tx: mpsc::Sender,
- cookie_store: Option,
+ cookie_behavior: CookieBehavior,
) -> yaak_http::error::Result;
}
+#[derive(Clone)]
+pub struct CookieBehavior {
+ pub store: Option,
+ pub send_cookies: bool,
+ pub store_cookies: bool,
+}
+
struct DefaultSendRequestExecutor;
#[async_trait]
@@ -127,11 +134,16 @@ impl SendRequestExecutor for DefaultSendRequestExecutor {
&self,
sendable_request: SendableHttpRequest,
event_tx: mpsc::Sender,
- cookie_store: Option,
+ cookie_behavior: CookieBehavior,
) -> yaak_http::error::Result {
let sender = ReqwestSender::new()?;
- let transaction = match cookie_store {
- Some(store) => HttpTransaction::with_cookie_store(sender, store),
+ let transaction = match cookie_behavior.store {
+ Some(store) => HttpTransaction::with_cookie_behavior(
+ sender,
+ store,
+ cookie_behavior.send_cookies,
+ cookie_behavior.store_cookies,
+ ),
None => HttpTransaction::new(sender),
};
let (_cancel_tx, cancel_rx) = watch::channel(false);
@@ -182,7 +194,7 @@ struct ConnectionManagerSendRequestExecutor<'a> {
connection_manager: &'a HttpConnectionManager,
plugin_context_id: String,
query_manager: QueryManager,
- workspace_id: String,
+ request: HttpRequest,
cancelled_rx: Option>,
}
@@ -192,11 +204,10 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
&self,
sendable_request: SendableHttpRequest,
event_tx: mpsc::Sender,
- cookie_store: Option,
+ cookie_behavior: CookieBehavior,
) -> yaak_http::error::Result {
- let runtime_config =
- resolve_http_send_runtime_config(&self.query_manager, &self.workspace_id)
- .map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
+ let runtime_config = resolve_http_send_runtime_config(&self.query_manager, &self.request)
+ .map_err(|e| yaak_http::error::Error::RequestError(e.to_string()))?;
let client_certificate =
find_client_certificate(&sendable_request.url, &runtime_config.client_certificates);
let cached_client = self
@@ -213,8 +224,13 @@ impl SendRequestExecutor for ConnectionManagerSendRequestExecutor<'_> {
cached_client.resolver.set_event_sender(Some(event_tx.clone())).await;
let sender = ReqwestSender::with_client(cached_client.client);
- let transaction = match cookie_store {
- Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
+ let transaction = match cookie_behavior.store {
+ Some(cs) => HttpTransaction::with_cookie_behavior(
+ sender,
+ cs,
+ cookie_behavior.send_cookies,
+ cookie_behavior.store_cookies,
+ ),
None => HttpTransaction::new(sender),
};
@@ -315,24 +331,28 @@ pub struct HttpSendRuntimeConfig {
pub fn resolve_http_send_runtime_config(
query_manager: &QueryManager,
- workspace_id: &str,
+ request: &HttpRequest,
) -> Result {
let db = query_manager.connect();
- let workspace = db.get_workspace(workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?;
+ let workspace =
+ db.get_workspace(&request.workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?;
+ let resolved_settings = db
+ .resolve_settings_for_http_request(request)
+ .map_err(SendHttpRequestError::ResolveRequestInheritance)?;
let settings = db.get_settings();
Ok(HttpSendRuntimeConfig {
send_options: SendableHttpRequestOptions {
- follow_redirects: workspace.setting_follow_redirects,
- timeout: if workspace.setting_request_timeout > 0 {
+ follow_redirects: resolved_settings.follow_redirects,
+ timeout: if resolved_settings.request_timeout > 0 {
Some(std::time::Duration::from_millis(
- workspace.setting_request_timeout.unsigned_abs() as u64,
+ resolved_settings.request_timeout.unsigned_abs() as u64,
))
} else {
None
},
},
- validate_certificates: workspace.setting_validate_certificates,
+ validate_certificates: resolved_settings.validate_certificates,
proxy: proxy_setting_from_settings(settings.proxy),
dns_overrides: workspace.setting_dns_overrides,
client_certificates: settings.client_certificates,
@@ -387,7 +407,7 @@ pub async fn send_http_request_with_plugins(
connection_manager,
plugin_context_id: params.plugin_context.id.clone(),
query_manager: params.query_manager.clone(),
- workspace_id: params.request.workspace_id.clone(),
+ request: params.request.clone(),
cancelled_rx: params.cancelled_rx.clone(),
});
@@ -454,12 +474,21 @@ pub async fn send_http_request(
} else {
resolve_inherited_request(params.query_manager, ¶ms.request)?
};
- let runtime_config =
- resolve_http_send_runtime_config(params.query_manager, ¶ms.request.workspace_id)?;
+ let runtime_config = resolve_http_send_runtime_config(params.query_manager, ¶ms.request)?;
let send_options = params.send_options.unwrap_or(runtime_config.send_options);
+ let resolved_settings = params
+ .query_manager
+ .connect()
+ .resolve_settings_for_http_request(¶ms.request)
+ .map_err(SendHttpRequestError::ResolveRequestInheritance)?;
let mut cookie_jar = load_cookie_jar(params.query_manager, params.cookie_jar_id.as_deref())?;
let cookie_store =
cookie_jar.as_ref().map(|jar| CookieStore::from_cookies(jar.cookies.clone()));
+ let cookie_behavior = CookieBehavior {
+ store: cookie_store,
+ send_cookies: resolved_settings.send_cookies,
+ store_cookies: resolved_settings.store_cookies,
+ };
let rendered_request = render_http_request(
&resolved_request,
@@ -585,33 +614,35 @@ pub async fn send_http_request(
let started_at = Instant::now();
let request_started_url = sendable_request.url.clone();
- let mut http_response = match executor
- .send(sendable_request, event_tx, cookie_store.clone())
- .await
- {
- Ok(response) => response,
- Err(err) => {
- persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
- if persist_response {
- let _ = persist_response_error(
+ let mut http_response =
+ match executor.send(sendable_request, event_tx, cookie_behavior.clone()).await {
+ Ok(response) => response,
+ Err(err) => {
+ persist_cookie_jar(
params.query_manager,
- params.blob_manager,
- ¶ms.update_source,
- &response,
- started_at,
- err.to_string(),
- request_started_url,
- );
+ cookie_jar.as_mut(),
+ cookie_behavior.store.as_ref(),
+ )?;
+ if persist_response {
+ let _ = persist_response_error(
+ params.query_manager,
+ params.blob_manager,
+ ¶ms.update_source,
+ &response,
+ started_at,
+ err.to_string(),
+ request_started_url,
+ );
+ }
+ if let Err(join_err) = event_handle.await {
+ warn!("Failed to join response event task: {}", join_err);
+ }
+ if let Some(task) = request_body_capture_task.take() {
+ let _ = task.await;
+ }
+ return Err(SendHttpRequestError::SendRequest(err));
}
- if let Err(join_err) = event_handle.await {
- warn!("Failed to join response event task: {}", join_err);
- }
- if let Some(task) = request_body_capture_task.take() {
- let _ = task.await;
- }
- return Err(SendHttpRequestError::SendRequest(err));
- }
- };
+ };
let headers_elapsed = duration_to_i32(started_at.elapsed());
std::fs::create_dir_all(params.response_dir).map_err(|source| {
@@ -781,7 +812,11 @@ pub async fn send_http_request(
request_started_url,
);
}
- persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
+ persist_cookie_jar(
+ params.query_manager,
+ cookie_jar.as_mut(),
+ cookie_behavior.store.as_ref(),
+ )?;
return Err(err);
}
@@ -806,7 +841,7 @@ pub async fn send_http_request(
response = final_response;
}
- persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_store.as_ref())?;
+ persist_cookie_jar(params.query_manager, cookie_jar.as_mut(), cookie_behavior.store.as_ref())?;
Ok(SendHttpRequestResult { rendered_request, response, response_body })
}
diff --git a/packages/ui/src/components/Icon.tsx b/packages/ui/src/components/Icon.tsx
index 059eca6a..6dcaf194 100644
--- a/packages/ui/src/components/Icon.tsx
+++ b/packages/ui/src/components/Icon.tsx
@@ -91,8 +91,9 @@ import {
HomeIcon,
ImportIcon,
InfoIcon,
- KeyboardIcon,
KeyRoundIcon,
+ KeyboardIcon,
+ ListXIcon,
LockIcon,
LockOpenIcon,
MergeIcon,
@@ -131,12 +132,15 @@ import {
SunIcon,
TableIcon,
Trash2Icon,
+ Undo2Icon,
UploadIcon,
VariableIcon,
Wand2Icon,
WifiIcon,
WrenchIcon,
XIcon,
+ ZapIcon,
+ ZapOffIcon,
} from "lucide-react";
import type { CSSProperties, HTMLAttributes } from "react";
import { memo } from "react";
@@ -238,6 +242,7 @@ const icons = {
keyboard: KeyboardIcon,
left_panel_hidden: PanelLeftOpenIcon,
left_panel_visible: PanelLeftCloseIcon,
+ list_x: ListXIcon,
lock: LockIcon,
lock_open: LockOpenIcon,
magic_wand: Wand2Icon,
@@ -271,6 +276,7 @@ const icons = {
table: TableIcon,
text: FileTextIcon,
trash: Trash2Icon,
+ undo_2: Undo2Icon,
unpin: PinOffIcon,
update: RefreshCcwIcon,
upload: UploadIcon,
@@ -278,6 +284,8 @@ const icons = {
wifi: WifiIcon,
wrench: WrenchIcon,
x: XIcon,
+ zap: ZapIcon,
+ zap_off: ZapOffIcon,
_unknown: ShieldAlertIcon,
empty: (props: HTMLAttributes) =>
,
diff --git a/packages/ui/src/components/Table.tsx b/packages/ui/src/components/Table.tsx
index bd069c20..d06b6230 100644
--- a/packages/ui/src/components/Table.tsx
+++ b/packages/ui/src/components/Table.tsx
@@ -1,4 +1,5 @@
import classNames from "classnames";
+import type { HTMLAttributes } from "react";
import type { ReactNode } from "react";
export function Table({
@@ -28,9 +29,14 @@ export function Table({
);
}
-export function TableBody({ children }: { children: ReactNode }) {
+export function TableBody({ children, className }: { children: ReactNode; className?: string }) {
return (
-
+ tr:not(:last-child)>td]:border-b [&>tr:not(:last-child)>td]:border-b-surface-highlight",
+ )}
+ >
{children}
);
@@ -49,8 +55,19 @@ export function TableHead({ children, className }: { children: ReactNode; classN
);
}
-export function TableRow({ children }: { children: ReactNode }) {
- return {children} ;
+export function TableRow({
+ children,
+ className,
+ ...props
+}: {
+ children: ReactNode;
+ className?: string;
+} & HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
}
export function TableCell({
@@ -98,7 +115,7 @@ export function TableHeaderCell({
{children}