[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 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, };
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
*/
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 FormInputSelect = {

View File

@@ -48,11 +48,7 @@ impl PluginContext {
}
pub fn new(label: Option<String>, workspace_id: Option<String>) -> Self {
Self {
label,
workspace_id,
id: generate_prefixed_id("pctx"),
}
Self { label, workspace_id, id: generate_prefixed_id("pctx") }
}
}
@@ -842,6 +838,7 @@ pub enum FormInput {
HStack(FormInputHStack),
Banner(FormInputBanner),
Markdown(FormInputMarkdown),
KeyValue(FormInputKeyValue),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
@@ -1095,6 +1092,14 @@ pub struct FormInputMarkdown {
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)]
#[serde(rename_all = "snake_case", tag = "type")]
#[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 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, };
@@ -275,6 +275,37 @@ defaultValue?: string, disabled?: boolean,
*/
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 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
2. Set up your signing algorithm and secret/key
3. Configure the required claims for your JWT
4. The plugin will generate, sign, and include the JWT in your requests
3. Add custom JWT header fields if needed
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

View File

@@ -46,6 +46,28 @@ export const plugin: PluginDefinition = {
name: 'secretBase64',
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',
name: 'location',
@@ -56,24 +78,16 @@ export const plugin: PluginDefinition = {
{ label: 'Append Query Parameter', value: 'query' },
],
},
{
type: 'h_stack',
inputs: [
{
type: 'text',
name: 'name',
label: 'Header Name',
defaultValue: 'Authorization',
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',
};
},
},
{
type: 'text',
@@ -81,6 +95,8 @@ export const plugin: PluginDefinition = {
label: 'Header Prefix',
optional: true,
defaultValue: 'Bearer',
},
],
dynamic(_ctx, args) {
if (args.values.location === 'query') {
return {
@@ -90,25 +106,38 @@ export const plugin: PluginDefinition = {
},
},
{
type: 'editor',
name: 'payload',
label: 'Payload',
language: 'json',
defaultValue: '{\n "foo": "bar"\n}',
placeholder: '{ }',
type: 'text',
name: 'name',
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
defaultValue: 'token',
optional: true,
dynamic(_ctx, args) {
if (args.values.location !== 'query') {
return {
hidden: true,
};
}
},
},
],
},
],
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 parsedHeaders = headers ? JSON.parse(`${headers}`) : undefined;
const token = jwt.sign(`${payload}`, secret, {
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') {
const paramName = String(values.name || 'token');
const paramValue = String(values.value || '');
return { setQueryParameters: [{ name: paramName, value: paramValue }] };
return { setQueryParameters: [{ name: paramName, value: token }] };
}
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
const headerName = String(values.name || 'Authorization');

View File

@@ -6,13 +6,14 @@ import type {
FormInputEditor,
FormInputFile,
FormInputHttpRequest,
FormInputKeyValue,
FormInputSelect,
FormInputText,
JsonPrimitive,
} from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useRandomKey } from '../hooks/useRandomKey';
import { capitalize } from '../lib/capitalize';
@@ -26,6 +27,8 @@ import { IconButton } from './core/IconButton';
import type { InputProps } from './core/Input';
import { Input } from './core/Input';
import { Label } from './core/Label';
import type { Pair } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
@@ -201,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
summary={input.label}
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
>
<div className="my-3">
<div className="mt-3">
<FormInputsStack
data={data}
disabled={disabled}
@@ -249,6 +252,18 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
);
case '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:
// @ts-expect-error
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}
className={classNames(
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',
!pair.enabled && 'opacity-60',
)}
@@ -576,7 +576,7 @@ export function PairEditorRow({
{...listeners}
className={classNames(
'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" />