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 IntrospectionQuery = 'IntrospectionQuery';
|
||||||
const IntrospectionQuerySelection = '__schema';
|
const IntrospectionQuerySelection = '__schema';
|
||||||
|
|
||||||
const PAYMENT_HEADER_KEY = 'x-payment';
|
export const PAYMENT_HEADER_KEY = 'x-payment';
|
||||||
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
|
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
|
||||||
|
|
||||||
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
|
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
|
||||||
@ -355,78 +355,108 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
|
|||||||
const querySelections = requestContext.operation?.selectionSet.selections
|
const querySelections = requestContext.operation?.selectionSet.selections
|
||||||
.map((selection: any) => (selection as FieldNode).name.value);
|
.map((selection: any) => (selection as FieldNode).name.value);
|
||||||
|
|
||||||
// Continue if it's an introspection query for schema
|
try {
|
||||||
// (made by ApolloServer playground / default landing page)
|
await validateGQLRequest(
|
||||||
if (
|
paymentsManager,
|
||||||
requestContext.operationName === IntrospectionQuery &&
|
{
|
||||||
querySelections && querySelections.length === 1 &&
|
querySelections,
|
||||||
querySelections[0] === IntrospectionQuerySelection
|
operationName: requestContext.operationName,
|
||||||
) {
|
paymentHeader: requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
} catch (error) {
|
||||||
|
if (error instanceof GQLPaymentError) {
|
||||||
const paymentHeader = requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY);
|
return {
|
||||||
if (paymentHeader == null) {
|
errors: [{ message: error.message }],
|
||||||
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 }],
|
|
||||||
http: new HTTPResponse(undefined, {
|
http: new HTTPResponse(undefined, {
|
||||||
headers: requestContext.response?.http?.headers,
|
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
|
// Helper method to modify a given JsonRpcProvider to make payment for required methods
|
||||||
// and attach the voucher details in reqeust URL
|
// and attach the voucher details in reqeust URL
|
||||||
export const setupProviderWithPayments = (
|
export const setupProviderWithPayments = (
|
||||||
|
Loading…
Reference in New Issue
Block a user