mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 12:26:19 +00:00
Add validateGQLRequest
method in PaymentsManager (#426)
* Refactor GQL validation for payment * Export payment util const PAYMENT_HEADER_KEY * Check payment header only for paid queries
This commit is contained in:
parent
caa8da7090
commit
8515630f49
@ -24,7 +24,7 @@ const log = debug('laconic:payments');
|
||||
const IntrospectionQuery = 'IntrospectionQuery';
|
||||
const IntrospectionQuerySelection = '__schema';
|
||||
|
||||
const PAYMENT_HEADER_KEY = 'x-payment';
|
||||
export const PAYMENT_HEADER_KEY = 'x-payment';
|
||||
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
|
||||
|
||||
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
|
||||
@ -355,78 +355,108 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
|
||||
const querySelections = requestContext.operation?.selectionSet.selections
|
||||
.map((selection: any) => (selection as FieldNode).name.value);
|
||||
|
||||
// Continue if it's an introspection query for schema
|
||||
// (made by ApolloServer playground / default landing page)
|
||||
if (
|
||||
requestContext.operationName === IntrospectionQuery &&
|
||||
querySelections && querySelections.length === 1 &&
|
||||
querySelections[0] === IntrospectionQuerySelection
|
||||
) {
|
||||
try {
|
||||
await validateGQLRequest(
|
||||
paymentsManager,
|
||||
{
|
||||
querySelections,
|
||||
operationName: requestContext.operationName,
|
||||
paymentHeader: requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY)
|
||||
}
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const paymentHeader = requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY);
|
||||
if (paymentHeader == null) {
|
||||
return {
|
||||
errors: [{ message: ERR_HEADER_MISSING }],
|
||||
http: new HTTPResponse(undefined, {
|
||||
headers: requestContext.response?.http?.headers,
|
||||
status: HTTP_CODE_BAD_REQUEST
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
let vhash: string, vsig: string;
|
||||
const match = paymentHeader.match(PAYMENT_HEADER_REGEX);
|
||||
|
||||
if (match) {
|
||||
[, vhash, vsig] = match;
|
||||
} else {
|
||||
return {
|
||||
errors: [{ message: ERR_INVALID_PAYMENT_HEADER }],
|
||||
http: new HTTPResponse(undefined, {
|
||||
headers: requestContext.response?.http?.headers,
|
||||
status: HTTP_CODE_BAD_REQUEST
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const signerAddress = nitroUtils.getSignerAddress(vhash, vsig);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for await (const querySelection of querySelections ?? []) {
|
||||
if (paymentsManager.freeQueriesList.includes(querySelection)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Serve a query for free if rate is not configured
|
||||
const configuredQueryCost = paymentsManager.queryRates[querySelection];
|
||||
if (configuredQueryCost === undefined) {
|
||||
log(`Query rate not configured for "${querySelection}", serving a free query to ${signerAddress}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, signerAddress, querySelection);
|
||||
if (!allowRequest) {
|
||||
const failResponse: GraphQLResponse = {
|
||||
errors: [{ message: rejectionMessage }],
|
||||
} catch (error) {
|
||||
if (error instanceof GQLPaymentError) {
|
||||
return {
|
||||
errors: [{ message: error.message }],
|
||||
http: new HTTPResponse(undefined, {
|
||||
headers: requestContext.response?.http?.headers,
|
||||
status: HTTP_CODE_PAYMENT_REQUIRED
|
||||
status: error.status
|
||||
})
|
||||
};
|
||||
|
||||
return failResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
class GQLPaymentError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor (message: string, status: number) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export const validateGQLRequest = async (
|
||||
paymentsManager: PaymentsManager,
|
||||
{ operationName, querySelections, paymentHeader }: {
|
||||
operationName?: string | null;
|
||||
querySelections?: string[];
|
||||
paymentHeader?: string | null;
|
||||
}
|
||||
): Promise<boolean> => {
|
||||
// Return true if it's an introspection query for schema
|
||||
// (made by ApolloServer playground / default landing page)
|
||||
if (
|
||||
operationName === IntrospectionQuery &&
|
||||
querySelections && querySelections.length === 1 &&
|
||||
querySelections[0] === IntrospectionQuerySelection
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const paidQuerySelections = (querySelections ?? []).filter(querySelection => {
|
||||
if (paymentsManager.freeQueriesList.includes(querySelection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serve a query for free if rate is not configured
|
||||
const configuredQueryCost = paymentsManager.queryRates[querySelection];
|
||||
if (configuredQueryCost === undefined) {
|
||||
log(`Query rate not configured for "${querySelection}", serving free query`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Return true if no paid queries exist
|
||||
if (!paidQuerySelections.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!paymentHeader) {
|
||||
throw new GQLPaymentError(ERR_HEADER_MISSING, HTTP_CODE_BAD_REQUEST);
|
||||
}
|
||||
|
||||
let vhash: string, vsig: string;
|
||||
const match = paymentHeader.match(PAYMENT_HEADER_REGEX);
|
||||
|
||||
if (match) {
|
||||
[, vhash, vsig] = match;
|
||||
} else {
|
||||
throw new GQLPaymentError(ERR_INVALID_PAYMENT_HEADER, HTTP_CODE_BAD_REQUEST);
|
||||
}
|
||||
|
||||
const signerAddress = nitroUtils.getSignerAddress(vhash, vsig);
|
||||
|
||||
for await (const querySelection of paidQuerySelections) {
|
||||
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, signerAddress, querySelection);
|
||||
if (!allowRequest) {
|
||||
throw new GQLPaymentError(rejectionMessage, HTTP_CODE_PAYMENT_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper method to modify a given JsonRpcProvider to make payment for required methods
|
||||
// and attach the voucher details in reqeust URL
|
||||
export const setupProviderWithPayments = (
|
||||
|
Loading…
Reference in New Issue
Block a user