feat: add support for HAWK authentication (#4694)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam 2025-04-29 15:21:16 +06:00 committed by GitHub
parent cccd711507
commit ccf3c6f834
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 709 additions and 50 deletions

View file

@ -483,6 +483,24 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
expect(error).toBeTruthy();
});
describe("HAWK Authentication", () => {
test("Correctly generates and attaches authorization headers to the request ", async () => {
const COLL_PATH = getTestJsonFilePath(
"hawk-auth-success-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath(
"hawk-auth-envs.json",
"environment"
);
const args = `test ${COLL_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Test `hopp test <file_path_or_id> --delay <delay_in_ms>` command:", () => {

View file

@ -0,0 +1,43 @@
{
"v": 3,
"name": "HAWK Auth (success state) - collection",
"folders": [],
"requests": [
{
"v": "12",
"id": "cm0dm70cw000687bnxi830zz2",
"auth": {
"authType": "hawk",
"authActive": true,
"authId": "<<id>>",
"authKey": "<<key>>",
"algorithm": "<<algorithm>>",
"includePayloadHash": false,
"user": "<<user>>",
"nonce": "<<nonce>>",
"ext": "<<ext>>",
"app": "<<app>>",
"dlg": "<<dlg>>",
"timestamp": "<<timestamp>>"
},
"body": {
"body": null,
"contentType": null
},
"name": "hawk-auth-headers",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<url>>",
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);});",
"preRequestScript": "",
"responses": {},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

View file

@ -0,0 +1,47 @@
{
"v": 1,
"id": "cm0dsn3v70004p4qk3l9b7sjm",
"name": "HAWK Auth - environments",
"variables": [
{
"key": "url",
"value": "https://postman-echo.com/auth/hawk"
},
{
"key": "id",
"value": "dh37fgj492je"
},
{
"key": "key",
"value": "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"
},
{
"key": "algorithm",
"value": "sha256"
},
{
"key": "user",
"value": ""
},
{
"key": "nonce",
"value": ""
},
{
"key": "ext",
"value": ""
},
{
"key": "app",
"value": ""
},
{
"key": "dlg",
"value": ""
},
{
"key": "timestamp",
"value": ""
}
]
}

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

View file

@ -32,6 +32,8 @@ import {
generateDigestAuthHeader,
} from "./auth/digest";
import { calculateHawkHeader } from "@hoppscotch/data";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and
* applies them on current request to generate updated request.
@ -287,6 +289,44 @@ export async function getEffectiveRESTRequest(
value: authHeaderValue,
description: "",
});
} else if (request.auth.authType === "hawk") {
const { method, endpoint } = request;
const hawkHeader = await calculateHawkHeader({
url: parseTemplateString(endpoint, resolvedVariables), // URL
method: method, // HTTP method
id: parseTemplateString(request.auth.authId, resolvedVariables),
key: parseTemplateString(request.auth.authKey, resolvedVariables),
algorithm: request.auth.algorithm,
// advanced parameters (optional)
includePayloadHash: request.auth.includePayloadHash,
nonce: request.auth.nonce
? parseTemplateString(request.auth.nonce, resolvedVariables)
: undefined,
ext: request.auth.ext
? parseTemplateString(request.auth.ext, resolvedVariables)
: undefined,
app: request.auth.app
? parseTemplateString(request.auth.app, resolvedVariables)
: undefined,
dlg: request.auth.dlg
? parseTemplateString(request.auth.dlg, resolvedVariables)
: undefined,
timestamp: request.auth.timestamp
? parseInt(
parseTemplateString(request.auth.timestamp, resolvedVariables),
10
)
: undefined,
});
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: hawkHeader,
description: "",
});
}
}

View file

@ -680,7 +680,8 @@
"localaccess_unsupported": "Current interceptor does not support local access, please consider using Agent, Extension interceptors or the Desktop App"
},
"auth": {
"digest": "Agent interceptor or the Hoppscotch Desktop app are recommended when using Digest Authorization."
"digest": "Agent interceptor or the Hoppscotch Desktop app are recommended when using Digest Authorization.",
"hawk": "Agent interceptor or the Hoppscotch Desktop app are recommended when using Hawk Authorization."
},
"body": {
"binary": "Sending binary data via the current interceptor is not supported yet."

View file

@ -48,6 +48,7 @@
"@shopify/lang-jsonc": "1.0.0",
"@tauri-apps/plugin-store": "2.2.0",
"@types/markdown-it": "14.1.2",
"@types/hawk": "9.0.6",
"@unhead/vue": "1.11.10",
"@urql/core": "5.0.6",
"@urql/devtools": "2.0.3",
@ -66,6 +67,7 @@
"graphql": "16.9.0",
"graphql-language-service-interface": "2.10.2",
"graphql-tag": "2.12.6",
"hawk": "9.0.2",
"insomnia-importers": "3.6.0",
"io-ts": "2.2.21",
"js-md5": "0.8.3",

View file

@ -151,6 +151,9 @@
<div v-if="auth.authType === 'aws-signature'">
<HttpAuthorizationAWSSign v-model="auth" :envs="envs" />
</div>
<div v-if="auth.authType === 'hawk'">
<HttpAuthorizationHAWK v-model="auth" :envs="envs" />
</div>
<div v-if="auth.authType === 'digest'">
<HttpAuthorizationDigest v-model="auth" :envs="envs" />
</div>
@ -193,6 +196,7 @@ import {
HoppRESTAuth,
HoppRESTAuthAWSSignature,
HoppRESTAuthDigest,
HoppRESTAuthHAWK,
HoppRESTAuthOAuth2,
} from "@hoppscotch/data"
@ -265,6 +269,15 @@ const selectAWSSignatureAuthType = () => {
}
}
const selectHAWKAuthType = () => {
const { algorithm = "sha256" } = auth.value as HoppRESTAuthHAWK
auth.value = {
...auth.value,
authType: "hawk",
algorithm,
} as HoppRESTAuth
}
const selectDigestAuthType = () => {
const {
username = "",
@ -318,6 +331,11 @@ const authTypes: AuthType[] = [
label: "AWS Signature",
handler: selectAWSSignatureAuthType,
},
{
key: "hawk",
label: "HAWK",
handler: selectHAWKAuthType,
},
]
const authType = pluckRef(auth, "authType")

View file

@ -69,7 +69,7 @@
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.maxBody"
v-model="auth.maxBodySize"
:auto-complete-env="true"
placeholder="Max Body Size"
:envs="envs"

View file

@ -18,7 +18,9 @@
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight"> Algorithm </label>
<label class="ml-4 text-secondaryLight">
{{ t("authorization.digest.algorithm") }}
</label>
<tippy
interactive
trigger="click"
@ -59,10 +61,15 @@
<!-- advanced config -->
<div>
<div class="flex flex-col divide-y divide-dividerLight">
<!-- label as advanced config here -->
<div class="p-4">
<label class="text-secondaryLight"> Optional Config </label>
<div class="p-4 flex flex-col space-y-1">
<label>
{{ t("authorization.advance_config") }}
</label>
<p class="text-secondaryLight">
{{ t("authorization.advance_config_description") }}
</p>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
@ -114,14 +121,15 @@
</div>
</div>
<div class="px-4 mt-6">
<!-- TODO: implement include payload hash -->
<!-- <div class="px-4 my-6">
<HoppSmartCheckbox
:on="auth.includePayloadHash"
@change="auth.includePayloadHash = !auth.includePayloadHash"
>
Include Payload Hash
</HoppSmartCheckbox>
</div>
</div> -->
</template>
<script setup lang="ts">

View file

@ -35,6 +35,8 @@ import {
fetchInitialDigestAuthInfo,
generateDigestAuthHeader,
} from "../auth/digest"
import { calculateHawkHeader } from "@hoppscotch/data"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
/**
* The effective final URL.
@ -44,7 +46,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalURL: string
effectiveFinalHeaders: HoppRESTHeaders
effectiveFinalParams: HoppRESTParams
effectiveFinalBody: FormData | string | null | File
effectiveFinalBody: FormData | string | null | File | Blob
effectiveFinalRequestVariables: { key: string; value: string }[]
}
@ -245,6 +247,41 @@ export const getComputedAuthHeaders = async (
})
})
}
} else if (request.auth.authType === "hawk") {
const { method, endpoint } = req as HoppRESTRequest
const hawkHeader = await calculateHawkHeader({
url: parseTemplateString(endpoint, envVars), // URL
method: method, // HTTP method
id: parseTemplateString(request.auth.authId, envVars),
key: parseTemplateString(request.auth.authKey, envVars),
algorithm: request.auth.algorithm,
// advanced parameters (optional)
includePayloadHash: request.auth.includePayloadHash,
nonce: request.auth.nonce
? parseTemplateString(request.auth.nonce, envVars)
: undefined,
ext: request.auth.ext
? parseTemplateString(request.auth.ext, envVars)
: undefined,
app: request.auth.app
? parseTemplateString(request.auth.app, envVars)
: undefined,
dlg: request.auth.dlg
? parseTemplateString(request.auth.dlg, envVars)
: undefined,
timestamp: request.auth.timestamp
? parseInt(parseTemplateString(request.auth.timestamp, envVars), 10)
: undefined,
})
headers.push({
active: true,
key: "Authorization",
value: hawkHeader,
description: "",
})
}
return headers

View file

@ -76,6 +76,22 @@ export class RequestInspectorService extends Service implements Inspector {
},
}),
},
{
matcher: (req) => (req.auth.authType === "hawk" ? {} : null),
requires: { type: "auth", name: "hawk" },
createInspection: () => ({
id: "hawk-auth",
icon: markRaw(IconAlertTriangle),
text: { type: "text", text: this.t("inspections.auth.hawk") },
severity: 2,
isApplicable: true,
locations: { type: "url" },
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/documentation/features/inspections",
},
}),
},
{
matcher: (req) => {
const index = req.headers.findIndex((h) =>

View file

@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 6,
v: 7,
name: "Echo",
requests: [
{
@ -57,7 +57,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 6,
v: 7,
name: "Echo",
requests: [
{

View file

@ -6,6 +6,7 @@ import V3_VERSION from "./v/3"
import V4_VERSION from "./v/4"
import V5_VERSION from "./v/5"
import V6_VERSION from "./v/6"
import V7_VERSION from "./v/7"
import { z } from "zod"
import { translateToNewRequest } from "../rest"
@ -17,7 +18,7 @@ const versionedObject = z.object({
})
export const HoppCollection = createVersionedEntity({
latestVersion: 6,
latestVersion: 7,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
@ -25,6 +26,7 @@ export const HoppCollection = createVersionedEntity({
4: V4_VERSION,
5: V5_VERSION,
6: V6_VERSION,
7: V7_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
@ -40,7 +42,7 @@ export const HoppCollection = createVersionedEntity({
export type HoppCollection = InferredEntity<typeof HoppCollection>
export const CollectionSchemaVersion = 6
export const CollectionSchemaVersion = 7
/**
* Generates a Collection object. This ignores the version number object

View file

@ -0,0 +1,37 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import { HoppGQLAuth } from "../../graphql/v/8"
import { HoppRESTAuth } from "../../rest/v/12"
import { V6_SCHEMA, v6_baseCollectionSchema } from "./6"
export const v7_baseCollectionSchema = v6_baseCollectionSchema.extend({
v: z.literal(7),
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
})
type Input = z.input<typeof v7_baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof v7_baseCollectionSchema> & {
folders: Output[]
}
export const V7_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> =
v7_baseCollectionSchema.extend({
folders: z.lazy(() => z.array(V7_SCHEMA)),
})
export default defineVersion({
initial: false,
schema: V7_SCHEMA,
// @ts-expect-error
up(old: z.infer<typeof V6_SCHEMA>) {
return {
...old,
v: 7 as const,
}
},
})

View file

@ -6,3 +6,5 @@ export * from "./environment"
export * from "./global-environment"
export * from "./predefinedVariables"
export * from "./utils/collection"
export * from "./utils/hawk"
export * from "./utils/akamai-eg"

View file

@ -18,6 +18,7 @@ import V8_VERSION from "./v/8"
import V9_VERSION from "./v/9"
import V10_VERSION, { HoppRESTReqBody } from "./v/10"
import V11_VERSION, { HoppRESTAuth, HoppRESTRequestResponses } from "./v/11"
import V12_VERSION from "./v/12"
export * from "./content-types"
@ -54,11 +55,13 @@ export {
export { HoppRESTReqBody } from "./v/10"
export { HoppRESTAuthOAuth2, ClientCredentialsGrantTypeParams } from "./v/11"
export {
HoppRESTAuthOAuth2,
HoppRESTAuthHAWK,
HoppRESTAuthAkamaiEdgeGrid,
HoppRESTAuth,
ClientCredentialsGrantTypeParams,
} from "./v/11"
} from "./v/12"
const versionedObject = z.object({
// v is a stringified number
@ -66,7 +69,7 @@ const versionedObject = z.object({
})
export const HoppRESTRequest = createVersionedEntity({
latestVersion: 11,
latestVersion: 12,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
@ -80,6 +83,7 @@ export const HoppRESTRequest = createVersionedEntity({
9: V9_VERSION,
10: V10_VERSION,
11: V11_VERSION,
12: V12_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
@ -122,7 +126,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
responses: lodashIsEqualEq,
})
export const RESTReqSchemaVersion = "11"
export const RESTReqSchemaVersion = "12"
export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]

View file

@ -0,0 +1,85 @@
import {
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthInherit,
HoppRESTAuthNone,
} from "./1"
import { HoppRESTAuthAPIKey } from "./4"
import { HoppRESTAuthAWSSignature } from "./7"
import { HoppRESTAuthDigest } from "./8"
import { z } from "zod"
import { defineVersion } from "verzod"
import { HoppRESTAuthOAuth2, V11_SCHEMA } from "./11"
export const HoppRESTAuthHAWK = z.object({
authType: z.literal("hawk"),
authId: z.string().catch(""),
authKey: z.string().catch(""),
algorithm: z.enum(["sha256", "sha1"]).catch("sha256"),
includePayloadHash: z.boolean().catch(false),
// Optional fields
user: z.string().optional(),
nonce: z.string().optional(),
ext: z.string().optional(),
app: z.string().optional(),
dlg: z.string().optional(),
timestamp: z.string().optional(),
})
export const HoppRESTAuthAkamaiEdgeGrid = z.object({
authType: z.literal("akamai-eg"),
accessToken: z.string().catch(""),
clientToken: z.string().catch(""),
clientSecret: z.string().catch(""),
// Optional fields
nonce: z.string().optional(),
timestamp: z.string().optional(),
host: z.string().optional(),
headersToSign: z.string().optional(),
maxBodySize: z.string().optional(),
})
export type HoppRESTAuthHAWK = z.infer<typeof HoppRESTAuthHAWK>
export type HoppRESTAuthAkamaiEdgeGrid = z.infer<
typeof HoppRESTAuthAkamaiEdgeGrid
>
export const HoppRESTAuth = z
.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthInherit,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
HoppRESTAuthAWSSignature,
HoppRESTAuthDigest,
HoppRESTAuthHAWK,
HoppRESTAuthAkamaiEdgeGrid,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
export const V12_SCHEMA = V11_SCHEMA.extend({
v: z.literal("12"),
auth: HoppRESTAuth,
})
export default defineVersion({
schema: V12_SCHEMA,
initial: false,
up(old: z.infer<typeof V11_SCHEMA>) {
return {
...old,
v: "12" as const,
}
},
})

View file

@ -0,0 +1,65 @@
export async function calculateAkamaiEdgeGridHeader(params: {
accessToken: string
clientToken: string
clientSecret: string
url: string
method: string
body?: string // Add body parameter
nonce?: string
timestamp?: string
host?: string
headersToSign?: string
maxBodySize?: string
}) {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const timestamp = params.timestamp || Math.floor(Date.now() / 1000).toString()
const nonce = params.nonce || crypto.randomUUID()
const host = params.host || new URL(params.url).host
// 1. Create signing key using clientSecret
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(params.clientSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
)
const signingKey = await crypto.subtle.sign(
"HMAC",
keyMaterial,
encoder.encode(timestamp)
)
// 2. Calculate content hash if request has body
let contentHash = ""
if (params.body) {
const hashBuffer = await crypto.subtle.digest(
"SHA-256",
encoder.encode(params.body)
)
contentHash = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}
// 3. Create string to sign
const data = `${params.method} ${params.url} ${host} ${timestamp} ${nonce} ${contentHash}`
// 4. Sign the string using the signing key
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
keyMaterial,
encoder.encode(data)
)
const signature = btoa(
String.fromCharCode(...new Uint8Array(signatureBuffer))
)
// 5. Format the final authorization header
const authorizationHeader = `EG1-HMAC-SHA256 client_token=${params.clientToken};access_token=${params.accessToken};timestamp=${timestamp};nonce=${nonce};signature=${signature}`
return authorizationHeader
}

View file

@ -0,0 +1,146 @@
interface HawkOptions {
id: string
key: string
algorithm: "sha256" | "sha1"
method: string
url: string
includePayloadHash: boolean
payload?: string | FormData | File | null
// Optional parameters
user?: string
nonce?: string
ext?: string
app?: string
dlg?: string
timestamp?: number
}
async function generateNonce(length: number = 6): Promise<string> {
const array = new Uint8Array(length)
crypto.getRandomValues(array)
return Array.from(array)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
.substring(0, length)
}
function sha256Hash(data: string): Promise<ArrayBuffer> {
const encoder = new TextEncoder()
const dataBuffer = encoder.encode(data)
return crypto.subtle.digest("SHA-256", dataBuffer)
}
async function hmacSign(
key: string,
message: string,
algorithm: "sha256" | "sha1"
): Promise<string> {
const encoder = new TextEncoder()
const keyData = encoder.encode(key)
const messageData = encoder.encode(message)
const cryptoAlgo = algorithm === "sha256" ? "SHA-256" : "SHA-1"
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{
name: "HMAC",
hash: { name: cryptoAlgo },
},
false,
["sign"]
)
const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData)
// Convert to base64 string
return btoa(String.fromCharCode.apply(null, [...new Uint8Array(signature)]))
}
async function getPayloadContent(
payload: string | FormData | File | null
): Promise<string> {
if (!payload) return ""
if (payload instanceof FormData) {
const pairs: string[] = []
payload.forEach((value, key) => {
pairs.push(`${key}=${value}`)
})
return pairs.join("&")
}
if (payload instanceof File) {
return await payload.text()
}
return payload.toString()
}
export async function calculateHawkHeader(
options: HawkOptions
): Promise<string> {
const timestamp =
options.timestamp !== undefined && options.timestamp !== null
? options.timestamp
: Math.floor(Date.now() / 1000)
// Use provided nonce or generate a new one
const nonce =
options.nonce && options.nonce !== ""
? options.nonce
: await generateNonce()
// Parse URL
const urlObj = new URL(options.url)
const host = urlObj.hostname
const port = urlObj.port || (urlObj.protocol === "https:" ? "443" : "80")
const path = urlObj.pathname + urlObj.search
// Create the normalized string
const artifacts = {
ts: timestamp,
nonce: nonce,
method: options.method.toUpperCase(),
resource: path,
host: host,
port: port,
hash: "",
ext: options.ext || "",
}
// Calculate payload hash if needed
if (options.includePayloadHash && options.payload) {
const content = await getPayloadContent(options.payload)
const contentType = "text/plain"
const hashBase = `hawk.1.payload\n${contentType}\n${content}\n`
const contentHash = await sha256Hash(hashBase)
artifacts.hash = btoa(
String.fromCharCode.apply(null, [...new Uint8Array(contentHash)])
)
}
// Construct the string to sign according to Hawk spec
const macBaseString = `hawk.1.header\n${artifacts.ts}\n${artifacts.nonce}\n${artifacts.method}\n${artifacts.resource}\n${artifacts.host}\n${artifacts.port}\n${artifacts.hash}\n${artifacts.ext}\n`
// Calculate MAC
const mac = await hmacSign(options.key, macBaseString, options.algorithm)
// Construct the Hawk header
const header = [
`Hawk id="${options.id}"`,
`ts="${artifacts.ts}"`,
`nonce="${artifacts.nonce}"`,
`mac="${mac}"`,
]
// Add optional parameters if present
if (options.ext && options.ext !== "") header.push(`ext="${options.ext}"`)
if (options.app && options.app !== "") header.push(`app="${options.app}"`)
if (options.dlg && options.dlg !== "") header.push(`dlg="${options.dlg}"`)
if (artifacts.hash !== "") header.push(`hash="${artifacts.hash}"`)
return header.join(", ")
}

View file

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

View file

@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 6,
v: 7,
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: 6,
v: 7,
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: 6,
v: 7,
_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: 6,
v: 7,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 6,
v: 7,
_ref_id,
auth,
headers: addDescriptionField(headers),
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 6,
v: 7,
auth,
headers: addDescriptionField(headers),
}

View file

@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 6,
v: 7,
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: 6,
v: 7,
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: 6,
v: 7,
_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: 6,
v: 7,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 6,
v: 7,
_ref_id,
auth,
headers: addDescriptionField(headers),
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 6,
v: 7,
auth,
headers: addDescriptionField(headers),
}

View file

@ -542,6 +542,9 @@ importers:
'@tauri-apps/plugin-store':
specifier: 2.2.0
version: 2.2.0
'@types/hawk':
specifier: 9.0.6
version: 9.0.6
'@types/markdown-it':
specifier: 14.1.2
version: 14.1.2
@ -599,6 +602,9 @@ importers:
graphql-tag:
specifier: 2.12.6
version: 2.12.6(graphql@16.9.0)
hawk:
specifier: 9.0.2
version: 9.0.2
insomnia-importers:
specifier: 3.6.0
version: 3.6.0(openapi-types@12.1.3)
@ -4027,6 +4033,19 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
'@hapi/b64@5.0.0':
resolution: {integrity: sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==}
'@hapi/boom@9.1.4':
resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==}
'@hapi/cryptiles@5.1.0':
resolution: {integrity: sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==}
engines: {node: '>=12.0.0'}
'@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
'@hoppscotch/httpsnippet@3.0.7':
resolution: {integrity: sha512-A7uoYLmkdjekFadnooOjpplFOIr0yHtY/wG63uoTbvL0hoEYH22fDQlJeYIlZe1kdv4q+xjnHj9VgdZQzMbauA==}
engines: {node: '^14.19.1 || ^16.14.2 || ^18.0.0 '}
@ -5275,6 +5294,9 @@ packages:
'@types/body-parser@1.19.5':
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
'@types/caseless@0.12.5':
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
@ -5293,6 +5315,9 @@ packages:
'@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -5326,6 +5351,9 @@ packages:
'@types/har-format@1.2.16':
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
'@types/hawk@9.0.6':
resolution: {integrity: sha512-yowsKIXd1wlNsn7cc4o2uWDTGJYj236R0uvN34aWmB5R4SSOj3/V3nHYBm9DfkhFvteyYuy45jZQnNai31DtaQ==}
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
@ -5467,6 +5495,9 @@ packages:
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/request@2.48.12':
resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@ -5500,6 +5531,9 @@ packages:
'@types/supertest@6.0.2':
resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@ -8120,6 +8154,10 @@ packages:
typescript: '>3.6.0'
webpack: ^5.11.0
form-data@2.5.2:
resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==}
engines: {node: '>= 0.12'}
form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@ -8455,6 +8493,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hawk@9.0.2:
resolution: {integrity: sha512-EJMLBZAWg+EoI/aAJWDhrYGvucWYvY37CdGXolkol0ITGswkDHtDnjbFcPBaIv3jbtpfWqYjXMm4KhfXPOLCRg==}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@ -10845,7 +10886,6 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
deprecated: |-
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
qs@6.11.0:
@ -15965,6 +16005,20 @@ snapshots:
dependencies:
graphql: 16.9.0
'@hapi/b64@5.0.0':
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/boom@9.1.4':
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/cryptiles@5.1.0':
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek@9.3.0': {}
'@hoppscotch/httpsnippet@3.0.7':
dependencies:
ajv: 6.12.3
@ -17515,6 +17569,8 @@ snapshots:
'@types/connect': 3.4.38
'@types/node': 22.9.3
'@types/caseless@0.12.5': {}
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.9.3
@ -17535,6 +17591,8 @@ snapshots:
dependencies:
'@types/node': 22.9.3
'@types/crypto-js@4.2.2': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@ -17585,6 +17643,13 @@ snapshots:
'@types/har-format@1.2.16': {}
'@types/hawk@9.0.6':
dependencies:
'@hapi/boom': 9.1.4
'@types/crypto-js': 4.2.2
'@types/node': 22.7.6
'@types/request': 2.48.12
'@types/http-errors@2.0.4': {}
'@types/istanbul-lib-coverage@2.0.6': {}
@ -17745,6 +17810,13 @@ snapshots:
'@types/range-parser@1.2.7': {}
'@types/request@2.48.12':
dependencies:
'@types/caseless': 0.12.5
'@types/node': 22.7.6
'@types/tough-cookie': 4.0.5
form-data: 2.5.2
'@types/resolve@1.20.2': {}
'@types/sax@1.2.7':
@ -17787,6 +17859,8 @@ snapshots:
'@types/methods': 1.1.4
'@types/superagent': 8.1.7
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7': {}
'@types/uuid@10.0.0': {}
@ -21532,6 +21606,13 @@ snapshots:
typescript: 5.3.3
webpack: 5.94.0(@swc/core@1.4.2)
form-data@2.5.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
safe-buffer: 5.2.1
form-data@4.0.0:
dependencies:
asynckit: 0.4.0
@ -21969,6 +22050,13 @@ snapshots:
dependencies:
function-bind: 1.1.2
hawk@9.0.2:
dependencies:
'@hapi/b64': 5.0.0
'@hapi/boom': 9.1.4
'@hapi/cryptiles': 5.1.0
'@hapi/hoek': 9.3.0
he@1.2.0: {}
header-case@2.0.4: