[Plugins] [Auth] [JWT] Add addtional JWT headers input (#247)

Co-authored-by: Gregory Schier <gschier1990@gmail.com>
This commit is contained in:
moshyfawn
2026-01-09 19:16:08 -05:00
committed by GitHub
parent 47c5ef1464
commit 4c8f768624
7 changed files with 222 additions and 61 deletions

View File

@@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FolderAction = { label: string, icon?: Icon, }; export type FolderAction = { label: string, icon?: Icon, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown; export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, }; export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
*/ */
description?: string, }; description?: string, };
export type FormInputKeyValue = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, }; export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = { export type FormInputSelect = {

View File

@@ -48,11 +48,7 @@ impl PluginContext {
} }
pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self { pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self {
Self { Self { label, workspace_id, id: generate_prefixed_id("pctx") }
label,
workspace_id,
id: generate_prefixed_id("pctx"),
}
} }
} }
@@ -842,6 +838,7 @@ pub enum FormInput {
HStack(FormInputHStack), HStack(FormInputHStack),
Banner(FormInputBanner), Banner(FormInputBanner),
Markdown(FormInputMarkdown), Markdown(FormInputMarkdown),
KeyValue(FormInputKeyValue),
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
@@ -1095,6 +1092,14 @@ pub struct FormInputMarkdown {
pub hidden: Option<bool>, pub hidden: Option<bool>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputKeyValue {
#[serde(flatten)]
pub base: FormInputBase,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]

View File

@@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FolderAction = { label: string, icon?: Icon, }; export type FolderAction = { label: string, icon?: Icon, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown; export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, }; export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
*/ */
description?: string, }; description?: string, };
export type FormInputKeyValue = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, }; export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = { export type FormInputSelect = {

View File

@@ -30,8 +30,9 @@ A JWT consists of three parts separated by dots:
1. Configure the request, folder, or workspace to use JWT Authentication 1. Configure the request, folder, or workspace to use JWT Authentication
2. Set up your signing algorithm and secret/key 2. Set up your signing algorithm and secret/key
3. Configure the required claims for your JWT 3. Add custom JWT header fields if needed
4. The plugin will generate, sign, and include the JWT in your requests 4. Configure the required claims for your JWT payload
5. The plugin will generate, sign, and include the JWT in your requests
## Common Use Cases ## Common Use Cases

View File

@@ -46,6 +46,28 @@ export const plugin: PluginDefinition = {
name: 'secretBase64', name: 'secretBase64',
label: 'Secret is base64 encoded', label: 'Secret is base64 encoded',
}, },
{
type: 'editor',
name: 'payload',
label: 'JWT Payload',
language: 'json',
defaultValue: '{\n "foo": "bar"\n}',
placeholder: '{ }',
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{
type: 'editor',
name: 'headers',
label: 'JWT Header',
description: 'Merged with auto-generated header fields like alg (e.g., kid)',
language: 'json',
defaultValue: '{}',
placeholder: '{ }',
optional: true,
},
{ {
type: 'select', type: 'select',
name: 'location', name: 'location',
@@ -56,24 +78,16 @@ export const plugin: PluginDefinition = {
{ label: 'Append Query Parameter', value: 'query' }, { label: 'Append Query Parameter', value: 'query' },
], ],
}, },
{
type: 'h_stack',
inputs: [
{ {
type: 'text', type: 'text',
name: 'name', name: 'name',
label: 'Header Name', label: 'Header Name',
defaultValue: 'Authorization', defaultValue: 'Authorization',
optional: true, optional: true,
dynamic(_ctx, args) {
if (args.values.location === 'query') {
return {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
};
}
return {
label: 'Header Name',
description: 'The name of the header to add to the request', description: 'The name of the header to add to the request',
};
},
}, },
{ {
type: 'text', type: 'text',
@@ -81,6 +95,8 @@ export const plugin: PluginDefinition = {
label: 'Header Prefix', label: 'Header Prefix',
optional: true, optional: true,
defaultValue: 'Bearer', defaultValue: 'Bearer',
},
],
dynamic(_ctx, args) { dynamic(_ctx, args) {
if (args.values.location === 'query') { if (args.values.location === 'query') {
return { return {
@@ -90,25 +106,38 @@ export const plugin: PluginDefinition = {
}, },
}, },
{ {
type: 'editor', type: 'text',
name: 'payload', name: 'name',
label: 'Payload', label: 'Parameter Name',
language: 'json', description: 'The name of the query parameter to add to the request',
defaultValue: '{\n "foo": "bar"\n}', defaultValue: 'token',
placeholder: '{ }', optional: true,
dynamic(_ctx, args) {
if (args.values.location !== 'query') {
return {
hidden: true,
};
}
},
},
],
}, },
], ],
async onApply(_ctx, { values }) { async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values; const { algorithm, secret: _secret, secretBase64, payload, headers } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`; const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
const parsedHeaders = headers ? JSON.parse(`${headers}`) : undefined;
const token = jwt.sign(`${payload}`, secret, { const token = jwt.sign(`${payload}`, secret, {
algorithm: algorithm as (typeof algorithms)[number], algorithm: algorithm as (typeof algorithms)[number],
// Extra header fields are merged with the auto-generated header (which includes alg)
header: parsedHeaders as jwt.JwtHeader | undefined,
}); });
if (values.location === 'query') { if (values.location === 'query') {
const paramName = String(values.name || 'token'); const paramName = String(values.name || 'token');
const paramValue = String(values.value || ''); return { setQueryParameters: [{ name: paramName, value: token }] };
return { setQueryParameters: [{ name: paramName, value: paramValue }] };
} }
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer'; const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
const headerName = String(values.name || 'Authorization'); const headerName = String(values.name || 'Authorization');

View File

@@ -6,13 +6,14 @@ import type {
FormInputEditor, FormInputEditor,
FormInputFile, FormInputFile,
FormInputHttpRequest, FormInputHttpRequest,
FormInputKeyValue,
FormInputSelect, FormInputSelect,
FormInputText, FormInputText,
JsonPrimitive, JsonPrimitive,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useRandomKey } from '../hooks/useRandomKey'; import { useRandomKey } from '../hooks/useRandomKey';
import { capitalize } from '../lib/capitalize'; import { capitalize } from '../lib/capitalize';
@@ -26,6 +27,8 @@ import { IconButton } from './core/IconButton';
import type { InputProps } from './core/Input'; import type { InputProps } from './core/Input';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { Label } from './core/Label'; import { Label } from './core/Label';
import type { Pair } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PlainInput } from './core/PlainInput'; import { PlainInput } from './core/PlainInput';
import { Select } from './core/Select'; import { Select } from './core/Select';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
@@ -201,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
summary={input.label} summary={input.label}
className={classNames('!mb-auto', disabled && 'opacity-disabled')} className={classNames('!mb-auto', disabled && 'opacity-disabled')}
> >
<div className="my-3"> <div className="mt-3">
<FormInputsStack <FormInputsStack
data={data} data={data}
disabled={disabled} disabled={disabled}
@@ -249,6 +252,18 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
); );
case 'markdown': case 'markdown':
return <Markdown key={i + stateKey}>{input.content}</Markdown>; return <Markdown key={i + stateKey}>{input.content}</Markdown>;
case 'key_value':
return (
<KeyValueArg
key={i + stateKey}
arg={input}
stateKey={stateKey}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '[]')
}
/>
);
default: default:
// @ts-expect-error // @ts-expect-error
throw new Error(`Invalid input type: ${input.type}`); throw new Error(`Invalid input type: ${input.type}`);
@@ -539,3 +554,52 @@ function CheckboxArg({
/> />
); );
} }
function KeyValueArg({
arg,
onChange,
value,
stateKey,
}: {
arg: FormInputKeyValue;
value: string;
onChange: (v: string) => void;
stateKey: string;
}) {
const pairs: Pair[] = useMemo(() => {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}, [value]);
const handleChange = useCallback(
(newPairs: Pair[]) => {
onChange(JSON.stringify(newPairs));
},
[onChange],
);
return (
<div className="w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)] overflow-hidden">
<Label
htmlFor={`input-${arg.name}`}
required={!arg.optional}
visuallyHidden={arg.hideLabel}
help={arg.description}
>
{arg.label ?? arg.name}
</Label>
<PairEditor
pairs={pairs}
onChange={handleChange}
stateKey={stateKey}
namePlaceholder="name"
valuePlaceholder="value"
noScroll
/>
</div>
);
}

View File

@@ -557,7 +557,7 @@ export function PairEditorRow({
ref={handleSetRef} ref={handleSetRef}
className={classNames( className={classNames(
className, className,
'group grid grid-cols-[auto_auto_minmax(0,1fr)_auto]', 'group/pair-row grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
'grid-rows-1 items-center', 'grid-rows-1 items-center',
!pair.enabled && 'opacity-60', !pair.enabled && 'opacity-60',
)} )}
@@ -576,7 +576,7 @@ export function PairEditorRow({
{...listeners} {...listeners}
className={classNames( className={classNames(
'py-2 h-7 w-4 flex items-center', 'py-2 h-7 w-4 flex items-center',
'justify-center opacity-0 group-hover:opacity-70', 'justify-center opacity-0 group-hover/pair-row:opacity-70',
)} )}
> >
<Icon size="sm" icon="grip_vertical" className="pointer-events-none" /> <Icon size="sm" icon="grip_vertical" className="pointer-events-none" />