mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 16:44:16 +00:00
fix(ui): correct “modified” indicator state across collection, folder, request, and presets/auth tabs (#3386) (#8027)
* fix: 3296 Folder-level No Auth inheritance is ignored; requests still use Collection Auth
This commit is contained in:
@@ -75,13 +75,13 @@ const AuthMode = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
|
||||
@@ -5,10 +5,11 @@ import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import Button from 'ui/Button';
|
||||
import { DEFAULT_PRESET_REQUEST_TYPE, PRESET_REQUEST_TYPES } from 'utils/common/constants';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
const initialPresets = { requestType: DEFAULT_PRESET_REQUEST_TYPE, requestUrl: '' };
|
||||
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
@@ -47,12 +48,13 @@ const PresetsSettings = ({ collection }) => {
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="http"
|
||||
data-testid="presets-request-type-http"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={(currentPresets.requestType || 'http') === 'http'}
|
||||
value={PRESET_REQUEST_TYPES.HTTP}
|
||||
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.HTTP}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
@@ -60,12 +62,13 @@ const PresetsSettings = ({ collection }) => {
|
||||
|
||||
<input
|
||||
id="graphql"
|
||||
data-testid="presets-request-type-graphql"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
value={PRESET_REQUEST_TYPES.GRAPHQL}
|
||||
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRAPHQL}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
@@ -73,12 +76,13 @@ const PresetsSettings = ({ collection }) => {
|
||||
|
||||
<input
|
||||
id="grpc"
|
||||
data-testid="presets-request-type-grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
value={PRESET_REQUEST_TYPES.GRPC}
|
||||
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRPC}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
@@ -86,12 +90,13 @@ const PresetsSettings = ({ collection }) => {
|
||||
|
||||
<input
|
||||
id="ws"
|
||||
data-testid="presets-request-type-ws"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="ws"
|
||||
checked={(currentPresets.requestType || 'http') === 'ws'}
|
||||
value={PRESET_REQUEST_TYPES.WS}
|
||||
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.WS}
|
||||
/>
|
||||
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
|
||||
WebSocket
|
||||
@@ -106,6 +111,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
id="request-url"
|
||||
data-testid="presets-request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
@@ -123,7 +129,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
<Button type="button" size="sm" data-testid="presets-save-btn" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Overview from './Overview/index';
|
||||
import { DEFAULT_PRESET_REQUEST_TYPE } from 'utils/common/constants';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -60,7 +61,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
const hasPresets = presets && ((presets.requestType && presets.requestType !== DEFAULT_PRESET_REQUEST_TYPE) || (presets.requestUrl && presets.requestUrl !== ''));
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -18,8 +18,9 @@ import OAuth1 from 'components/RequestPane/Auth/OAuth1';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
import Button from 'ui/Button';
|
||||
import { getEffectiveAuthSource } from 'utils/auth';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -52,41 +53,6 @@ const Auth = ({ collection, folder }) => {
|
||||
let request = get(folderRoot, 'request', {});
|
||||
const authMode = get(folderRoot, 'request.auth.mode');
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
|
||||
const folderAuth = get(parentFolderRoot, 'request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
@@ -98,6 +64,11 @@ const Auth = ({ collection, folder }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const inheritedSource = useMemo(
|
||||
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, folder) : null),
|
||||
[authMode, folder, collection]
|
||||
);
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
@@ -202,12 +173,11 @@ const Auth = ({ collection, folder }) => {
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
<div>Auth inherited from {inheritedSource.name}: </div>
|
||||
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -81,14 +81,15 @@ const AuthMode = ({ collection, folder }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
showTickMark={true}
|
||||
data-testid="auth-mode-dropdown"
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,7 +10,7 @@ import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import get from 'lodash/get';
|
||||
import { hasEffectiveAuth } from 'utils/auth';
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -31,8 +31,11 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
const folderAuthMode = folder?.draft?.request?.auth?.mode ?? folder?.root?.request?.auth?.mode;
|
||||
const hasAuth = useMemo(
|
||||
() => hasEffectiveAuth(collection, folder),
|
||||
[folder, folderAuthMode, collection]
|
||||
);
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -95,7 +98,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" data-testid="folder-settings-tab-auth" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <StatusDot />}
|
||||
{hasAuth && <StatusDot dataTestId="auth" />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" data-testid="folder-settings-tab-docs" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
|
||||
@@ -81,14 +81,15 @@ const AuthMode = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
showTickMark={true}
|
||||
data-testid="auth-mode-dropdown"
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
@@ -15,22 +15,11 @@ import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import OAuth2 from './OAuth2/index';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
import { getEffectiveAuthSource } from 'utils/auth';
|
||||
|
||||
const Auth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Create a request object to pass to the auth components
|
||||
const request = item.draft
|
||||
@@ -42,34 +31,10 @@ const Auth = ({ item, collection }) => {
|
||||
return dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
const inheritedSource = useMemo(
|
||||
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
|
||||
[authMode, item, collection]
|
||||
);
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
@@ -104,12 +69,11 @@ const Auth = ({ item, collection }) => {
|
||||
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
<div>Auth inherited from {inheritedSource.name}: </div>
|
||||
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,10 +24,12 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import useGraphqlSchema from '../GraphQLSchemaActions/useGraphqlSchema';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { hasEffectiveAuth } from 'utils/auth';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import AuthMode from '../Auth/AuthMode/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'query', label: 'Query' },
|
||||
@@ -172,7 +174,20 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
|
||||
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
|
||||
const hasAuth = useMemo(
|
||||
() => hasEffectiveAuth(collection, item),
|
||||
[item, itemAuthMode, collection]
|
||||
);
|
||||
|
||||
const allTabs = useMemo(
|
||||
() => TAB_CONFIG.map(({ key, label }) => ({
|
||||
key,
|
||||
label,
|
||||
indicator: key === 'auth' && hasAuth ? <StatusDot dataTestId="auth" /> : null
|
||||
})),
|
||||
[hasAuth]
|
||||
);
|
||||
|
||||
const handlePrettify = useCallback(() => {
|
||||
if (queryEditorRef.current?.beautifyRequestBody) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import GrpcAuthMode from './GrpcAuthMode';
|
||||
@@ -9,32 +9,32 @@ import OAuth2 from '../../Auth/OAuth2/index';
|
||||
import WsseAuth from '../../Auth/WsseAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { getEffectiveAuthSource } from 'utils/auth';
|
||||
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// List of auth modes supported by gRPC
|
||||
// Note: Only header-based auth modes work with gRPC
|
||||
// Complex auth modes like AWS Sig v4, Digest, and NTLM require axios interceptors
|
||||
// and cannot be supported in gRPC requests as of now
|
||||
const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'wsse', 'none', 'inherit'];
|
||||
import { AUTH_MODES_GRPC } from 'utils/common/constants';
|
||||
|
||||
const GrpcAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
const inheritedSource = useMemo(
|
||||
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
|
||||
[authMode, item, collection]
|
||||
);
|
||||
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
// Reset to 'none' if current auth mode is not supported by gRPC
|
||||
useEffect(() => {
|
||||
if (authMode && !supportedGrpcAuthModes.includes(authMode)) {
|
||||
if (authMode && !AUTH_MODES_GRPC.includes(authMode)) {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
@@ -45,35 +45,6 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
}
|
||||
}, [authMode, collection.uid, dispatch, item.uid]);
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'none': {
|
||||
@@ -95,15 +66,13 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
return <WsseAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
// Only show inherited auth if it's one of the supported types
|
||||
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
|
||||
if (inheritedSource && AUTH_MODES_GRPC.includes(inheritedSource.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
<div>Auth inherited from {inheritedSource.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ import Documentation from 'components/Documentation/index';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { hasEffectiveAuth } from 'utils/auth';
|
||||
import { AUTH_MODES_GRPC } from 'utils/common/constants';
|
||||
|
||||
const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -53,8 +55,11 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
const body = getPropertyFromDraftOrRequest(item, 'request.body');
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
|
||||
const hasAuth = useMemo(
|
||||
() => hasEffectiveAuth(collection, item, AUTH_MODES_GRPC),
|
||||
[item, itemAuthMode, collection]
|
||||
);
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
const grpcMessagesCount = body?.grpc?.length || 0;
|
||||
|
||||
@@ -88,7 +93,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
{
|
||||
key: 'auth',
|
||||
label: 'Auth',
|
||||
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
|
||||
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
|
||||
},
|
||||
{
|
||||
key: 'docs',
|
||||
@@ -96,7 +101,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
|
||||
}
|
||||
];
|
||||
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]);
|
||||
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, hasAuth, docs]);
|
||||
|
||||
// Initialize tab to 'body' if no tab is currently set
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import StatusDot from 'components/StatusDot';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import AuthMode from '../Auth/AuthMode/index';
|
||||
import { hasEffectiveAuth } from 'utils/auth';
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'params', label: 'Params' },
|
||||
@@ -54,7 +55,6 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const getProperty = useCallback(
|
||||
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
|
||||
[item.draft, item]
|
||||
@@ -86,6 +86,12 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
|
||||
const hasAuth = useMemo(
|
||||
() => hasEffectiveAuth(collection, item),
|
||||
[item, itemAuthMode, collection]
|
||||
);
|
||||
|
||||
const indicators = useMemo(() => {
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
const hasTestError = item.testScriptErrorMessage;
|
||||
@@ -94,7 +100,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
|
||||
body: body.mode !== 'none' ? <StatusDot /> : null,
|
||||
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
|
||||
auth: auth.mode !== 'none' ? <StatusDot /> : null,
|
||||
auth: hasAuth ? <StatusDot dataTestId="auth" /> : null,
|
||||
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
|
||||
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
|
||||
@@ -102,7 +108,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
docs: docs?.length > 0 ? <StatusDot /> : null,
|
||||
settings: tags?.length > 0 ? <StatusDot /> : null
|
||||
};
|
||||
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
|
||||
}, [activeCounts, body.mode, hasAuth, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
|
||||
|
||||
const allTabs = useMemo(
|
||||
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import BearerAuth from '../../Auth/BearerAuth';
|
||||
@@ -6,16 +6,15 @@ import BasicAuth from '../../Auth/BasicAuth';
|
||||
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { getEffectiveAuthSource } from 'utils/auth';
|
||||
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const supportedAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];
|
||||
import { AUTH_MODES_WS } from 'utils/common/constants';
|
||||
|
||||
const WSAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
@@ -25,9 +24,14 @@ const WSAuth = ({ item, collection }) => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const inheritedSource = useMemo(
|
||||
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
|
||||
[authMode, item, collection]
|
||||
);
|
||||
|
||||
// Reset to 'none' if current auth mode is not supported
|
||||
useEffect(() => {
|
||||
if (authMode && !supportedAuthModes.includes(authMode)) {
|
||||
if (authMode && !AUTH_MODES_WS.includes(authMode)) {
|
||||
dispatch(updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
@@ -36,35 +40,6 @@ const WSAuth = ({ item, collection }) => {
|
||||
}
|
||||
}, [authMode, collection.uid, dispatch, item.uid]);
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'none': {
|
||||
@@ -91,26 +66,24 @@ const WSAuth = ({ item, collection }) => {
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
// Check if inherited auth is OAuth1/OAuth2 - not supported for WebSockets
|
||||
if (source?.auth?.mode === 'oauth1' || source?.auth?.mode === 'oauth2') {
|
||||
if (inheritedSource?.auth?.mode === 'oauth1' || inheritedSource?.auth?.mode === 'oauth2') {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
{source.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
|
||||
{inheritedSource.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Only show inherited auth if it's one of the supported types
|
||||
if (source && supportedAuthModes.includes(source.auth?.mode)) {
|
||||
if (inheritedSource && AUTH_MODES_WS.includes(inheritedSource.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div> Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
<div> Auth inherited from {inheritedSource.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,8 @@ import StyledWrapper from './StyledWrapper';
|
||||
import WSAuth from './WSAuth';
|
||||
import WSAuthMode from './WSAuth/WSAuthMode';
|
||||
import WSSettingsPane from '../WSSettingsPane/index';
|
||||
import { hasEffectiveAuth } from 'utils/auth';
|
||||
import { AUTH_MODES_WS } from 'utils/common/constants';
|
||||
|
||||
const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -102,8 +104,11 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
|
||||
const hasAuth = useMemo(
|
||||
() => hasEffectiveAuth(collection, item, AUTH_MODES_WS),
|
||||
[item, itemAuthMode, collection]
|
||||
);
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const allTabs = useMemo(() => {
|
||||
@@ -121,7 +126,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
{
|
||||
key: 'auth',
|
||||
label: 'Auth',
|
||||
indicator: auth.mode !== 'none' ? <StatusDot type="default" /> : null
|
||||
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
@@ -134,7 +139,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
|
||||
}
|
||||
];
|
||||
}, [activeHeadersLength, auth.mode, docs]);
|
||||
}, [activeHeadersLength, hasAuth, docs]);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
switch (requestPaneTab) {
|
||||
|
||||
@@ -53,7 +53,12 @@ const Timeline = ({ collection, item }) => {
|
||||
useTrackScroll({ ref: wrapperRef, selector: null, onChange: setScroll, initialValue: scroll });
|
||||
const [activeFilter, setActiveFilter] = useState('all');
|
||||
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
|
||||
const authSource = useMemo(
|
||||
() => getEffectiveAuthSource(collection, item),
|
||||
[item, itemAuthMode, collection]
|
||||
);
|
||||
const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';
|
||||
|
||||
const entries = useMemo(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
|
||||
const StatusDot = ({ type = 'default' }) => (
|
||||
const StatusDot = ({ type = 'default', dataTestId = null }) => (
|
||||
<sup
|
||||
className={`ml-[.125rem] opacity-80 font-medium ${
|
||||
type === 'error' ? 'text-red-500' : ''
|
||||
}`}
|
||||
data-testid={dataTestId ? `status-dot-${dataTestId}` : 'status-dot'}
|
||||
>
|
||||
<DotIcon width="10" />
|
||||
</sup>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { get } from 'lodash';
|
||||
import {
|
||||
getTreePathFromCollectionToItem
|
||||
} from 'utils/collections/index';
|
||||
import { AUTH_MODES } from 'utils/common/constants';
|
||||
|
||||
// Resolve inherited auth by traversing up the folder hierarchy
|
||||
export const resolveInheritedAuth = (item, collection) => {
|
||||
@@ -25,8 +26,9 @@ export const resolveInheritedAuth = (item, collection) => {
|
||||
const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
// Walk ancestor folders from deepest up; pick the first one with a concrete auth mode (skip 'none'/'inherit').
|
||||
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
|
||||
const i = requestTreePath[idx];
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
@@ -41,3 +43,51 @@ export const resolveInheritedAuth = (item, collection) => {
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
|
||||
export const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item?.draft
|
||||
? get(item, 'draft.request.auth.mode')
|
||||
: (get(item, 'request.auth.mode') ?? get(item, 'root.request.auth.mode'));
|
||||
if (authMode !== AUTH_MODES.INHERIT) return null;
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
|
||||
const i = requestTreePath[idx];
|
||||
if (i?.uid === item?.uid) continue;
|
||||
if (i?.type !== 'folder') continue;
|
||||
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
|
||||
if (!folderAuth || !folderAuth.mode) continue;
|
||||
if (folderAuth.mode === AUTH_MODES.INHERIT) continue;
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
// Returns true when an item actually has auth applied — resolves `inherit` up
|
||||
// the chain, then checks that the effective mode is set, not 'none', and (if a
|
||||
// supportedModes list is passed) is one the protocol can apply.
|
||||
export const hasEffectiveAuth = (collection, item, supportedModes) => {
|
||||
const auth = item?.draft
|
||||
? get(item, 'draft.request.auth')
|
||||
: (get(item, 'request.auth') ?? get(item, 'root.request.auth'));
|
||||
const mode = auth?.mode === AUTH_MODES.INHERIT
|
||||
? getEffectiveAuthSource(collection, item)?.auth?.mode
|
||||
: auth?.mode;
|
||||
if (!mode || mode === AUTH_MODES.NONE) return false;
|
||||
if (supportedModes && !supportedModes.includes(mode)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { resolveInheritedAuth } from './index';
|
||||
import { getEffectiveAuthSource, resolveInheritedAuth } from './index';
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
// General path finder: walks the collection.items tree until it finds the
|
||||
// item with the matching uid and returns the full path to it.
|
||||
getTreePathFromCollectionToItem: (collection, item) => {
|
||||
const itemUid = item.uid;
|
||||
|
||||
if (itemUid === 'r1') {
|
||||
return [collection.items[0], collection.items[0].items[0]];
|
||||
}
|
||||
return [];
|
||||
const findPath = (items, targetUid, path = []) => {
|
||||
for (const i of items || []) {
|
||||
const next = [...path, i];
|
||||
if (i.uid === targetUid) return next;
|
||||
if (i.items) {
|
||||
const found = findPath(i.items, targetUid, next);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return findPath(collection.items, item?.uid) || [];
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -17,7 +25,7 @@ const buildCollection = () => {
|
||||
uid: 'c1',
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
@@ -64,7 +72,7 @@ describe('auth-utils.resolveInheritedAuth', () => {
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('bearer');
|
||||
expect(resolved.auth.bearer.token).toBe('COLLECTION');
|
||||
expect(resolved.auth.bearer.token).toBe('COLLECTION_LEVEL_TOKEN');
|
||||
});
|
||||
|
||||
it('should return original request when mode is not inherit', () => {
|
||||
@@ -77,3 +85,211 @@ describe('auth-utils.resolveInheritedAuth', () => {
|
||||
expect(resolved.auth.basic.username).toBe('override');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth-utils.getEffectiveAuthSource', () => {
|
||||
it('returns null when the request mode is not inherit', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0]; // r1
|
||||
item.request.auth = { mode: 'bearer', bearer: { token: 'MOCK_REQUEST_OWN_TOKEN_STRING' } };
|
||||
|
||||
expect(getEffectiveAuthSource(collection, item)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when the request has no auth configured', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0];
|
||||
item.request.auth = undefined;
|
||||
|
||||
expect(getEffectiveAuthSource(collection, item)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the nearest configured folder when request inherits and folder has auth', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0]; // r1, mode 'inherit'
|
||||
|
||||
const source = getEffectiveAuthSource(collection, item);
|
||||
expect(source).toEqual({
|
||||
type: 'folder',
|
||||
name: 'Folder',
|
||||
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the collection when no ancestor folder has configured auth', () => {
|
||||
const collection = buildCollection();
|
||||
// make the folder also inherit so the walk falls through to the collection
|
||||
collection.items[0].root.request.auth = { mode: 'inherit' };
|
||||
const item = collection.items[0].items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, item);
|
||||
expect(source).toEqual({
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('skips the item itself when the item is a folder in inherit mode', () => {
|
||||
// Build a parent → child folder chain; child is the item under test.
|
||||
const collection = {
|
||||
uid: 'c1',
|
||||
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'parent',
|
||||
type: 'folder',
|
||||
name: 'Parent',
|
||||
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'child',
|
||||
type: 'folder',
|
||||
name: 'Child',
|
||||
root: { request: { auth: { mode: 'inherit' } } },
|
||||
items: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
const child = collection.items[0].items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, child);
|
||||
expect(source).toEqual({
|
||||
type: 'folder',
|
||||
name: 'Parent',
|
||||
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers the draft mode when item.draft exists', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0];
|
||||
item.request.auth = { mode: 'bearer' }; // saved is not inherit
|
||||
item.draft = { request: { auth: { mode: 'inherit' } } }; // draft is inherit
|
||||
|
||||
const source = getEffectiveAuthSource(collection, item);
|
||||
expect(source).toEqual({
|
||||
type: 'folder',
|
||||
name: 'Folder',
|
||||
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves correctly when both draft and saved auth are inherit on a folder whose parent is also a folder', () => {
|
||||
const collection = {
|
||||
uid: 'c1',
|
||||
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'parent',
|
||||
type: 'folder',
|
||||
name: 'Parent',
|
||||
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'child',
|
||||
type: 'folder',
|
||||
name: 'Child',
|
||||
root: { request: { auth: { mode: 'inherit' } } }, // saved: inherit
|
||||
draft: { request: { auth: { mode: 'inherit' } } }, // draft: inherit
|
||||
items: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
const child = collection.items[0].items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, child);
|
||||
expect(source).toEqual({
|
||||
type: 'folder',
|
||||
name: 'Parent',
|
||||
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('handles a folder item without draft using its root.request.auth.mode', () => {
|
||||
// The folder's mode is read from root.request.auth.mode when no draft exists.
|
||||
const collection = {
|
||||
uid: 'c1',
|
||||
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'folder-inherit',
|
||||
type: 'folder',
|
||||
name: 'FolderInherit',
|
||||
root: { request: { auth: { mode: 'inherit' } } },
|
||||
items: []
|
||||
}
|
||||
]
|
||||
};
|
||||
const folder = collection.items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, folder);
|
||||
expect(source).toEqual({
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('handles a folder item with draft by reading its draft.request.auth.mode (not the saved root mode)', () => {
|
||||
// Saved mode is 'basic' (would return null since not inherit), but draft is 'inherit'
|
||||
// so the walk should run and resolve to the collection.
|
||||
const collection = {
|
||||
uid: 'c1',
|
||||
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'folder-draft-inherit',
|
||||
type: 'folder',
|
||||
name: 'FolderDraftInherit',
|
||||
root: { request: { auth: { mode: 'basic', basic: { username: 'saved', password: 'saved' } } } },
|
||||
draft: { request: { auth: { mode: 'inherit' } } },
|
||||
items: []
|
||||
}
|
||||
]
|
||||
};
|
||||
const folder = collection.items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, folder);
|
||||
expect(source).toEqual({
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('skips ancestor folders whose auth.mode is itself "inherit"', () => {
|
||||
// Parent folder also inherits — walk should continue past it to collection.
|
||||
const collection = {
|
||||
uid: 'c1',
|
||||
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'parent',
|
||||
type: 'folder',
|
||||
name: 'Parent',
|
||||
root: { request: { auth: { mode: 'inherit' } } },
|
||||
items: [
|
||||
{
|
||||
uid: 'r1',
|
||||
type: 'request',
|
||||
name: 'Request',
|
||||
request: { auth: { mode: 'inherit' } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
const item = collection.items[0].items[0];
|
||||
|
||||
const source = getEffectiveAuthSource(collection, item);
|
||||
expect(source).toEqual({
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
export const REQUEST_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];
|
||||
|
||||
export const DEFAULT_COLLECTION_FORMAT = 'yml';
|
||||
|
||||
export const PRESET_REQUEST_TYPES = {
|
||||
HTTP: 'http',
|
||||
GRAPHQL: 'graphql',
|
||||
GRPC: 'grpc',
|
||||
WS: 'ws'
|
||||
};
|
||||
|
||||
export const DEFAULT_PRESET_REQUEST_TYPE = PRESET_REQUEST_TYPES.HTTP;
|
||||
|
||||
export const AUTH_MODES = {
|
||||
AWSV4: 'awsv4',
|
||||
BASIC: 'basic',
|
||||
BEARER: 'bearer',
|
||||
DIGEST: 'digest',
|
||||
NTLM: 'ntlm',
|
||||
OAUTH1: 'oauth1',
|
||||
OAUTH2: 'oauth2',
|
||||
WSSE: 'wsse',
|
||||
APIKEY: 'apikey',
|
||||
NONE: 'none',
|
||||
INHERIT: 'inherit'
|
||||
};
|
||||
|
||||
// Auth modes supported by WS protocol.
|
||||
export const AUTH_MODES_WS = [
|
||||
AUTH_MODES.BASIC,
|
||||
AUTH_MODES.BEARER,
|
||||
AUTH_MODES.APIKEY,
|
||||
AUTH_MODES.OAUTH2,
|
||||
AUTH_MODES.NONE,
|
||||
AUTH_MODES.INHERIT
|
||||
];
|
||||
|
||||
// Auth modes supported by GRPC protocol
|
||||
export const AUTH_MODES_GRPC = [
|
||||
AUTH_MODES.BASIC,
|
||||
AUTH_MODES.BEARER,
|
||||
AUTH_MODES.APIKEY,
|
||||
AUTH_MODES.OAUTH2,
|
||||
AUTH_MODES.WSSE,
|
||||
AUTH_MODES.NONE,
|
||||
AUTH_MODES.INHERIT
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts,
|
||||
import { toOpenCollectionEnvironments } from "./environment";
|
||||
import { toOpenCollectionFolder } from "./folder";
|
||||
import { toOpenCollectionItems } from "./items";
|
||||
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
|
||||
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
|
||||
|
||||
const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {
|
||||
if (!brunoConfig) {
|
||||
@@ -147,10 +147,7 @@ export const brunoToOpenCollection = (collection: BrunoCollection): OpenCollecti
|
||||
|
||||
const brunoExtension: {
|
||||
ignore?: string[];
|
||||
presets?: {
|
||||
requestType?: string;
|
||||
requestUrl?: string;
|
||||
};
|
||||
presets?: BrunoPresets;
|
||||
} = {};
|
||||
|
||||
if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenCollection } from "@opencollection/types";
|
||||
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, PemCertificate, Pkcs12Certificate } from "./types";
|
||||
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, PemCertificate, Pkcs12Certificate } from "./types";
|
||||
import { fromOpenCollectionAuth, fromOpenCollectionHeaders, fromOpenCollectionScripts, fromOpenCollectionVariables } from "./common";
|
||||
import { uuid } from "../common";
|
||||
import { fromOpenCollectionItems } from "./items";
|
||||
@@ -9,10 +9,7 @@ import { fromOpenCollectionEnvironments } from "./environment";
|
||||
const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
|
||||
const brunoExtension = oc.extensions?.bruno as {
|
||||
ignore?: string[];
|
||||
presets?: {
|
||||
requestType?: string;
|
||||
requestUrl?: string;
|
||||
};
|
||||
presets?: BrunoPresets;
|
||||
} | undefined;
|
||||
|
||||
const ignoreList = brunoExtension && Array.isArray(brunoExtension.ignore)
|
||||
|
||||
@@ -168,15 +168,17 @@ export type {
|
||||
WebSocketMessage as BrunoWsMessage
|
||||
} from '@usebruno/schema-types/requests/websocket';
|
||||
|
||||
export interface BrunoPresets {
|
||||
requestType?: string;
|
||||
requestUrl?: string;
|
||||
}
|
||||
|
||||
export interface BrunoConfig {
|
||||
version?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
ignore?: string[];
|
||||
presets?: {
|
||||
requestType?: string;
|
||||
requestUrl?: string;
|
||||
};
|
||||
presets?: BrunoPresets;
|
||||
protobuf?: {
|
||||
protoFiles?: { path: string }[];
|
||||
importPaths?: { path: string; enabled?: boolean }[];
|
||||
|
||||
@@ -809,7 +809,7 @@ const mergeAuth = (collection, request, requestTreePath) => {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
const folderAuth = get(folderRoot, 'request.auth');
|
||||
// Only consider folders that have a valid auth mode
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
lastFolderWithAuth = i;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
|
||||
import { toBrunoPostResponseVariables } from './common/actions';
|
||||
import { toBrunoScripts } from './common/scripts';
|
||||
import { ensureString } from '../../utils';
|
||||
import type { BrunoPresetsExtension } from '../../types';
|
||||
|
||||
interface ParsedCollection {
|
||||
collectionRoot: FolderRoot;
|
||||
@@ -32,11 +33,11 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
||||
|
||||
// presets
|
||||
if (brunoExtension?.presets) {
|
||||
const presets = brunoExtension.presets as any;
|
||||
const presets = brunoExtension.presets as BrunoPresetsExtension;
|
||||
if (presets.request) {
|
||||
brunoConfig.presets = {
|
||||
requestType: presets.request.type || [],
|
||||
requestUrl: presets.request.url || []
|
||||
requestType: presets.request.type || '',
|
||||
requestUrl: presets.request.url || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
||||
name: ensureString(info?.name, 'Untitled Folder'),
|
||||
seq: info?.seq || 1
|
||||
},
|
||||
request: null,
|
||||
docs: null
|
||||
};
|
||||
|
||||
// request defaults
|
||||
if (ocFolder.request) {
|
||||
folderRoot.request = {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: null,
|
||||
auth: toBrunoAuth(ocFolder.request?.auth),
|
||||
script: {
|
||||
req: null,
|
||||
res: null
|
||||
@@ -37,40 +31,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
||||
res: []
|
||||
},
|
||||
tests: null
|
||||
};
|
||||
},
|
||||
docs: null
|
||||
};
|
||||
|
||||
if (ocFolder.request) {
|
||||
const folderRequest = folderRoot.request!;
|
||||
|
||||
// headers
|
||||
const headers = toBrunoHttpHeaders(ocFolder.request.headers);
|
||||
if (headers) {
|
||||
folderRoot.request.headers = headers;
|
||||
}
|
||||
|
||||
// auth
|
||||
const auth = toBrunoAuth(ocFolder.request.auth);
|
||||
if (auth) {
|
||||
folderRoot.request.auth = auth;
|
||||
folderRequest.headers = headers;
|
||||
}
|
||||
|
||||
// variables
|
||||
const variables = toBrunoVariables(ocFolder.request.variables);
|
||||
const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
|
||||
folderRoot.request.vars = {
|
||||
folderRequest.vars = {
|
||||
req: variables.req,
|
||||
res: postResponseVars
|
||||
};
|
||||
|
||||
// scripts
|
||||
const scripts = toBrunoScripts(ocFolder.request.scripts);
|
||||
if (scripts?.script && folderRoot.request.script) {
|
||||
if (scripts?.script && folderRequest.script) {
|
||||
if (scripts.script.req) {
|
||||
folderRoot.request.script.req = scripts.script.req;
|
||||
folderRequest.script.req = scripts.script.req;
|
||||
}
|
||||
if (scripts.script.res) {
|
||||
folderRoot.request.script.res = scripts.script.res;
|
||||
folderRequest.script.res = scripts.script.res;
|
||||
}
|
||||
}
|
||||
if (scripts?.tests) {
|
||||
folderRoot.request.tests = scripts.tests;
|
||||
folderRequest.tests = scripts.tests;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/empty-request.yml
vendored
Normal file
7
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/empty-request.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
opencollection: 1.0.0
|
||||
info:
|
||||
name: My Collection
|
||||
extensions:
|
||||
bruno:
|
||||
presets:
|
||||
request: {}
|
||||
3
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/no-presets.yml
vendored
Normal file
3
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/no-presets.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
opencollection: 1.0.0
|
||||
info:
|
||||
name: My Collection
|
||||
6
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/no-request-key.yml
vendored
Normal file
6
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/no-request-key.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
opencollection: 1.0.0
|
||||
info:
|
||||
name: My Collection
|
||||
extensions:
|
||||
bruno:
|
||||
presets: {}
|
||||
24
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml
vendored
Normal file
24
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: md8
|
||||
config:
|
||||
proxy:
|
||||
inherit: true
|
||||
config:
|
||||
protocol: http
|
||||
hostname: ""
|
||||
port: ""
|
||||
auth:
|
||||
username: ""
|
||||
password: ""
|
||||
bypassProxy: ""
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
presets:
|
||||
request:
|
||||
type: graphql
|
||||
25
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml
vendored
Normal file
25
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: md8
|
||||
config:
|
||||
proxy:
|
||||
inherit: true
|
||||
config:
|
||||
protocol: http
|
||||
hostname: ""
|
||||
port: ""
|
||||
auth:
|
||||
username: ""
|
||||
password: ""
|
||||
bypassProxy: ""
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
presets:
|
||||
request:
|
||||
type: http
|
||||
url: https://example.com/graphql
|
||||
9
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml
vendored
Normal file
9
packages/bruno-filestore/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
opencollection: 1.0.0
|
||||
info:
|
||||
name: My Collection
|
||||
extensions:
|
||||
bruno:
|
||||
presets:
|
||||
request:
|
||||
type: graphql
|
||||
url: https://example.com/graphql
|
||||
@@ -0,0 +1,63 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import parseCollection from '../parseCollection';
|
||||
|
||||
const loadFixture = (name) =>
|
||||
fs.readFileSync(path.join(__dirname, 'fixtures', 'presets', `${name}.yml`), 'utf8');
|
||||
|
||||
describe('yml parseCollection - presets', () => {
|
||||
it('parses presets when request.type and request.url are present', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('with-type-and-url'));
|
||||
|
||||
expect(brunoConfig.presets).toEqual({
|
||||
requestType: 'graphql',
|
||||
requestUrl: 'https://example.com/graphql'
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults requestType and requestUrl to empty strings (not arrays) when request fields are missing', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('empty-request'));
|
||||
|
||||
expect(brunoConfig.presets).toEqual({
|
||||
requestType: '',
|
||||
requestUrl: ''
|
||||
});
|
||||
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
|
||||
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not set presets when the extension has no presets block', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('no-presets'));
|
||||
|
||||
expect(brunoConfig.presets).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not set presets when presets exists but request key is absent', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('no-request-key'));
|
||||
|
||||
expect(brunoConfig.presets).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses a realistic collection with only request.type set (no url) — defaults url to empty string', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('type-only-realistic'));
|
||||
|
||||
expect(brunoConfig.presets).toEqual({
|
||||
requestType: 'graphql',
|
||||
requestUrl: ''
|
||||
});
|
||||
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
|
||||
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
|
||||
});
|
||||
|
||||
it('parses a realistic collection with request.type http and request.url set', () => {
|
||||
const { brunoConfig } = parseCollection(loadFixture('url-only-realistic'));
|
||||
|
||||
expect(brunoConfig.presets).toEqual({
|
||||
requestType: 'http',
|
||||
requestUrl: 'https://example.com/graphql'
|
||||
});
|
||||
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
|
||||
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
|
||||
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
|
||||
});
|
||||
});
|
||||
@@ -20,3 +20,10 @@ export interface WorkerTask {
|
||||
export interface Lane {
|
||||
maxSize: number;
|
||||
}
|
||||
|
||||
export interface BrunoPresetsExtension {
|
||||
request?: {
|
||||
type?: string;
|
||||
url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user