From 4c8f76862482c5e281140a1bca855bc927e7f019 Mon Sep 17 00:00:00 2001 From: moshyfawn Date: Fri, 9 Jan 2026 19:16:08 -0500 Subject: [PATCH] [Plugins] [Auth] [JWT] Add addtional JWT headers input (#247) Co-authored-by: Gregory Schier --- crates/yaak-plugins/bindings/gen_events.ts | 33 ++++- crates/yaak-plugins/src/events.rs | 15 ++- .../src/bindings/gen_events.ts | 33 ++++- plugins/auth-jwt/README.md | 5 +- plugins/auth-jwt/src/index.ts | 123 +++++++++++------- src-web/components/DynamicForm.tsx | 70 +++++++++- src-web/components/core/PairEditor.tsx | 4 +- 7 files changed, 222 insertions(+), 61 deletions(-) diff --git a/crates/yaak-plugins/bindings/gen_events.ts b/crates/yaak-plugins/bindings/gen_events.ts index f10515d6..e217e577 100644 --- a/crates/yaak-plugins/bindings/gen_events.ts +++ b/crates/yaak-plugins/bindings/gen_events.ts @@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array, }; 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, 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 = { diff --git a/crates/yaak-plugins/src/events.rs b/crates/yaak-plugins/src/events.rs index bef16a96..e478a99f 100644 --- a/crates/yaak-plugins/src/events.rs +++ b/crates/yaak-plugins/src/events.rs @@ -48,11 +48,7 @@ impl PluginContext { } pub fn new(label: Option, workspace_id: Option) -> 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, } +#[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")] diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index f10515d6..e217e577 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -92,7 +92,7 @@ export type FindHttpResponsesResponse = { httpResponses: Array, }; 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, 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 = { diff --git a/plugins/auth-jwt/README.md b/plugins/auth-jwt/README.md index b7f1f3f5..34985436 100644 --- a/plugins/auth-jwt/README.md +++ b/plugins/auth-jwt/README.md @@ -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 diff --git a/plugins/auth-jwt/src/index.ts b/plugins/auth-jwt/src/index.ts index e6e2d9ef..158d7fda 100644 --- a/plugins/auth-jwt/src/index.ts +++ b/plugins/auth-jwt/src/index.ts @@ -46,69 +46,98 @@ export const plugin: PluginDefinition = { name: 'secretBase64', label: 'Secret is base64 encoded', }, - { - type: 'select', - name: 'location', - label: 'Behavior', - defaultValue: 'header', - options: [ - { label: 'Insert Header', value: 'header' }, - { label: 'Append Query Parameter', value: 'query' }, - ], - }, - { - 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', - name: 'headerPrefix', - label: 'Header Prefix', - optional: true, - defaultValue: 'Bearer', - dynamic(_ctx, args) { - if (args.values.location === 'query') { - return { - hidden: true, - }; - } - }, - }, { type: 'editor', name: 'payload', - label: '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', + label: 'Behavior', + defaultValue: 'header', + options: [ + { label: 'Insert Header', value: 'header' }, + { label: 'Append Query Parameter', value: 'query' }, + ], + }, + { + type: 'h_stack', + inputs: [ + { + type: 'text', + name: 'name', + label: 'Header Name', + defaultValue: 'Authorization', + optional: true, + description: 'The name of the header to add to the request', + }, + { + type: 'text', + name: 'headerPrefix', + label: 'Header Prefix', + optional: true, + defaultValue: 'Bearer', + }, + ], + dynamic(_ctx, args) { + if (args.values.location === 'query') { + return { + hidden: true, + }; + } + }, + }, + { + 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'); diff --git a/src-web/components/DynamicForm.tsx b/src-web/components/DynamicForm.tsx index 48757df4..244fa495 100644 --- a/src-web/components/DynamicForm.tsx +++ b/src-web/components/DynamicForm.tsx @@ -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'; @@ -80,7 +83,7 @@ export function DynamicForm>({ function FormInputsStack>({ className, ...props -}: FormInputsProps & { className?: string }) { +}: FormInputsProps & { className?: string}) { return ( >({ summary={input.label} className={classNames('!mb-auto', disabled && 'opacity-disabled')} > -
+
>({ ); case 'markdown': return {input.content}; + case 'key_value': + return ( + 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 ( +
+ + +
+ ); +} diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index 49b58202..e56de8c2 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -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', )} >