@graphql-mesh/hmac-upstream-signature
Version:
179 lines (175 loc) • 5.77 kB
JavaScript
import { serializeExecutionRequest } from '@graphql-tools/executor-common';
import { handleMaybePromise } from '@whatwg-node/promise-helpers';
import jsonStableStringify from 'json-stable-stringify';
const loggerByRequest = /* @__PURE__ */ new WeakMap();
function loggerForRequest(log, request) {
const reqLog = loggerByRequest.get(request);
if (reqLog) {
return reqLog;
}
loggerByRequest.set(request, log);
return log;
}
const DEFAULT_EXTENSION_NAME = "hmac-signature";
const DEFAULT_SHOULD_SIGN_FN = () => true;
const defaultExecutionRequestSerializer = (executionRequest) => jsonStableStringify(
serializeExecutionRequest({
executionRequest: {
document: executionRequest.document,
variables: executionRequest.variables
}
})
);
const defaultParamsSerializer = (params) => jsonStableStringify({
query: params.query,
variables: params.variables != null && Object.keys(params.variables).length > 0 ? params.variables : void 0
});
function createCryptoKey({
textEncoder,
crypto,
secret,
usages
}) {
return crypto.subtle.importKey(
"raw",
textEncoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
usages
);
}
function useHmacUpstreamSignature(options) {
if (!options.secret) {
throw new Error(
'Property "secret" is required for useHmacUpstreamSignature plugin'
);
}
const shouldSign = options.shouldSign || DEFAULT_SHOULD_SIGN_FN;
const extensionName = options.extensionName || DEFAULT_EXTENSION_NAME;
const serializeExecutionRequest2 = options.serializeExecutionRequest || defaultExecutionRequestSerializer;
let key$;
let fetchAPI;
let textEncoder;
return {
onYogaInit({ yoga }) {
fetchAPI = yoga.fetchAPI;
},
onSubgraphExecute({
subgraphName,
subgraph,
executionRequest,
log: rootLog
}) {
const log = rootLog.child("[useHmacUpstreamSignature] ");
log.debug(`Running shouldSign for subgraph ${subgraphName}`);
if (shouldSign({ subgraphName, subgraph, executionRequest })) {
log.debug(
`shouldSign is true for subgraph ${subgraphName}, signing request`
);
textEncoder ||= new fetchAPI.TextEncoder();
return handleMaybePromise(
() => key$ ||= createCryptoKey({
textEncoder,
crypto: fetchAPI.crypto,
secret: options.secret,
usages: ["sign"]
}),
(key) => {
key$ = key;
const serializedExecutionRequest = serializeExecutionRequest2(executionRequest);
const encodedContent = textEncoder.encode(
serializedExecutionRequest
);
return handleMaybePromise(
() => fetchAPI.crypto.subtle.sign("HMAC", key, encodedContent),
(signature) => {
const extensionValue = fetchAPI.btoa(
String.fromCharCode(...new Uint8Array(signature))
);
log.debug(
{
signature: extensionValue,
payload: serializedExecutionRequest
},
`Produced hmac signature for subgraph ${subgraphName}`
);
if (!executionRequest.extensions) {
executionRequest.extensions = {};
}
executionRequest.extensions[extensionName] = extensionValue;
}
);
}
);
} else {
log.debug(
`shouldSign is false for subgraph ${subgraphName}, skipping hmac signature`
);
}
}
};
}
function useHmacSignatureValidation(options) {
if (!options.secret) {
throw new Error(
'Property "secret" is required for useHmacSignatureValidation plugin'
);
}
const extensionName = options.extensionName || DEFAULT_EXTENSION_NAME;
let key$;
let textEncoder;
const paramsSerializer = options.serializeParams || defaultParamsSerializer;
return {
onParams({ params, fetchAPI, request }) {
const log = loggerForRequest(options.log, request).child(
"[useHmacSignatureValidation] "
);
textEncoder ||= new fetchAPI.TextEncoder();
const extension = params.extensions?.[extensionName];
if (!extension) {
throw new Error(
`Missing HMAC signature: extension ${extensionName} not found in request.`
);
}
return handleMaybePromise(
() => key$ ||= createCryptoKey({
textEncoder,
crypto: fetchAPI.crypto,
secret: options.secret,
usages: ["verify"]
}),
(key) => {
key$ = key;
const sigBuf = Uint8Array.from(
atob(extension),
(c) => c.charCodeAt(0)
);
const serializedParams = paramsSerializer(params);
log.debug(
{ serializedParams },
"HMAC signature will be calculate based on serialized params"
);
return handleMaybePromise(
() => fetchAPI.crypto.subtle.verify(
"HMAC",
key,
sigBuf,
textEncoder.encode(serializedParams)
),
(result) => {
if (!result) {
log.error(
"HMAC signature does not match the body content. short circuit request."
);
throw new Error(
`Invalid HMAC signature: extension ${extensionName} does not match the body content.`
);
}
}
);
}
);
}
};
}
export { defaultExecutionRequestSerializer, defaultParamsSerializer, useHmacSignatureValidation, useHmacUpstreamSignature };