mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-20 05:29:46 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4092511f22 | |||
| 3de9a1edd4 | |||
| 1b28dfd9d1 | |||
| 9f51c61447 | |||
| b17ccbeebe | |||
| 463cc6f5a3 | |||
| 1307ea4e67 | |||
| 710b8e34ac | |||
| f251772a4a |
@@ -4,6 +4,7 @@ import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||
import { useState } from "react";
|
||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { showErrorToast } from "../lib/toast";
|
||||
import { Button } from "./core/Button";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
@@ -89,6 +90,10 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
<CommercialUseBanner source="git-clone" title="Using Git for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<PlainInput
|
||||
required
|
||||
label="Repository URL"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
||||
import { useEffect, useState } from "react";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||
|
||||
export function CommercialUseBanner({
|
||||
children,
|
||||
source,
|
||||
title,
|
||||
}: {
|
||||
children: string;
|
||||
source: string;
|
||||
title: string;
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
shouldShowCommercialUsePrompt()
|
||||
.then((shouldShow) => {
|
||||
if (!canceled) setVisible(shouldShow);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [source]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<DismissibleBanner
|
||||
id="commercial-use"
|
||||
color="primary"
|
||||
className="w-full"
|
||||
dismissForDays={7}
|
||||
actions={[
|
||||
{
|
||||
label: "View plans",
|
||||
color: "primary",
|
||||
variant: "solid",
|
||||
onClick: () => {
|
||||
openCommercialUsePricing(source).catch(console.error);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-text">{title}</p>
|
||||
<p className="mt-0.5 text-text-subtle">{children}</p>
|
||||
</div>
|
||||
</DismissibleBanner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
||||
// Open-source builds omit the Rust license plugin, so never show commercial-use prompts there.
|
||||
if (appInfo.featureLicense !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
||||
return license.status !== "active" && license.status !== "trialing";
|
||||
} catch (err) {
|
||||
console.log("Failed to check license before commercial-use prompt", err);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function openCommercialUsePricing(source: string): Promise<void> {
|
||||
await openUrl(`https://yaak.app/pricing?s=${source}&ref=app.yaak.desktop`).catch(console.error);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import slugify from "slugify";
|
||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||
import { pluralizeCount } from "../lib/pluralize";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { Button } from "./core/Button";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
import { DetailsBanner } from "./core/DetailsBanner";
|
||||
@@ -85,8 +86,12 @@ function ExportDataDialogContent({
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
const noneSelected = numSelected === 0;
|
||||
return (
|
||||
<div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<div className="h-full w-full grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden rounded-b-lg">
|
||||
<VStack space={3} className="overflow-auto px-5 pb-6">
|
||||
<CommercialUseBanner source="data-export" title="Exporting work data?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -137,9 +142,9 @@ function ExportDataDialogContent({
|
||||
/>
|
||||
</DetailsBanner>
|
||||
</VStack>
|
||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
|
||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface py-3 border-t border-border-subtle">
|
||||
<div>
|
||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
|
||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtlest">
|
||||
Create Run Button
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VStack } from "@yaakapp-internal/ui";
|
||||
import { useState } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { Button } from "./core/Button";
|
||||
import { SelectFile } from "./SelectFile";
|
||||
|
||||
@@ -14,6 +15,10 @@ export function ImportDataDialog({ importData }: Props) {
|
||||
|
||||
return (
|
||||
<VStack space={5} className="pb-4">
|
||||
<CommercialUseBanner source="data-import" title="Importing work data?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<VStack space={1}>
|
||||
<ul className="list-disc pl-5">
|
||||
<li>OpenAPI 3.0, 3.1</li>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useRef } from "react";
|
||||
import { showConfirmDelete } from "../../lib/confirm";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { Button } from "../core/Button";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
import { DetailsBanner } from "../core/DetailsBanner";
|
||||
@@ -232,6 +233,10 @@ export function SettingsCertificates() {
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<CommercialUseBanner source="client-certificates" title="Using certificates for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
{certificates.length > 0 && (
|
||||
<VStack space={3}>
|
||||
{certificates.map((cert, index) => (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../lib/requestSettings";
|
||||
import { revealInFinderText } from "../../lib/reveal";
|
||||
import { CargoFeature } from "../CargoFeature";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { IconButton } from "../core/IconButton";
|
||||
import {
|
||||
ModelSettingRowBoolean,
|
||||
@@ -38,10 +39,15 @@ export function SettingsGeneral() {
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<Heading>General</Heading>
|
||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||
</div>
|
||||
<div className="mt-3 mb-5">
|
||||
<CommercialUseBanner source="settings-general" title="Using Yaak for work?">
|
||||
A Yaak license is required for commercial use and helps support future development.
|
||||
</CommercialUseBanner>
|
||||
</div>
|
||||
<SettingsList className="space-y-8">
|
||||
<CargoFeature feature="updater">
|
||||
<SettingsSection title="Updates">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||
import type { ProxySetting } from "@yaakapp-internal/models";
|
||||
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import {
|
||||
SettingRowBoolean,
|
||||
SettingRowSelect,
|
||||
@@ -33,6 +34,9 @@ export function SettingsProxy() {
|
||||
traffic, or routing through specific infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
<CommercialUseBanner source="proxy-settings" title="Using a proxy for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
<SettingsList className="space-y-8">
|
||||
<SettingsSection title="Proxy">
|
||||
<SettingRowSelect
|
||||
|
||||
@@ -1,57 +1,84 @@
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import type { BannerProps } from "@yaakapp-internal/ui";
|
||||
import { Banner, HStack } from "@yaakapp-internal/ui";
|
||||
import { Banner } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function DismissibleBanner({
|
||||
children,
|
||||
className,
|
||||
dismissForDays,
|
||||
id,
|
||||
actions,
|
||||
...props
|
||||
}: BannerProps & {
|
||||
id: string;
|
||||
actions?: { label: string; onClick: () => void; color?: Color }[];
|
||||
dismissForDays?: number;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
color?: Color;
|
||||
variant?: ButtonProps["variant"];
|
||||
}[];
|
||||
}) {
|
||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
||||
const {
|
||||
isLoading,
|
||||
set: setDismissed,
|
||||
value: dismissed,
|
||||
} = useKeyValue<boolean | string>({
|
||||
namespace: "global",
|
||||
key: ["dismiss-banner", id],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
if (dismissed) return null;
|
||||
if (isLoading || isDismissed(dismissed, dismissForDays)) return null;
|
||||
|
||||
return (
|
||||
<Banner
|
||||
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<HStack space={1.5}>
|
||||
{actions?.map((a) => (
|
||||
<Button
|
||||
key={a.label}
|
||||
variant="border"
|
||||
color={a.color ?? props.color}
|
||||
size="xs"
|
||||
onClick={a.onClick}
|
||||
title={a.label}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => setDismissed((d) => !d)}
|
||||
title="Dismiss message"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</HStack>
|
||||
<Banner className={classNames(className, "relative")} {...props}>
|
||||
<div className="@container">
|
||||
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
|
||||
{children}
|
||||
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => setDismissed(dismissForDays == null ? true : new Date().toISOString())}
|
||||
title="Dismiss message"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
{actions?.map((a) => (
|
||||
<Button
|
||||
key={a.label}
|
||||
variant={a.variant ?? "border"}
|
||||
color={a.color ?? props.color}
|
||||
size="xs"
|
||||
onClick={a.onClick}
|
||||
title={a.label}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
function isDismissed(
|
||||
dismissed: boolean | string | null,
|
||||
dismissForDays: number | undefined,
|
||||
): boolean {
|
||||
if (dismissed === false || dismissed == null) return false;
|
||||
if (dismissed === true) return true;
|
||||
if (dismissForDays == null) return dismissed.length > 0;
|
||||
|
||||
const dismissedAt = new Date(dismissed).getTime();
|
||||
if (Number.isNaN(dismissedAt)) return false;
|
||||
|
||||
return Date.now() - dismissedAt < dismissForDays * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
@@ -290,10 +290,10 @@ function BaseInput({
|
||||
<HStack
|
||||
className={classNames(
|
||||
inputWrapperClassName,
|
||||
"w-full min-w-0 px-2",
|
||||
"flex-1 min-w-0 px-2",
|
||||
fullHeight && "h-full",
|
||||
leftSlot ? "pl-0.5 -ml-2" : null,
|
||||
rightSlot ? "pr-0.5 -mr-2" : null,
|
||||
leftSlot ? "pl-0" : null,
|
||||
rightSlot ? "pr-0" : null,
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
|
||||
@@ -16,6 +16,7 @@ import { resolvedModelName } from "../../lib/resolvedModelName";
|
||||
import { showConfirm } from "../../lib/confirm";
|
||||
import { showErrorToast } from "../../lib/toast";
|
||||
import { sync } from "../../init/sync";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { Button } from "../core/Button";
|
||||
import type { CheckboxProps } from "../core/Checkbox";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
@@ -205,7 +206,10 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
layout="horizontal"
|
||||
defaultRatio={0.6}
|
||||
firstSlot={({ style }) => (
|
||||
<div style={style} className="h-full px-4">
|
||||
<div style={style} className="h-full px-4 grid grid-rows-[auto_minmax(0,1fr)] gap-3">
|
||||
<CommercialUseBanner source="git-commit" title="Using Git for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
<SplitLayout
|
||||
storageKey="commit-vertical"
|
||||
layout="vertical"
|
||||
|
||||
@@ -69,6 +69,7 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex
|
||||
text={text}
|
||||
language={language}
|
||||
stateKey={`response.body.${response.id}`}
|
||||
filterStateKey={`response.body.${response.requestId}`}
|
||||
pretty={pretty}
|
||||
className={className}
|
||||
onFilter={filterCallback}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
text: string;
|
||||
language: EditorProps["language"];
|
||||
stateKey: string | null;
|
||||
filterStateKey?: string | null;
|
||||
pretty?: boolean;
|
||||
className?: string;
|
||||
onFilter?: (filter: string) => {
|
||||
@@ -27,16 +28,25 @@ interface Props {
|
||||
|
||||
const useFilterText = createGlobalState<Record<string, string | null>>({});
|
||||
|
||||
export function TextViewer({ language, text, stateKey, pretty, className, onFilter }: Props) {
|
||||
export function TextViewer({
|
||||
language,
|
||||
text,
|
||||
stateKey,
|
||||
filterStateKey,
|
||||
pretty,
|
||||
className,
|
||||
onFilter,
|
||||
}: Props) {
|
||||
const filterKey = filterStateKey ?? stateKey;
|
||||
const [filterTextMap, setFilterTextMap] = useFilterText();
|
||||
const filterText = stateKey ? (filterTextMap[stateKey] ?? null) : null;
|
||||
const filterText = filterKey ? (filterTextMap[filterKey] ?? null) : null;
|
||||
const debouncedFilterText = useDebouncedValue(filterText);
|
||||
const setFilterText = useCallback(
|
||||
(v: string | null) => {
|
||||
if (!stateKey) return;
|
||||
setFilterTextMap((m) => ({ ...m, [stateKey]: v }));
|
||||
if (!filterKey) return;
|
||||
setFilterTextMap((m) => ({ ...m, [filterKey]: v }));
|
||||
},
|
||||
[setFilterTextMap, stateKey],
|
||||
[filterKey, setFilterTextMap],
|
||||
);
|
||||
|
||||
const isSearching = filterText != null;
|
||||
@@ -64,7 +74,7 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
|
||||
nodes.push(
|
||||
<div key="input" className="w-full !opacity-100">
|
||||
<Input
|
||||
key={stateKey ?? "filter"}
|
||||
key={filterKey ?? "filter"}
|
||||
validate={!filteredResponse.error}
|
||||
hideLabel
|
||||
autoFocus
|
||||
@@ -76,7 +86,7 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
|
||||
defaultValue={filterText}
|
||||
onKeyDown={(e) => e.key === "Escape" && toggleSearch()}
|
||||
onChange={setFilterText}
|
||||
stateKey={stateKey ? `filter.${stateKey}` : null}
|
||||
stateKey={filterKey ? `filter.${filterKey}` : null}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
@@ -97,12 +107,12 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
|
||||
return nodes;
|
||||
}, [
|
||||
canFilter,
|
||||
filterKey,
|
||||
filterText,
|
||||
filteredResponse.error,
|
||||
filteredResponse.isPending,
|
||||
isSearching,
|
||||
language,
|
||||
stateKey,
|
||||
setFilterText,
|
||||
toggleSearch,
|
||||
]);
|
||||
|
||||
@@ -35,10 +35,15 @@ export async function deleteModelWithConfirm(
|
||||
<>
|
||||
the following?
|
||||
<Prose className="mt-2">
|
||||
<ul>
|
||||
<ul className="space-y-1">
|
||||
{models.map((m) => (
|
||||
<li key={m.id}>
|
||||
<InlineCode>{resolvedModelName(m)}</InlineCode>
|
||||
<InlineCode
|
||||
className="inline-block truncate align-bottom max-w-full"
|
||||
title={resolvedModelName(m)}
|
||||
>
|
||||
{resolvedModelName(m)}
|
||||
</InlineCode>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"decompress": "^4.2.1",
|
||||
"internal-ip": "^8.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.14",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
"rollup": "^4.60.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
|
||||
Generated
+53
-5
@@ -186,7 +186,7 @@
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"decompress": "^4.2.1",
|
||||
"internal-ip": "^8.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.14",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
"rollup": "^4.60.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
@@ -223,6 +223,54 @@
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"apps/yaak-client/node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"apps/yaak-client/node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"apps/yaak-client/node_modules/uuid": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
|
||||
@@ -16805,9 +16853,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -16919,7 +16967,7 @@
|
||||
"packages/plugin-runtime": {
|
||||
"name": "@yaakapp-internal/plugin-runtime",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^8.5.13"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^8.5.13"
|
||||
|
||||
@@ -125,10 +125,11 @@ function bannerColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
return {
|
||||
text: color.lift(0.8).css(),
|
||||
textSubtle: color.translucify(0.3).css(),
|
||||
textSubtlest: color.translucify(0.6).css(),
|
||||
text: color.desaturate(0.5).lift(0.12).css(),
|
||||
textSubtle: color.desaturate(0.58).lift(0.04).translucify(0.04).css(),
|
||||
textSubtlest: color.desaturate(0.65).translucify(0.18).css(),
|
||||
surface: color.translucify(0.95).css(),
|
||||
surfaceHighlight: color.translucify(0.85).css(),
|
||||
border: color.lift(0.3).translucify(0.8).css(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,6 +181,78 @@ export function convertCurl(rawData: string) {
|
||||
};
|
||||
}
|
||||
|
||||
interface ExtractedAuthentication {
|
||||
authenticationType: string | null;
|
||||
authentication: Record<string, string>;
|
||||
filteredHeaders: HttpUrlParameter[]; // headers without authorization
|
||||
}
|
||||
|
||||
function extractAuthenticationFromHeaders(headers: HttpUrlParameter[]): ExtractedAuthentication {
|
||||
const authorizationHeaderIndex = headers.findIndex(
|
||||
(h) => h.name.toLowerCase() === "authorization",
|
||||
);
|
||||
|
||||
const authorizationHeader = headers[authorizationHeaderIndex];
|
||||
if (authorizationHeader == null) {
|
||||
return {
|
||||
authenticationType: null,
|
||||
authentication: {},
|
||||
filteredHeaders: headers,
|
||||
};
|
||||
}
|
||||
|
||||
const value = authorizationHeader.value.trim();
|
||||
const spaceIndex = value.indexOf(" ");
|
||||
|
||||
if (spaceIndex <= 0) {
|
||||
return {
|
||||
authenticationType: null,
|
||||
authentication: {},
|
||||
filteredHeaders: headers,
|
||||
};
|
||||
}
|
||||
|
||||
const scheme = value.slice(0, spaceIndex).toLowerCase();
|
||||
const credentials = value.slice(spaceIndex + 1).trim();
|
||||
|
||||
// Bearer authentication (RFC 6750)
|
||||
if (scheme === "bearer") {
|
||||
const filteredHeaders = headers.filter((_, i) => i !== authorizationHeaderIndex);
|
||||
return {
|
||||
authenticationType: "bearer",
|
||||
authentication: { token: credentials, prefix: "Bearer" },
|
||||
filteredHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
// Basic authentication (RFC 7617)
|
||||
if (scheme === "basic") {
|
||||
try {
|
||||
const decoded = Buffer.from(credentials, "base64").toString();
|
||||
const colonIndex = decoded.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
const filteredHeaders = headers.filter((_, i) => i !== authorizationHeaderIndex);
|
||||
return {
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: decoded.slice(0, colonIndex),
|
||||
password: decoded.slice(colonIndex + 1),
|
||||
},
|
||||
filteredHeaders,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Invalid base64, keep header as-is
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authenticationType: null,
|
||||
authentication: {},
|
||||
filteredHeaders: headers,
|
||||
};
|
||||
}
|
||||
|
||||
function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// ~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Collect all the flags //
|
||||
@@ -323,8 +395,23 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// Extract authentication from Authorization headers (Bearer/Basic)
|
||||
const {
|
||||
authenticationType: extractedAuthenticationType,
|
||||
authentication: extractedAuthentication,
|
||||
filteredHeaders,
|
||||
} = extractAuthenticationFromHeaders(headers);
|
||||
|
||||
// Use extracted authentication from header if found, otherwise fall back to -u/--user parsing
|
||||
const finalAuthenticationType = extractedAuthenticationType || authenticationType;
|
||||
const finalAuthentication = extractedAuthenticationType
|
||||
? extractedAuthentication
|
||||
: authentication;
|
||||
|
||||
// Body (Text or Blob)
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
|
||||
const contentTypeHeader = filteredHeaders.find(
|
||||
(header) => header.name.toLowerCase() === "content-type",
|
||||
);
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0]?.trim() : null;
|
||||
|
||||
// Extract boundary from Content-Type header for multipart parsing
|
||||
@@ -398,7 +485,7 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
value: decodeURIComponent(parameter.value || ""),
|
||||
})),
|
||||
};
|
||||
headers.push({
|
||||
filteredHeaders.push({
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
@@ -419,7 +506,7 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
form: formDataParams,
|
||||
};
|
||||
if (mimeType == null) {
|
||||
headers.push({
|
||||
filteredHeaders.push({
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
@@ -442,9 +529,9 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
urlParameters,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
authentication,
|
||||
authenticationType,
|
||||
headers: filteredHeaders,
|
||||
authentication: finalAuthentication,
|
||||
authenticationType: finalAuthenticationType,
|
||||
body,
|
||||
bodyType,
|
||||
folderId: null,
|
||||
|
||||
@@ -332,6 +332,142 @@ describe("importer-curl", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Imports Bearer token from Authorization header", () => {
|
||||
expect(convertCurl('curl -H "Authorization: Bearer token123" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: "token123",
|
||||
prefix: "Bearer",
|
||||
},
|
||||
headers: [],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Trims whitespace before Bearer token from Authorization header", () => {
|
||||
expect(convertCurl('curl -H "Authorization: Bearer token123" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: "token123",
|
||||
prefix: "Bearer",
|
||||
},
|
||||
headers: [],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Imports Basic auth from Authorization header (base64 decoded)", () => {
|
||||
expect(
|
||||
convertCurl('curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: "user",
|
||||
password: "password",
|
||||
},
|
||||
headers: [],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Authorization header takes precedence over -u flag", () => {
|
||||
expect(
|
||||
convertCurl('curl -u admin:secret -H "Authorization: Bearer token123" https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: "token123",
|
||||
prefix: "Bearer",
|
||||
},
|
||||
headers: [],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Authorization header extraction is case-insensitive", () => {
|
||||
expect(convertCurl('curl -H "authorization: bearer lowercaseToken" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: "lowercaseToken",
|
||||
prefix: "Bearer",
|
||||
},
|
||||
headers: [],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Preserves other headers when extracting Authorization", () => {
|
||||
expect(
|
||||
convertCurl('curl -H "Authorization: Bearer token123" -H "X-Custom: value" https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: "token123",
|
||||
prefix: "Bearer",
|
||||
},
|
||||
headers: [{ name: "X-Custom", value: "value", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Invalid base64 in Basic auth keeps header in headers", () => {
|
||||
expect(
|
||||
convertCurl('curl -H "Authorization: Basic not-valid-base64!!!" https://yaak.app'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Authorization", value: "Basic not-valid-base64!!!", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Imports cookie as header", () => {
|
||||
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
|
||||
@@ -69,6 +69,9 @@ const config = JSON.stringify({
|
||||
const normalizedAdditionalArgs = [];
|
||||
for (let i = 0; i < additionalArgs.length; i++) {
|
||||
const arg = additionalArgs[i];
|
||||
if (arg === "--") {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--config" && i + 1 < additionalArgs.length) {
|
||||
const value = additionalArgs[i + 1];
|
||||
const isInlineJson = value.trimStart().startsWith("{");
|
||||
|
||||
Reference in New Issue
Block a user