feat(common): add advanced configuration for auth and token request parameters (#5253)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam 2025-07-29 00:13:08 +06:00 committed by GitHub
parent 8f5eed5151
commit b07212cb05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1425 additions and 112 deletions

View file

@ -485,17 +485,17 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] =
[
{
v: 8,
v: 9,
id: "clx1f86hv000010f8szcfya0t",
name: "Multiple child collections with authorization & headers set at each level",
folders: [
{
v: 8,
v: 9,
id: "clx1fjgah000110f8a5bs68gd",
name: "folder-1",
folders: [
{
v: 8,
v: 9,
id: "clx1fjwmm000410f8l1gkkr1a",
name: "folder-11",
folders: [],
@ -537,7 +537,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fjyxm000510f8pv90dt43",
name: "folder-12",
folders: [],
@ -595,7 +595,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fk1cv000610f88kc3aupy",
name: "folder-13",
folders: [],
@ -707,12 +707,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fjk9o000210f8j0573pls",
name: "folder-2",
folders: [
{
v: 8,
v: 9,
id: "clx1fk516000710f87sfpw6bo",
name: "folder-21",
folders: [],
@ -752,7 +752,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fk72t000810f8gfwkpi5y",
name: "folder-22",
folders: [],
@ -810,7 +810,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fk95g000910f8bunhaoo8",
name: "folder-23",
folders: [],
@ -915,12 +915,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1fjmlq000310f86o4d3w2o",
name: "folder-3",
folders: [
{
v: 8,
v: 9,
id: "clx1iwq0p003e10f8u8zg0p85",
name: "folder-31",
folders: [],
@ -960,7 +960,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1izut7003m10f894ip59zg",
name: "folder-32",
folders: [],
@ -1018,7 +1018,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
],
},
{
v: 8,
v: 9,
id: "clx1j2ka9003q10f8cdbzpgpg",
name: "folder-33",
folders: [],

View file

@ -76,6 +76,71 @@ const normalizeEnvironmentVariable = (variable: HoppEnvPair): HoppEnvPair => {
};
};
/**
* Transforms the given `HoppRESTAuth` object to ensure it conforms to the latest
* OAuth 2.0 authentication structure. Depending on the `grantType` within the
* `grantTypeInfo` property, this function adds or initializes specific fields
* such as `clientAuthentication`, `authRequestParams`, `tokenRequestParams`,
* and `refreshRequestParams` to maintain compatibility with updated schema
* requirements.
*
* - For "CLIENT_CREDENTIALS" grant type, sets `clientAuthentication` to "IN_BODY"
* and initializes `tokenRequestParams` and `refreshRequestParams` as empty arrays.
* - For "AUTHORIZATION_CODE" grant type, initializes `authRequestParams`,
* `tokenRequestParams`, and `refreshRequestParams` as empty arrays.
* - For "PASSWORD" grant type, initializes `tokenRequestParams` and
* `refreshRequestParams` as empty arrays.
* - For "IMPLICIT" grant type, initializes `authRequestParams` and
* `refreshRequestParams` as empty arrays.
*
* If the `authType` is not "oauth-2", the original `auth` object is returned unchanged.
*
* @param {HoppRESTAuth} auth - The authentication object to transform.
* @returns {HoppRESTAuth} The transformed authentication object with updated grant type information.
*/
const transformAuth = (auth: HoppRESTAuth): HoppRESTAuth => {
if (auth.authType === "oauth-2") {
const oldGrantTypeInfo = auth.grantTypeInfo;
let newGrantTypeInfo = oldGrantTypeInfo;
// Add clientAuthentication for CLIENT_CREDENTIALS
if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
clientAuthentication: "IN_BODY",
tokenRequestParams: [],
refreshRequestParams: [],
};
} else if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
};
} else if (oldGrantTypeInfo.grantType === "PASSWORD") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
};
} else if (oldGrantTypeInfo.grantType === "IMPLICIT") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
refreshRequestParams: [],
};
}
return {
...auth,
grantTypeInfo: newGrantTypeInfo,
};
}
return auth;
};
/**
* Transforms workspace environment data to the `HoppEnvironment` format.
*
@ -115,21 +180,9 @@ export const transformWorkspaceCollections = (
const { auth = { authType: "inherit", authActive: true }, headers = [] } =
parsedData;
const migratedAuth: HoppRESTAuth =
auth.authType === "oauth-2"
? {
...auth,
grantTypeInfo:
auth.grantTypeInfo.grantType === "CLIENT_CREDENTIALS"
? {
...auth.grantTypeInfo,
clientAuthentication: "IN_BODY",
}
: auth.grantTypeInfo,
}
: auth;
const transformedAuth = transformAuth(auth);
const migratedHeaders = headers.map((header) =>
const transformedHeaders = headers.map((header) =>
header.description ? header : { ...header, description: "" }
);
@ -142,8 +195,8 @@ export const transformWorkspaceCollections = (
name: title,
folders: transformWorkspaceCollections(folders),
requests: transformWorkspaceRequests(requests),
auth: migratedAuth,
headers: migratedHeaders,
auth: transformedAuth,
headers: transformedHeaders,
};
});
};

View file

@ -241,7 +241,11 @@
"label_send_as": "Client Authentication",
"label_send_in_body": "Send Credentials in Body",
"label_send_as_basic_auth": "Send Credentials as Basic Auth",
"enter_value": "Enter value"
"enter_value": "Enter value",
"auth_request": "Auth Request",
"token_request": "Token Request",
"refresh_request": "Refresh Request",
"send_in": "Send In"
},
"pass_key_by": "Pass by",
"pass_by_query_params_label": "Query Parameters",

View file

@ -211,7 +211,9 @@ declare module 'vue' {
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideChevronUp: typeof import('~icons/lucide/chevron-up')['default']
IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
@ -221,7 +223,6 @@ declare module 'vue' {
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
@ -254,6 +255,7 @@ declare module 'vue' {
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default']
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']

View file

@ -23,7 +23,7 @@
:model-value="name"
:placeholder="t('count.key')"
:auto-complete-source="keyAutoCompleteSource"
:auto-complete-env="true"
:auto-complete-env="autoCompleteEnv"
:envs="envs"
:inspection-results="inspectionKeyResult"
@update:model-value="emit('update:name', $event)"
@ -41,7 +41,7 @@
:class="{ 'opacity-50': !entityActive }"
:model-value="value"
:placeholder="t('count.value')"
:auto-complete-env="true"
:auto-complete-env="autoCompleteEnv"
:envs="envs"
:inspection-results="inspectionValueResult"
@update:model-value="emit('update:value', $event)"
@ -56,7 +56,10 @@
"
/>
<slot name="after-value"></slot>
<input
v-if="showDescription"
:value="description"
:placeholder="t('count.description')"
class="flex flex-1 px-4 bg-transparent"
@ -133,7 +136,9 @@ type Entity = {
const t = useI18n()
withDefaults(
defineProps<{
showDescription?: boolean
total: number
index: number
entityId: number
@ -145,8 +150,19 @@ defineProps<{
inspectionValueResult?: InspectorResult[]
description?: string
envs?: AggregateEnvironment[]
autoCompleteEnv?: boolean
keyAutoCompleteSource?: string[]
}>()
}>(),
{
showDescription: true,
description: "",
inspectionKeyResult: () => [],
inspectionValueResult: () => [],
envs: () => [],
autoCompleteEnv: true,
keyAutoCompleteSource: () => [],
}
)
const emit = defineEmits<{
(e: "update:name", value: string): void

View file

@ -172,6 +172,364 @@
</span>
</div>
<div class="flex flex-col">
<div
class="flex cursor-pointer items-center justify-between py-2 pl-4 text-secondaryLight transition hover:text-secondary"
@click="toggleAdvancedConfig"
>
<span class="select-none">{{ t("authorization.advance_config") }}</span>
<IconChevronDown
v-if="!isAdvancedConfigExpanded"
class="mr-4 opacity-50"
/>
<IconChevronUp v-else class="mr-4 opacity-50" />
</div>
<div v-show="isAdvancedConfigExpanded">
<div class="flex flex-col border-t border-dividerLight">
<!-- Auth Request Parameters Section -->
<div class="border-b border-dividerLight">
<div class="flex items-center justify-between p-4">
<label class="font-semibold text-secondaryLight">
{{ t("authorization.oauth.auth_request") }}
</label>
</div>
<div>
<!-- Column Headers -->
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight bg-primaryLight"
>
<span class="w-8"></span>
<!-- Drag handle space -->
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.key") }}
</span>
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.value") }}
</span>
<span class="w-8"></span>
<!-- Active/Inactive toggle space -->
<span class="w-8"></span>
<!-- Delete button space -->
</div>
<div
v-if="!workingAuthRequestParams.length"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<span class="text-center">
{{ t("empty.parameters") }}
</span>
<HoppButtonSecondary
class="mt-2"
:icon="IconPlus"
:label="`${t('action.add')}`"
@click="addAuthRequestParam()"
/>
</div>
<!-- Parameter rows -->
<div class="divide-y divide-dividerLight" v-else>
<HttpKeyValue
:show-description="false"
v-for="(param, index) in workingAuthRequestParams"
:key="`auth-request-param-${param.id}`"
v-model:name="param.key"
v-model:value="param.value"
:total="workingAuthRequestParams.length"
:index="index"
:entity-id="param.id"
:entity-active="param.active"
:is-active="param.hasOwnProperty('active')"
:envs="envs"
:auto-complete-env="true"
:key-auto-complete-source="commonOAuth2AuthParams"
@update-entity="
updateAuthRequestParam($event.index, $event.payload)
"
@delete-entity="deleteAuthRequestParam($event)"
/>
</div>
</div>
</div>
</div>
<div class="flex flex-col border-t border-dividerLight">
<!-- Token Request Parameters Section -->
<div class="border-b border-dividerLight">
<div class="flex items-center justify-between p-4">
<label class="font-semibold text-secondaryLight">
{{ t("authorization.oauth.token_request") }}
</label>
</div>
<div>
<!-- Column Headers -->
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight bg-primaryLight"
>
<span class="w-8"></span>
<!-- Drag handle space -->
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.key") }}
</span>
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.value") }}
</span>
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("authorization.oauth.send_in") }}
</span>
<span class="w-8"></span>
<!-- Active/Inactive toggle space -->
<span class="w-8"></span>
<!-- Delete button space -->
</div>
<div
v-if="!workingTokenRequestParams.length"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<span class="text-center">
{{ t("empty.parameters") }}
</span>
<HoppButtonSecondary
class="mt-2"
:icon="IconPlus"
:label="`${t('action.add')}`"
@click="addTokenRequestParam()"
/>
</div>
<!-- Parameter rows -->
<div class="divide-y divide-dividerLight" v-else>
<HttpKeyValue
:show-description="false"
v-for="(param, index) in workingTokenRequestParams"
:key="`token-request-param-${param.id}`"
v-model:name="param.key"
v-model:value="param.value"
:total="workingTokenRequestParams.length"
:index="index"
:entity-id="param.id"
:entity-active="param.active"
:is-active="param.hasOwnProperty('active')"
:envs="envs"
:auto-complete-env="true"
:key-auto-complete-source="commonOAuth2TokenParams"
@update-entity="
updateTokenRequestParam($event.index, {
id: $event.payload.id,
key: $event.payload.key,
value: $event.payload.value,
sendIn: param.sendIn,
active: $event.payload.active,
})
"
@delete-entity="deleteTokenRequestParam($event)"
>
<template #after-value>
<div class="flex flex-1">
<tippy interactive trigger="click" theme="popover">
<HoppSmartSelectWrapper>
<HoppButtonSecondary
:class="{ 'opacity-50': !param.active }"
class="flex-1 rounded-none text-left"
:label="
sendInOptions.find(
(option) => option.value === param.sendIn
)?.label || t('authorization.oauth.send_in')
"
/>
</HoppSmartSelectWrapper>
<template #content="{ hide }">
<div
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-for="option in sendInOptions"
:key="option.value"
:label="option.label"
:icon="
param.sendIn === option.value
? IconCircleDot
: IconCircle
"
:active="param.sendIn === option.value"
@click="
() => {
updateTokenRequestParam(index, {
...param,
sendIn: option.value as
| 'headers'
| 'body'
| 'url',
})
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
</HttpKeyValue>
</div>
</div>
</div>
</div>
<div class="flex flex-col border-t border-dividerLight">
<!-- Refresh Request Parameters Section -->
<div class="border-b border-dividerLight">
<div class="flex items-center justify-between p-4">
<label class="font-semibold text-secondaryLight">
{{ t("authorization.oauth.refresh_request") }}
</label>
</div>
<div>
<!-- Column Headers -->
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight bg-primaryLight"
>
<span class="w-8"></span>
<!-- Drag handle space -->
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.key") }}
</span>
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("count.value") }}
</span>
<span
class="flex-1 px-4 py-2 text-xs font-semibold text-secondaryLight"
>
{{ t("authorization.oauth.send_in") }}
</span>
<span class="w-8"></span>
<!-- Active/Inactive toggle space -->
<span class="w-8"></span>
<!-- Delete button space -->
</div>
<div
v-if="!workingRefreshRequestParams.length"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<span class="text-center">
{{ t("empty.parameters") }}
</span>
<HoppButtonSecondary
class="mt-2"
:icon="IconPlus"
:label="`${t('action.add')}`"
@click="addRefreshRequestParam()"
/>
</div>
<!-- Parameter rows -->
<div class="divide-y divide-dividerLight" v-else>
<HttpKeyValue
:show-description="false"
v-for="(param, index) in workingRefreshRequestParams"
:key="`refresh-request-param-${param.id}`"
v-model:name="param.key"
v-model:value="param.value"
:total="workingRefreshRequestParams.length"
:index="index"
:entity-id="param.id"
:entity-active="param.active"
:is-active="param.hasOwnProperty('active')"
:envs="envs"
:auto-complete-env="true"
:key-auto-complete-source="commonOAuth2RefreshParams"
@update-entity="
updateRefreshRequestParam($event.index, {
id: $event.payload.id,
key: $event.payload.key,
value: $event.payload.value,
sendIn: param.sendIn,
active: $event.payload.active,
})
"
@delete-entity="deleteRefreshRequestParam($event)"
>
<template #after-value>
<div class="flex flex-1">
<tippy interactive trigger="click" theme="popover">
<HoppSmartSelectWrapper>
<HoppButtonSecondary
:class="{ 'opacity-50': !param.active }"
class="flex-1 rounded-none text-left"
:label="
sendInOptions.find(
(option) => option.value === param.sendIn
)?.label || t('authorization.oauth.send_in')
"
/>
</HoppSmartSelectWrapper>
<template #content="{ hide }">
<div
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-for="option in sendInOptions"
:key="option.value"
:label="option.label"
:icon="
param.sendIn === option.value
? IconCircleDot
: IconCircle
"
:active="param.sendIn === option.value"
@click="
() => {
updateRefreshRequestParam(index, {
...param,
sendIn: option.value as
| 'headers'
| 'body'
| 'url',
})
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
</HttpKeyValue>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-2 gap-1 flex">
<HoppButtonSecondary
filled
@ -192,12 +550,21 @@
import { HoppGQLAuthOAuth2, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as E from "fp-ts/Either"
import { Ref, computed, ref } from "vue"
import { Ref, computed, ref, watch, onMounted } from "vue"
import { z } from "zod"
import { useI18n } from "~/composables/i18n"
import { refWithCallbackOnChange } from "~/composables/ref"
import { useToast } from "~/composables/toast"
import { replaceTemplateStringsInObjectValues } from "~/helpers/auth"
import {
replaceTemplateString,
replaceTemplateStringsInObjectValues,
} from "~/helpers/auth"
import {
commonOAuth2AuthParams,
commonOAuth2RefreshParams,
commonOAuth2TokenParams,
sendInOptions,
} from "~/helpers/oauth2Params"
import { AggregateEnvironment } from "~/newstore/environments"
import authCode, {
AuthCodeOauthFlowParams,
@ -225,6 +592,9 @@ import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconChevronUp from "~icons/lucide/chevron-up"
import IconPlus from "~icons/lucide/plus"
const t = useI18n()
const toast = useToast()
@ -417,6 +787,33 @@ const supportedGrantTypes = [
scopes: scopes.value,
isPKCE: isPKCE.value,
codeVerifierMethod: codeChallenge.value?.id,
authRequestParams: workingAuthRequestParams.value
.filter((p) => p.active && p.key && p.value)
.map((p) => ({
id: p.id,
key: replaceTemplateString(p.key),
value: replaceTemplateString(p.value),
active: p.active,
sendIn: p.sendIn,
})),
tokenRequestParams: workingTokenRequestParams.value
.filter((p) => p.active && p.key && p.value)
.map((p) => ({
id: p.id,
key: replaceTemplateString(p.key),
value: replaceTemplateString(p.value),
active: p.active,
sendIn: p.sendIn,
})),
refreshRequestParams: workingRefreshRequestParams.value
.filter((p) => p.active && p.key && p.value)
.map((p) => ({
id: p.id,
key: replaceTemplateString(p.key),
value: replaceTemplateString(p.value),
active: p.active,
sendIn: p.sendIn,
})),
}
const unwrappedParams = replaceTemplateStringsInObjectValues(params)
@ -998,7 +1395,7 @@ const setAccessTokenInActiveContext = (
tabService.currentActiveTab.value.document.request.auth.authType ===
"oauth-2"
) {
// @ts-expect-error - todo: narrow the grantType to only supporting refresh tokens
// @ts-expect-error - TODO: narrow the grantType to only supporting refresh tokens
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken =
refreshToken
}
@ -1133,4 +1530,305 @@ const grantTypeTippyActions = ref<HTMLElement | null>(null)
const pkceTippyActions = ref<HTMLElement | null>(null)
const authTippyActions = ref<HTMLElement | null>(null)
const clientAuthenticationTippyActions = ref<HTMLElement | null>(null)
// Advanced Configuration state
const isAdvancedConfigExpanded = ref(false)
const toggleAdvancedConfig = () => {
isAdvancedConfigExpanded.value = !isAdvancedConfigExpanded.value
}
// Advanced Configuration: Auth Request Parameters
type OAuth2AdvancedParam = {
id: number
key: string
value: string
active: boolean
sendIn?: "headers" | "url" | "body"
}
let paramsIdCounter = 1000
// Initialize working auth request params
const workingAuthRequestParams = ref<OAuth2AdvancedParam[]>([
{ id: paramsIdCounter++, key: "", value: "", active: true },
])
// Watch for changes in working auth request params
watch(
workingAuthRequestParams,
(newParams: OAuth2AdvancedParam[]) => {
// Auto-add empty row when the last row is filled
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
workingAuthRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
active: true,
})
}
// Update auth.value.grantTypeInfo with non-empty params
const nonEmptyParams = newParams.filter(
(p: OAuth2AdvancedParam) => p.key !== "" || p.value !== ""
)
if ("authRequestParams" in auth.value.grantTypeInfo) {
auth.value.grantTypeInfo.authRequestParams = nonEmptyParams.map(
(param) => ({
id: param.id,
key: param.key,
value: param.value,
active: param.active,
})
)
}
},
{ deep: true }
)
// Functions for auth request params management
const addAuthRequestParam = () => {
workingAuthRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
active: true,
})
}
const updateAuthRequestParam = (
index: number,
payload: OAuth2AdvancedParam
) => {
workingAuthRequestParams.value[index] = payload
}
const deleteAuthRequestParam = (index: number) => {
// Only delete if it's not the last empty row, or if there are multiple rows
if (workingAuthRequestParams.value.length > 1) {
workingAuthRequestParams.value.splice(index, 1)
}
}
// Token Request Parameters
interface OAuth2TokenParam {
id: number
key: string
value: string
sendIn: "headers" | "body" | "url"
active: boolean
}
// Initialize working token request params
const workingTokenRequestParams = ref<OAuth2TokenParam[]>([
{
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
},
])
// Watch for changes in working token request params
watch(
workingTokenRequestParams,
(newParams: OAuth2TokenParam[]) => {
// Auto-add empty row when the last row is filled
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
workingTokenRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
})
}
// Update auth.value.grantTypeInfo with non-empty params
const nonEmptyParams = newParams.filter(
(p: OAuth2TokenParam) => p.key !== "" || p.value !== ""
)
if ("tokenRequestParams" in auth.value.grantTypeInfo) {
auth.value.grantTypeInfo.tokenRequestParams = nonEmptyParams.map(
(param) => ({
id: param.id,
key: param.key,
value: param.value,
sendIn: param.sendIn,
active: param.active,
})
)
}
},
{ deep: true }
)
// Functions for token request params management
const addTokenRequestParam = () => {
workingTokenRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
})
}
const updateTokenRequestParam = (index: number, payload: OAuth2TokenParam) => {
workingTokenRequestParams.value[index] = payload
}
const deleteTokenRequestParam = (index: number) => {
// Only delete if it's not the last empty row, or if there are multiple rows
if (workingTokenRequestParams.value.length > 1) {
workingTokenRequestParams.value.splice(index, 1)
}
}
// Refresh Request Parameters
interface OAuth2RefreshParam {
id: number
key: string
value: string
sendIn: "headers" | "body" | "url"
active: boolean
}
// Initialize working refresh request params
const workingRefreshRequestParams = ref<OAuth2RefreshParam[]>([
{
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
},
])
// Watch for changes in working refresh request params
watch(
workingRefreshRequestParams,
(newParams: OAuth2RefreshParam[]) => {
// Auto-add empty row when the last row is filled
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
workingRefreshRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
})
}
// Update auth.value.grantTypeInfo with non-empty params
const nonEmptyParams = newParams.filter(
(p: OAuth2RefreshParam) => p.key !== "" || p.value !== ""
)
if ("refreshRequestParams" in auth.value.grantTypeInfo) {
auth.value.grantTypeInfo.refreshRequestParams = nonEmptyParams.map(
(param) => ({
id: param.id,
key: param.key,
value: param.value,
sendIn: param.sendIn,
active: param.active,
})
)
}
},
{ deep: true }
)
// Functions for refresh request params management
const addRefreshRequestParam = () => {
workingRefreshRequestParams.value.push({
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
})
}
const updateRefreshRequestParam = (
index: number,
payload: OAuth2RefreshParam
) => {
workingRefreshRequestParams.value[index] = payload
}
const deleteRefreshRequestParam = (index: number) => {
// Only delete if it's not the last empty row, or if there are multiple rows
if (workingRefreshRequestParams.value.length > 1) {
workingRefreshRequestParams.value.splice(index, 1)
}
}
// Initialize advanced parameters from the auth object when component mounts
onMounted(() => {
if (
"authRequestParams" in auth.value.grantTypeInfo &&
auth.value.grantTypeInfo.authRequestParams &&
auth.value.grantTypeInfo.authRequestParams.length > 0
) {
workingAuthRequestParams.value =
auth.value.grantTypeInfo.authRequestParams.map((param) => ({
id: param.id || paramsIdCounter++,
key: param.key,
value: param.value,
active: param.active,
}))
}
if (
"tokenRequestParams" in auth.value.grantTypeInfo &&
auth.value.grantTypeInfo.tokenRequestParams &&
auth.value.grantTypeInfo.tokenRequestParams.length > 0
) {
workingTokenRequestParams.value = [
...auth.value.grantTypeInfo.tokenRequestParams.map((param) => ({
id: param.id || paramsIdCounter++,
key: param.key,
value: param.value,
sendIn: param.sendIn || "body",
active: param.active,
})),
{
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
},
]
}
if (
"refreshRequestParams" in auth.value.grantTypeInfo &&
auth.value.grantTypeInfo.refreshRequestParams &&
auth.value.grantTypeInfo.refreshRequestParams.length > 0
) {
workingRefreshRequestParams.value = [
...auth.value.grantTypeInfo.refreshRequestParams.map((param) => ({
id: param.id || paramsIdCounter++,
key: param.key,
value: param.value,
sendIn: param.sendIn || "body",
active: param.active,
})),
{
id: paramsIdCounter++,
key: "",
value: "",
sendIn: "body",
active: true,
},
]
}
})
</script>

View file

@ -56,3 +56,7 @@ export const replaceTemplateStringsInObjectValues = <
return newObj as T
}
export const replaceTemplateString = (str: string): string => {
return replaceTemplateStringsInObjectValues({ value: str }).value
}

View file

@ -401,7 +401,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
.forEach(({ key, value }) => (finalHeaders[key] = value))
const gqlRequest: HoppGQLRequest = {
v: 8,
v: 9,
name: options.name || "Untitled Request",
url: finalUrl,
headers: request.headers,

View file

@ -136,7 +136,7 @@ describe("GraphQL Response Transformation", () => {
describe("GraphQL Request Transformation", () => {
const baseRequest: HoppGQLRequest = {
v: 8,
v: 9,
name: "Test Query",
url: "https://api.example.com/graphql",
headers: [],
@ -235,7 +235,7 @@ describe("REST Response Transformation", () => {
}
const originalRequest: HoppRESTRequest = {
v: "11",
v: "15",
endpoint: "https://api.example.com",
name: "Test Request",
method: "GET",
@ -244,7 +244,7 @@ describe("REST Response Transformation", () => {
preRequestScript: "",
testScript: "",
auth: { authType: "none", authActive: true },
body: { contentmediaType: null, body: null },
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
}
@ -271,7 +271,7 @@ describe("REST Response Transformation", () => {
}
const originalRequest: HoppRESTRequest = {
v: "11",
v: "15",
endpoint: "https://api.example.com",
name: "Test Request",
method: "GET",
@ -280,7 +280,7 @@ describe("REST Response Transformation", () => {
preRequestScript: "",
testScript: "",
auth: { authType: "none", authActive: true },
body: { contentmediaType: null, body: null },
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
}
@ -300,7 +300,7 @@ describe("REST Response Transformation", () => {
describe("REST Request Transformation", () => {
const baseEffectiveRequest: EffectiveHoppRESTRequest = {
v: "11",
v: "15",
name: "Test Request",
method: "GET",
endpoint: "https://api.example.com",
@ -310,7 +310,7 @@ describe("REST Request Transformation", () => {
preRequestScript: "",
testScript: "",
auth: { authType: "none", authActive: true },
body: { contentmediaType: null, body: null },
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
effectiveFinalHeaders: [],

View file

@ -0,0 +1,59 @@
export const commonOAuth2AuthParams = [
"audience",
"scope",
"state",
"nonce",
"prompt",
"max_age",
"ui_locales",
"id_token_hint",
"login_hint",
"acr_values",
"response_mode",
"display",
"claims",
"request",
"request_uri",
]
export const commonOAuth2TokenParams = [
"grant_type",
"code",
"redirect_uri",
"client_id",
"client_secret",
"code_verifier",
"username",
"password",
"scope",
"audience",
"resource",
"assertion",
"assertion_type",
"refresh_token",
]
export const commonOAuth2RefreshParams = [
"grant_type",
"refresh_token",
"client_id",
"client_secret",
"scope",
"audience",
"resource",
]
export const sendInOptions = [
{
label: "Request Body",
value: "body",
},
{
label: "Request URL",
value: "url",
},
{
label: "Request Headers",
value: "headers",
},
]

View file

@ -10,20 +10,46 @@ import { z } from "zod"
import { getService } from "~/modules/dioc"
import * as E from "fp-ts/Either"
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
import { AuthCodeGrantTypeParams } from "@hoppscotch/data"
import { content } from "@hoppscotch/kernel"
const persistenceService = getService(PersistenceService)
const interceptorService = getService(KernelInterceptorService)
const AuthCodeOauthFlowParamsSchema = AuthCodeGrantTypeParams.pick({
authEndpoint: true,
tokenEndpoint: true,
clientID: true,
clientSecret: true,
scopes: true,
isPKCE: true,
codeVerifierMethod: true,
const AuthCodeOauthFlowParamsSchema = z
.object({
authEndpoint: z.string(),
tokenEndpoint: z.string(),
clientID: z.string(),
clientSecret: z.string().optional(),
scopes: z.string().optional(),
isPKCE: z.boolean(),
codeVerifierMethod: z.enum(["plain", "S256"]).optional(),
authRequestParams: z.array(
z.object({
id: z.number(),
key: z.string(),
value: z.string(),
active: z.boolean(),
})
),
refreshRequestParams: z.array(
z.object({
id: z.number(),
key: z.string(),
value: z.string(),
active: z.boolean(),
sendIn: z.enum(["headers", "url", "body"]).optional(),
})
),
tokenRequestParams: z.array(
z.object({
id: z.number(),
key: z.string(),
value: z.string(),
active: z.boolean(),
sendIn: z.enum(["headers", "url", "body"]).optional(),
})
),
})
.refine(
(params) => {
@ -63,6 +89,9 @@ export const getDefaultAuthCodeOauthFlowParams =
scopes: undefined,
isPKCE: false,
codeVerifierMethod: "S256",
authRequestParams: [],
refreshRequestParams: [],
tokenRequestParams: [],
})
const initAuthCodeOauthFlow = async ({
@ -73,6 +102,9 @@ const initAuthCodeOauthFlow = async ({
authEndpoint,
isPKCE,
codeVerifierMethod,
authRequestParams,
refreshRequestParams,
tokenRequestParams,
}: AuthCodeOauthFlowParams) => {
const state = generateRandomString()
@ -99,6 +131,24 @@ const initAuthCodeOauthFlow = async ({
codeVerifierMethod?: string
codeChallenge?: string
scopes?: string
authRequestParams?: Array<{
key: string
value: string
active: boolean
sendIn?: string
}>
refreshRequestParams?: Array<{
key: string
value: string
active: boolean
sendIn?: string
}>
tokenRequestParams?: Array<{
key: string
value: string
active: boolean
sendIn?: string
}>
} = {
state,
grant_type: "AUTHORIZATION_CODE",
@ -109,6 +159,9 @@ const initAuthCodeOauthFlow = async ({
isPKCE,
codeVerifierMethod,
scopes,
authRequestParams,
refreshRequestParams,
tokenRequestParams,
}
if (codeVerifier && codeChallenge) {
@ -160,6 +213,14 @@ const initAuthCodeOauthFlow = async ({
url.searchParams.set("code_challenge_method", codeVerifierMethod)
}
if (authRequestParams.length > 0) {
authRequestParams.forEach((param) => {
if (param.active && param.key && param.value) {
url.searchParams.set(param.key, param.value)
}
})
}
// Redirect to the authorization server
window.location.assign(url.toString())

View file

@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 8,
v: 9,
name: "Echo",
requests: [
{
@ -57,11 +57,11 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 8,
v: 9,
name: "Echo",
requests: [
{
v: 8,
v: 9,
name: "Echo test",
url: "https://echo.hoppscotch.io/graphql",
headers: [],
@ -182,7 +182,7 @@ export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [
{
v: 1,
request: {
v: 8,
v: 9,
name: "Untitled",
url: "https://echo.hoppscotch.io/graphql",
query: "query Request { url }",
@ -203,7 +203,7 @@ export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
doc: {
request: {
v: 8,
v: 9,
name: "Untitled",
url: "https://echo.hoppscotch.io/graphql",
headers: [],

View file

@ -8,6 +8,7 @@ import V5_VERSION from "./v/5"
import V6_VERSION from "./v/6"
import V7_VERSION from "./v/7"
import V8_VERSION from "./v/8"
import V9_VERSION from "./v/9"
import { z } from "zod"
import { translateToNewRequest } from "../rest"
@ -19,7 +20,7 @@ const versionedObject = z.object({
})
export const HoppCollection = createVersionedEntity({
latestVersion: 8,
latestVersion: 9,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
@ -29,6 +30,7 @@ export const HoppCollection = createVersionedEntity({
6: V6_VERSION,
7: V7_VERSION,
8: V8_VERSION,
9: V9_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
@ -44,7 +46,7 @@ export const HoppCollection = createVersionedEntity({
export type HoppCollection = InferredEntity<typeof HoppCollection>
export const CollectionSchemaVersion = 8
export const CollectionSchemaVersion = 9
/**
* Generates a Collection object. This ignores the version number object

View file

@ -0,0 +1,92 @@
import { defineVersion, entityRefUptoVersion } from "verzod"
import { z } from "zod"
import { HoppGQLAuth } from "../../graphql/v/9"
import { HoppRESTAuth } from "../../rest/v/15/auth"
import { HoppCollection } from ".."
import { v8_baseCollectionSchema, V8_SCHEMA } from "./8"
export const v9_baseCollectionSchema = v8_baseCollectionSchema.extend({
v: z.literal(9),
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
})
type Input = z.input<typeof v9_baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof v9_baseCollectionSchema> & {
folders: Output[]
}
export const V9_SCHEMA = v9_baseCollectionSchema.extend({
folders: z.lazy(() => z.array(entityRefUptoVersion(HoppCollection, 9))),
}) as z.ZodType<Output, z.ZodTypeDef, Input>
export default defineVersion({
initial: false,
schema: V9_SCHEMA,
up(old: z.infer<typeof V8_SCHEMA>) {
// Migrate auth field if it's OAuth2 to include new advanced parameters
let newAuth: z.infer<typeof V9_SCHEMA>["auth"]
if (old.auth.authType === "oauth-2") {
const oldGrantTypeInfo = old.auth.grantTypeInfo
let newGrantTypeInfo
// Add the advanced parameters to the appropriate grant type
if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "PASSWORD") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "IMPLICIT") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
refreshRequestParams: [],
}
} else {
newGrantTypeInfo = oldGrantTypeInfo
}
newAuth = {
...old.auth,
grantTypeInfo: newGrantTypeInfo,
} as z.infer<typeof V9_SCHEMA>["auth"]
} else {
newAuth = old.auth
}
const result: z.infer<typeof V9_SCHEMA> = {
...old,
v: 9 as const,
auth: newAuth,
folders: old.folders.map((folder) => {
const result = HoppCollection.safeParseUpToVersion(folder, 9)
if (result.type !== "ok") {
throw new Error("Failed to migrate child collections")
}
return result.value
}),
}
return result
},
})

View file

@ -8,6 +8,7 @@ import V5_VERSION from "./v/5"
import V6_VERSION from "./v/6"
import V7_VERSION from "./v/7"
import V8_VERSION from "./v/8"
import V9_VERSION from "./v/9"
export {
HoppGQLAuthBasic,
@ -19,16 +20,16 @@ export {
export { HoppGQLAuthAPIKey } from "./v/4"
export { GQLHeader, HoppGQLAuthAWSSignature } from "./v/6"
export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/8"
export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/9"
export const GQL_REQ_SCHEMA_VERSION = 8
export const GQL_REQ_SCHEMA_VERSION = 9
const versionedObject = z.object({
v: z.number(),
})
export const HoppGQLRequest = createVersionedEntity({
latestVersion: 8,
latestVersion: 9,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
@ -38,6 +39,7 @@ export const HoppGQLRequest = createVersionedEntity({
6: V6_VERSION,
7: V7_VERSION,
8: V8_VERSION,
9: V9_VERSION,
},
getVersion(x) {
const result = versionedObject.safeParse(x)

View file

@ -0,0 +1,94 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import {
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthInherit,
HoppGQLAuthNone,
} from "./2"
import { HoppGQLAuthAPIKey } from "./4"
import { HoppGQLAuthAWSSignature } from "./6"
import { HoppRESTAuthOAuth2 } from "../../rest/v/15/auth"
import { V8_SCHEMA } from "./8"
export { HoppRESTAuthOAuth2 as HoppGQLAuthOAuth2 } from "../../rest/v/15/auth"
export const HoppGQLAuth = z
.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthInherit,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppRESTAuthOAuth2,
HoppGQLAuthAPIKey,
HoppGQLAuthAWSSignature,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
export const V9_SCHEMA = V8_SCHEMA.extend({
v: z.literal(9),
auth: HoppGQLAuth,
})
export default defineVersion({
schema: V9_SCHEMA,
initial: false,
up(old: z.infer<typeof V8_SCHEMA>) {
// If the auth is OAuth2, migrate it to include the new advanced parameters
let newAuth: z.infer<typeof HoppGQLAuth>
if (old.auth.authType === "oauth-2") {
const oldGrantTypeInfo = old.auth.grantTypeInfo
let newGrantTypeInfo
// Add the advanced parameters to the appropriate grant type
if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "PASSWORD") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "IMPLICIT") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
refreshRequestParams: [],
}
} else {
newGrantTypeInfo = oldGrantTypeInfo
}
newAuth = {
...old.auth,
grantTypeInfo: newGrantTypeInfo,
} as z.infer<typeof HoppGQLAuth>
} else {
newAuth = old.auth
}
return {
...old,
v: 9 as const,
auth: newAuth,
}
},
})

View file

@ -5,6 +5,7 @@ import V2_VERSION from "./v/2"
import V3_VERSION from "./v/3"
import V4_VERSION from "./v/4"
import V5_VERSION from "./v/5"
import V6_VERSION from "./v/6"
const versionedObject = z.object({
// v is a stringified number
@ -12,13 +13,14 @@ const versionedObject = z.object({
})
export const HoppRESTResponseOriginalRequest = createVersionedEntity({
latestVersion: 5,
latestVersion: 6,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
3: V3_VERSION,
4: V4_VERSION,
5: V5_VERSION,
6: V6_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
@ -32,7 +34,7 @@ export const HoppRESTResponseOriginalRequest = createVersionedEntity({
},
})
export const HoppRESTResOriginalReqSchemaVersion = "5"
export const HoppRESTResOriginalReqSchemaVersion = "6"
export type HoppRESTResponseOriginalRequest = InferredEntity<
typeof HoppRESTResponseOriginalRequest

View file

@ -0,0 +1,65 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import { V5_SCHEMA } from "./5"
import { HoppRESTAuth } from "../../../rest/v/15/auth"
export const V6_SCHEMA = V5_SCHEMA.extend({
v: z.literal("6"),
auth: HoppRESTAuth,
})
export default defineVersion({
initial: false,
schema: V6_SCHEMA,
up(old: z.infer<typeof V5_SCHEMA>) {
// If the auth is OAuth2, migrate it to include the new advanced parameters
let newAuth: z.infer<typeof HoppRESTAuth>
if (old.auth.authType === "oauth-2") {
const oldGrantTypeInfo = old.auth.grantTypeInfo
let newGrantTypeInfo
// Add the advanced parameters to the appropriate grant type
if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "PASSWORD") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "IMPLICIT") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
refreshRequestParams: [],
}
} else {
newGrantTypeInfo = oldGrantTypeInfo
}
newAuth = {
...old.auth,
grantTypeInfo: newGrantTypeInfo,
} as z.infer<typeof HoppRESTAuth>
} else {
newAuth = old.auth
}
return {
...old,
v: "6" as const,
auth: newAuth,
}
},
})

View file

@ -21,8 +21,9 @@ import { HoppRESTReqBody } from "./v/10/body"
import V11_VERSION from "./v/11"
import V12_VERSION from "./v/12"
import V13_VERSION from "./v/13"
import { HoppRESTAuth } from "./v/13/auth"
import { HoppRESTAuth } from "./v/15/auth"
import V14_VERSION from "./v/14"
import V15_VERSION from "./v/15/index"
import { HoppRESTRequestResponses } from "../rest-request-response"
export * from "./content-types"
@ -36,32 +37,30 @@ export {
export { HoppRESTRequestVariables } from "./v/2"
export { ImplicitOauthFlowParams } from "./v/3"
export { HoppRESTAuthAPIKey } from "./v/4"
export { AuthCodeGrantTypeParams } from "./v/5"
export {
HoppRESTAuthAWSSignature,
HoppRESTHeaders,
HoppRESTParams,
} from "./v/7"
export { HoppRESTAuthDigest, PasswordGrantTypeParams } from "./v/8/auth"
export { HoppRESTAuthDigest } from "./v/8/auth"
export { FormDataKeyValue } from "./v/9/body"
export {
HoppRESTAuthOAuth2,
ClientCredentialsGrantTypeParams,
} from "./v/11/auth"
export { HoppRESTReqBody } from "./v/10/body"
export { HoppRESTAuthHAWK, HoppRESTAuthAkamaiEdgeGrid } from "./v/12/auth"
export { HoppRESTAuth, HoppRESTAuthJWT } from "./v/13/auth"
export { HoppRESTAuth, HoppRESTAuthJWT } from "./v/15/auth"
export { AuthCodeGrantTypeParams } from "./v/15/auth"
export { PasswordGrantTypeParams } from "./v/15/auth"
export { ImplicitOauthFlowParams } from "./v/15/auth"
export {
HoppRESTAuthOAuth2,
ClientCredentialsGrantTypeParams,
} from "./v/15/auth"
export {
HoppRESTRequestResponse,
@ -74,7 +73,7 @@ const versionedObject = z.object({
})
export const HoppRESTRequest = createVersionedEntity({
latestVersion: 14,
latestVersion: 15,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
@ -91,6 +90,7 @@ export const HoppRESTRequest = createVersionedEntity({
12: V12_VERSION,
13: V13_VERSION,
14: V14_VERSION,
15: V15_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
@ -133,7 +133,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
responses: lodashIsEqualEq,
})
export const RESTReqSchemaVersion = "14"
export const RESTReqSchemaVersion = "15"
export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]

View file

@ -0,0 +1,90 @@
import { z } from "zod"
import {
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthInherit,
HoppRESTAuthNone,
} from "../1"
import { HoppRESTAuthAPIKey } from "../4"
import { AuthCodeGrantTypeParams as AuthCodeGrantTypeParamsOld } from "../7"
import { HoppRESTAuthAWSSignature } from "../7"
import {
HoppRESTAuthDigest,
PasswordGrantTypeParams as PasswordGrantTypeParamsOld,
} from "../8/auth"
import { HoppRESTAuthAkamaiEdgeGrid, HoppRESTAuthHAWK } from "../12/auth"
import { HoppRESTAuthJWT } from "../13/auth"
import { ClientCredentialsGrantTypeParams as ClientCredentialsGrantTypeParamsOld } from "../11/auth"
import { ImplicitOauthFlowParams as ImplicitOauthFlowParamsOld } from "../3"
export { HoppRESTAuthJWT } from "../13/auth"
// Define the OAuth2 advanced parameter structure
const OAuth2AdvancedParam = z.object({
id: z.number(),
key: z.string(),
value: z.string(),
active: z.boolean(),
sendIn: z.enum(["headers", "url", "body"]).catch("headers"),
})
// omit sendIn from OAuth2AuthRequestParam
const OAuth2AuthRequestParam = OAuth2AdvancedParam.omit({ sendIn: true })
export const AuthCodeGrantTypeParams = AuthCodeGrantTypeParamsOld.extend({
authRequestParams: z.array(OAuth2AuthRequestParam).optional().default([]),
tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
})
export const ClientCredentialsGrantTypeParams =
ClientCredentialsGrantTypeParamsOld.extend({
tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
})
export const PasswordGrantTypeParams = PasswordGrantTypeParamsOld.extend({
tokenRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
})
export const ImplicitOauthFlowParams = ImplicitOauthFlowParamsOld.extend({
authRequestParams: z.array(OAuth2AuthRequestParam).optional().default([]),
refreshRequestParams: z.array(OAuth2AdvancedParam).optional().default([]),
})
// Extend OAuth2 with advanced parameters
export const HoppRESTAuthOAuth2 = z.object({
authType: z.literal("oauth-2"),
grantTypeInfo: z.discriminatedUnion("grantType", [
AuthCodeGrantTypeParams,
ClientCredentialsGrantTypeParams,
PasswordGrantTypeParams,
ImplicitOauthFlowParams,
]),
addTo: z.enum(["HEADERS", "QUERY_PARAMS"]).catch("HEADERS"),
})
export type HoppRESTAuthOAuth2 = z.infer<typeof HoppRESTAuthOAuth2>
export const HoppRESTAuth = z
.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthInherit,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
HoppRESTAuthAWSSignature,
HoppRESTAuthDigest,
HoppRESTAuthHAWK,
HoppRESTAuthAkamaiEdgeGrid,
HoppRESTAuthJWT,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>

View file

@ -0,0 +1,69 @@
import { V14_SCHEMA } from "../14"
import { z } from "zod"
import { defineVersion } from "verzod"
import { HoppRESTAuth } from "./auth"
export const V15_SCHEMA = V14_SCHEMA.extend({
v: z.literal("15"),
auth: HoppRESTAuth,
})
const V15_VERSION = defineVersion({
schema: V15_SCHEMA,
initial: false,
up(old: z.infer<typeof V14_SCHEMA>) {
// If the auth is OAuth2, migrate it to include the new advanced parameters
let newAuth: z.infer<typeof HoppRESTAuth>
if (old.auth.authType === "oauth-2") {
const oldGrantTypeInfo = old.auth.grantTypeInfo
let newGrantTypeInfo
// Add the advanced parameters to the appropriate grant type
if (oldGrantTypeInfo.grantType === "AUTHORIZATION_CODE") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "CLIENT_CREDENTIALS") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "PASSWORD") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
tokenRequestParams: [],
refreshRequestParams: [],
}
} else if (oldGrantTypeInfo.grantType === "IMPLICIT") {
newGrantTypeInfo = {
...oldGrantTypeInfo,
authRequestParams: [],
refreshRequestParams: [],
}
} else {
newGrantTypeInfo = oldGrantTypeInfo
}
newAuth = {
...old.auth,
grantTypeInfo: newGrantTypeInfo,
} as z.infer<typeof HoppRESTAuth>
} else {
newAuth = old.auth
}
return {
...old,
v: "15" as const,
auth: newAuth,
}
},
})
export default V15_VERSION

View file

@ -134,7 +134,7 @@ function exportedCollectionToHoppCollection(
return {
id: restCollection.id,
v: 8,
v: 9,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -196,7 +196,7 @@ function exportedCollectionToHoppCollection(
return {
id: gqlCollection.id,
v: 8,
v: 9,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -374,7 +374,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
auth: data.auth,
headers: addDescriptionField(data.headers),
})
@ -382,7 +382,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
auth: data.auth,
headers: addDescriptionField(data.headers),
})
@ -607,7 +607,7 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 8,
v: 9,
auth,
headers: addDescriptionField(headers),
}
@ -1037,7 +1037,7 @@ function transformDuplicatedCollections(
name,
folders,
requests,
v: 8,
v: 9,
auth,
headers: addDescriptionField(headers),
}

View file

@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 8,
v: 9,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection(
return {
id: gqlCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 8,
v: 9,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 8,
v: 9,
_ref_id,
auth,
headers: addDescriptionField(headers),
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 8,
v: 9,
auth,
headers: addDescriptionField(headers),
}

View file

@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 8,
v: 9,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection(
return {
id: gqlCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 8,
v: 9,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 8,
v: 9,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 8,
v: 9,
_ref_id,
auth,
headers: addDescriptionField(headers),
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 8,
v: 9,
auth,
headers: addDescriptionField(headers),
}