I'm trying to use Google cloudtasks from Cloudflare workers.
This is a JS environment limited to web-workers standards minus some things that Cloudflare didn't implement.
Bottom line - I can't use Google's provided SDKs in that environment.
I'm trying to call the API using simple fetch, but always fail on the authentication part.
The discovery document says that
"parameters": {
...
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query",
"type": "string"
}
}
So I tried calling the api with ?key=MY_API_KEY query param
Didn't work.
I also tried generating a token using Service Account downloaded json file with this library
Didn't work.
I tried following this guide to generate oauth access token which was what the error message told me that I need. But
running the command gcloud auth application-default print-access-token returned the error:
WARNING: Compute Engine Metadata server unavailable onattempt 1 of 3. Reason: timed out
WARNING: Compute Engine Metadata server unavailable onattempt 2 of 3. Reason: timed out
WARNING: Compute Engine Metadata server unavailable onattempt 3 of 3. Reason: [Errno 64] Host is down
WARNING: Authentication failed using Compute Engine authentication due to unavailable metadata server.
ERROR: (gcloud.auth.application-default.print-access-token) Could not automatically determine credentials. Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. For more information, please see https://cloud.google.com/docs/authentication/getting-started
The env variable above is set correctly to the service-account json file.
Even if it worked, I didn't understand how am I supposed to use it from my code, while it uses the cli tool gcloud
So my question is this - how can I access Google's cloud APIs from Cloudflare workers (web-workers javascript env.), specifically I'm interested in Cloudtasks, without using any CLI tool or Google SDK.
More specifically - how can I generate the required oauth2 access token?
Based on #john-hanley blog post I was able to make the following code work:
const fetch = require('node-fetch'); //in cloudflare workers env. this is not needed. 'fetch' is globally available
const jwt = require('jsonwebtoken');
const q = require('querystring');
async function main() {
const project = 'xxxx';
const location = 'us-central1';
const scopes = "https://www.googleapis.com/auth/cloud-platform"
const queue = 'queueName';
const parent = `projects/${project}/locations/${location}/queues/${queue}`
const url = `https://cloudtasks.googleapis.com/v2/${parent}/tasks`
const sjwt = await createSignedJwt(json.private_key, json.private_key_id, json.client_email, scopes);
const {token} = await exchangeJwtForAccessToken(sjwt)
const headers = { Authorization: `Bearer ${token}` }
const body = Buffer.from(JSON.stringify({"c":"b"})).toString('base64'); //Note this is not a string!
const task = { //in my case the task is HTTP request that google will send to my service outside GCP. You can create an appEngine task instead
httpRequest: {
"url": "https://where-google-should-send-the-task.com",
"httpMethod": "POST",
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "only-if-you-need-it"
},
"body": body
}
};
const request = {
parent: parent,
task: task
};
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(request),
headers
})
const res = await response.json()
console.log(res)
}
async function createSignedJwt (pkey, pkey_id, email, scope) {
const authUrl = "https://oauth2.googleapis.com/token"
const options = {
algorithm: "RS256",
keyid: pkey_id,
expiresIn: 3600,
audience: authUrl,
issuer: email
// header: { //this is not needed because it's jsonwebtoken's default behavior to add the correct typ when the payload is a json
// "typ": "JWT"
// }
}
const payload = {
"scope": scope
}
return jwt.sign(payload, pkey, options)
}
/**
* This function takes a Signed JWT and exchanges it for a Google OAuth Access Token
*/
async function exchangeJwtForAccessToken(signedJwt) {
const authUrl = "https://oauth2.googleapis.com/token"
const params = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signedJwt
}
const body = q.stringify(params);
const res = await fetch(authUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
})
if (!res.ok) {
return {
error: "Could not fetch access token. " + await res.text()
}
}
const resJson = await res.json();
return {
token: resJson.access_token
}
}
// for convenience, I'm placing the JSON here. For production it should be stored in secret-manager or injected via environment variable
const json = {
"type": "service_account",
"project_id": "xxxx",
"private_key_id": "xxxx",
"private_key": "-----BEGIN PRIVATE KEY-----xxxx-----END PRIVATE KEY-----\n",
"client_email": "xxxx#xxxx.iam.gserviceaccount.com",
"client_id": "xxxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxxx%40xxxx.iam.gserviceaccount.com"
}
main()
Related
Some Background:
I am using Amazon Alexa smart home skill for controlling some IoT devices with AWS Lambda function written in NodeJS as the default endpoint. I have already linked the Alexa Skill with lambda function, created a Login With Amazon security profile, and enabled account linking for the alexa skill. For now, I have turned off the "Send Alexa Events" permission because that adds its own complexity in terms of responding the events. Currently I am in Beta Testing stage of my skill.
My use case: I have 3 different customers who have different name, different email IDs and different amazon accounts. All 3 customers have downloaded my Alexa skill in their alexa app and linked it to their amazon account. All those customers have 2 devices (light and fan) at their home, which are connected to my cloud. I have stored their device information against their email IDs on my cloud in a database as shown below:
bob#gmail.com, Light -> ON, Fan -> OFF
jhonny#gmail.com, Light -> OFF, Fan-> OFF
alice#gmail.com, Light -> OFF, Fan -> OFF
Now let's say one of those customer, Jhonny, wants to turn on the light. So when Jhonny asks Alexa for turning on the light, my lambda function will receive below directive:
{
"directive": {
"header": {
"namespace": "Alexa.PowerController",
"name": "TurnOn",
"payloadVersion": "3",
"messageId": "003edc9f-68ad-4618-a982-f73fc37d25be",
"correlationToken": "AAAAAAAAAQDaOl/vVaX+GlN2m9SvqFri/AEAAAAAAACdKWHpFqLjNmk4Y6DeNDRoGjdMODTZW6kkXKZoe3Ya289F45koL55JJmcv05BVBFceJH9FXOb/YJcwQH+xk4yx1KMa92zoBTl7jNayw8r4Pzfvd8oO486Fx3q3g35xAeNfNPbHalpV7ftYw86qXsurSfRkk2vgqWQu9CsYH013/fqY3ojnQySOIu31BAaiGWI4Pur4x/2zg3HOBKQkzguIMVmWxZP7de+VCoD5cqEQYOoE7ACQi6NAGPJHbS5cLY/2FBO163wLeDGoJZ8sfEcroQYpqGCkQ+KTLolZ2SDle2VvubB7Ntc0Rzpfg45dGFj6T2Mb8mP/PgxH+mfgTzjTUgTq9N6wfA/zAvXWnpCkC6/3nFUS3NzsYqaa+ff/Zm7smI645BJU6BJu19f6oRi4sjK+mqQLJDax9orIjrZ2Yd6ASq2Z31lZthgDFTyqe5b+JTP1Sp4j6S9uayIxyGj59eYxB1YMCxrm3clMJRKBphwiNrGewcGWZ2Qb2saB3Ctmy+fxPmasqFfbxnf4LYBib0VQrOpmvbRR8u3CT14ltCTubEA/iw2krMfqlM6xvzukFyRj++8oOfXNMGefGfe11GlcbDWXVDoJURbOjbaGGqks6jdZ0TOD/TxepHHnlBfWgHA9pfibikqBsB8NXvp"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "Atza|IwEBIAdKRpKJ5emckzWUGPRR-O9_Pg2mWV0BDxgfUCJbcqNNWSb8zfl4ueaG8eu-1YZOyA3qTyJVnn9X9JtYOfzcJClEROo1bDoGMs_VEeA-7aTZK5RKMWIHIbz8BIdmt6Ncr6bF8WkZnhNjS4q-qim4ICRfatrIaD2C0KXykXNJnYco11aSR7tGkhcwKm27jjPoewap2k07BqMhmaaB2ie_-v_2ojbDWmKW95MuCeYMoZmYTmNh4o4A5YH_UlFO9atUTjr9oA4ROwL_3R02Yi_VYRf8iZJCF4FmxiXRGGMqwEMF1KNeV6zcUFAjBIvSORAOpSO6iRySn9lZeAazywrdCIYBc8LFnDtGQIeYdKXSW39qYFbfC-Hy3RCJwuVPQPzS9jX99pyYZ0q1ZuRRUg"
},
"endpointId": "Light",
"cookie": {}
},
"payload": {}
}
}
So in order to turn on the light at Jhonny's home, I must somehow use the BearerToken from Alexa directive to retrieve the email address or user ID of Jhonny from the Amazon OAuth provider service. Is that correct? Otherwise, I wouldn't know which entry to change in my database.
Now My Question is: How do I use this BearerToken to get the user ID and other information? I need some example code in NodeJS which I can implement in my Lambda Function for this purpose.
We can get the user profile by sending a HTTP GET request to the host using the bearer token. Below link gives a beautiful and very simple way to verify user profile by providing your bearer token, and provide the basic understanding.
https://reqbin.com/req/5k564bhv/get-request-bearer-token-authorization-header-example
In case you are using Login With Amazon as your OAuth2.0 identity provider, then just mention following items on above link:
Host URL: https://api.amazon.com/user/profile
Select Authorization
Select Bearer Token
Copy the bearer token received from alexa directive and paste it in the box (it generally starts with Atza|xxxyyyzzz)
Click on Send
If you have entered correct host URL, path and bearer token, and if your bearer token has not already expired, then you should see 200 response code with user profile details such as user_id, name, email etc.
In case your bearer token has expired, you can simply ask Alexa to operate one of the devices, and obtain a new bearer token for verifying from the AWS CloudWatch logs.
Bearer Token usually expires in 3600 seconds.
Now to answer my original question, here is a simple code snippet of AWS Lambda using NodeJS which worked for me:
const https = require('https');
const handlerInput = {
"directive": {
"header": {
"namespace": "Alexa.PowerController",
"name": "TurnOn",
"payloadVersion": "3",
"messageId": "f8107929-cc45-4bfe-b7ae-eb9b61c09d5c",
"correlationToken": "AAAAAAAAAQDaOl/vVaX+GlN2m9SvqFri/AEAAAAAAADsiP98Ew8DnuYv7JWgKtb4W/WFyycSA33aKocZD0AOnl5+PmKQuqgx2p/CT5Efln0lbORsb9jSB/zB9s3RLgO2dCG3B5/b6jzVzch98o3ULz61HgoAPz/ZsZIFWXyfqUhLrdwyggsnInsCOeUOHpRZ15VLJ6oEyW7zNE6MCfcH+SfPB2BMr7ex1wP3Ghz7fPNIwTeMhV/ZstF/mF7K74gH1psCLgGLzdc4YYtRtz7KxHCS3I8eUd2UQZ4NyCf9gVktphgTzXu6ezBp5qoexsmgkHQ0duK0zF2HoKvY8jhVD8MH2unJQLbcn5VQyMololRkQ3E1orZXy86t5Ls1ILC6qbtT1tKbeTqVChTBiCh5jczOuSW5mES+1fmhbBxx1HbE8OXhfSNcXcE2VAY49LTBafnUQ7NftkDTRRFJKgL+IIdBq283SgZNFWmJxwGBN5OXC5vKQGD1QzHUlhb91I9xQ1bmx9jKPP8tBj0ydkY/nD++34zQfxDObbX7cmVs4/4nTKNhm2o2RGZmVyMr6PzhvRzq777Vi+keJM+qXQxSRO0NzdSFWdBDBHCL46nAvwlOjNIvHsOoaODEVNK5HzT6a+H4PsbDazZgfXB4QFjIb9FvFZ6wmlGM1iOU/sN40ro3pQ12IE+Dn+tweemgRaRucn/mT0/E5IRgnliR"
},
"endpoint": {
"scope": {
"type": "BearerToken",
"token": "Atza|IwEBINNVA3FmduJsc_iA2Jpo2hBfl2XKPCzujkVWgix2TychTLTuAbnchakWuNiNGnpimsAx2dwoJCoUbarJXRv5a1ECrYoVi-SnAt6xxVvxi4CX4zd13rCq1wddl90Lp71bJDq9qfvlS-d9KillCwRYh4BBY0GegU16hM4RC07nBMvra07Dfe_kq9PIzu4yOeSaLwobiEAhNex3Qyo4n0jW6iyB5YzKUe2UAu0pQZmkeNjRmhPVpy0L4Pv539O87yXBT7iQ8pkhZBKeZWcuFltCN2_v1SfhMNQbnLlt_CMFry-eQg0t9oSaweCJOvrAYVKmg0cd-gNd27ttqZkmD0rMGERmTNkij_rTQJ9iiQKjEW1Xx_7TWVniqda1Hp_o1wA5p4aT6BYQKkoFrRm4pSa3iUA"
},
"endpointId": "Light",
"cookie": {}
},
"payload": {}
}
};
exports.handler = async (event) => {
// TODO implement
const accessToken = handlerInput.directive.endpoint.scope.token;
console.log(accessToken);
const info = await getUserInfo(accessToken);
console.log(`info: ${JSON.stringify(info)}`);
const response = {
user_ID: info.user_id,
name: info.name,
email: info.email,
};
return response;
};
async function getUserInfo(accessToken) {
return new Promise((resolve, reject) => {
const options = {
"method": "GET",
"hostname": "api.amazon.com",
"path": "/user/profile",
"headers": {
"Authorization": `Bearer ${accessToken}`
}
};
let req = https.request(options, (response) => {
let returnData = '';
response.on('data', (chunk) => {
returnData += chunk;
});
response.on('end', () => {
resolve(JSON.parse(returnData));
});
response.on("error", (error) => {
reject(error);
});
});
req.end();
});
}
Make sure to replace the handlerInput in above code with the most recent copy. You can acquire it from AWS CloudWatch under Logs -> Log Groups.
Below YouTube video also has very useful information about overall process of creating the skill, account linking and identifying user through access token:
https://www.youtube.com/watch?v=NrBBM9XhzG0
Unfortunately, the video doesn't explain how to do it for smart home skill, though it's very similar.
I have this code:
import {v2beta3} from "#google-cloud/tasks";
const project = 'xxxxxxx'
const location = 'yyyyyyy'
const queue = 'zzzzzzzzz'
const client = new v2beta3.CloudTasksClient()
const parent = client.queuePath(project, location, queue)
const payload = {eventId: "fred"}
const convertedPayload = JSON.stringify(payload)
const body = Buffer.from(convertedPayload).toString('base64');
const task = {
httpRequest: {
httpMethod: "POST",
url: "https://webhook.site/9sssssssssss",
oidcToken: {
serviceAccountEmail: "aaaaaaaaaa#appspot.gserviceaccount.com",
},
headers: {
'Content-Type': 'application/json',
},
body,
},
};
(async function() {
try {
const [response] = await client.createTask({parent, task});
console.log(`Created task ${response.name}`);
} catch (error) {
console.log(error)
}
}());
When I run it from my laptop, it just works, which seems unauthenticated to me. Anyone can now enqueue a task on my queue.
What is the right way to authenticate to the GCP Cloud Tasks enqueue API?
As John Hanley pointed out in the comments, my local app was using Application Default Credentials to authenticate itself. When I switched to a different gcloud account by doing this:
gcloud auth application-default login
I get this error message when I try to run the code:
Error: 7 PERMISSION_DENIED: The principal (user or service account) lacks IAM permission "cloudtasks.tasks.create" for the resource "projects/yyyyyyy/locations/europe-west1/queues/default-xxxxxx" (or the resource may not exist).
I am trying to create projects programatically through the resource manager API from a google cloud function like so:
exports.createProjectAsync = async (projectId, projectName) => {
const scopes = "https://www.googleapis.com/auth/cloud-platform"
const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?scopes=${scopes}`
const tokenResult = await fetch(url, {
headers: {
"Metadata-Flavor": "Google"
},
});
const tokenStatus = tokenResult.status;
functions.logger.log(`Call to get token has status ${tokenStatus}`);
const tokenData = await tokenResult.json()
functions.logger.log(`Call to get token has data ${JSON.stringify(tokenData)}`);
const accessToken = tokenData.access_token;
if (accessToken === null) {
throw new Error(`Failed to retrieve access token`);
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
};
const payload = {
"projectId": projectId,
"name": projectName,
"parent": {
"type": "folder",
"id": FOLDER_NUMBER
}
};
const projectsCreateUrl = `https://cloudresourcemanager.googleapis.com/v1/projects/`
const result = await fetch(projectsCreateUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
const status = result.status;
functions.logger.log(`Call to create project returned status ${status}`);
const data = await result.json()
functions.logger.log(`data: ${JSON.stringify(data)}`);
return data;
}
For testing purposes I've added the Organization Administrator role to the default service account. I cannot see the projects creator role in IAM:
When calling the API I get the following error:
{"error":{"code":403,"message":"The caller does not have permission","status":"PERMISSION_DENIED"}}
How can I successfully access this resource?
Although of course, it gives you the ability to modify its own permissions, as you can verify in the GCP documentation, the Organization Admin role does not allow to create a new project.
As you indicated, for that purpose the service account should be granted the Project Creator (roles/resourcemanager.projectCreator) role.
According to your screenshot, you are trying to grant this permission at the project level, but please, be aware that this role can only be granted at the organization and folder levels. This is the reason why the dropdown menu in the Google Cloud Web console is not providing you the Project Creator option.
If you have the necessary permissions over the folder or organization, try to assign that role at the corresponding level.
I got some troubles configuring an Hasura auth hook using a Lambda. I need such a function as I am storing my JWT token in an HTTP-only cookie, for security reasons.
I'm using a serverless function which returns a correct response (either when testing a curl request directly, or even when logging lambda):
{
"statusCode":200,
"body":"{\"X-Hasura-User-Id\":\"74d3bfa9-0983-4f09-be02-6a36888b382e\",\"X-Hasura-Role\":\"user\"}"
}
Yet, Hasura hook doesn't seem to recognize the response:
{
"type": "webhook-log",
"timestamp": "2020-02-07T10:27:34.844+0000",
"level": "info",
"detail": {
"response": null,
"url": "http://serverless:3000/auth",
"method": "GET",
"http_error": null,
"status_code": 200
}
}
These two lines of logs are adjacent in my logs. I just reformatted them a little bit to ease reading.
My lambda code looks like:
export const handler = async (event) => {
const cookies = getCookiesFromHeader(event.headers);
const { access_token: accessToken } = cookies;
let decodedToken = null;
try {
const cert = fs.readFileSync("./src/pem/dev.pem");
decodedToken = jwt.verify(accessToken, cert);
} catch (err) {
console.error(err);
return {
statusCode: 401,
};
}
const hasuraClaims = decodedToken['https://hasura.io/jwt/claims'];
return {
statusCode: 200,
body: JSON.stringify({
"X-Hasura-User-Id": hasuraClaims['x-hasura-user-id'],
"X-Hasura-Role": hasuraClaims['x-hasura-default-role']
})
}
}
Any idea on what is going on? Note that I'm using serverless offline, in case of. :)
In AWS Lambda, the spec requires the response body to be stringified and the actual response will be a parsed JSON object which is what Hasura will receive from the auth webhook.
When you are using serverless-offline, the response body is returned as a String (since JSON.stringify is used) without getting parsed. A simple curl will give you the difference.
The above code will work on Lambda but not on local development using serverless-offline. You will have to use the event object to see if isOffline is true and return JSON directly and if not return the stringified version.
Example code:
if(event.isOffline) {
// make it work with serverless-offline
return { "x-hasura-role": "user" ....};
} else {
// make it work with lambda
return { statusCode: 200, body: JSON.stringify({"x-hasura-role": "user"}) };
}
Official example in the serverless-offline repo along with error handling.
Related issues:
https://github.com/dherault/serverless-offline/issues/530
https://github.com/dherault/serverless-offline/issues/488
I am referring to Amazon documentation for the purpose of Customer Authentication. Currently, I am using LWA.
Steps I followed:
I enabled the Send Alexa Events Permission from the Alexa developer Console in Build > Permission page.
I took the grant code from the request in the cloudwatch logs which was sent when I logged in using Alexa companion app.
Example:-
{
"directive": {
"header": {
"messageId": "Example",
"name": "AcceptGrant",
"namespace": "Alexa.Authorization",
"payloadVersion": "3"
},
"payload": {
"grant": {
"code": "Example2",
"type": "OAuth2.AuthorizationCode"
},
"grantee": {
"token": "Example3",
"type": "BearerToken"
}
}
}
}
Permission Page under build on Alexa Developer console gave me client-Id and client-secret Which I used for making the post request to https://api.amazon.com/auth/o2/token.
Example:-
POST /auth/o2/token HTTP/l.l
Host: api.amazon.com
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
grant_type=authorization_code&code=&client_id=&client_secret=
I passed the code,client_id, and client_secret in the above example and made the post request to this URL https://api.amazon.com/auth/o2/token
I tried using x-www-form-urlencoded;charset=UTF-8 and also JSON for the Content-Type.
I followed the step given in the above documentation and I am stuck on the error ( 401 Unauthorized ):
{
"error_description": "The request has an invalid grant parameter : code",
"error": "invalid_grant"
}
I tried implementing it using Python code and Postman both. Ending up with the Same above error scenario.
Here is a sample code to help you and others who are looking to send events to alexa gateway.
const AWS = require('aws-sdk');
AWS.config.update({region: 'eu-west-1'});
// Create the DynamoDB service object
const ddb = new AWS.DynamoDB({ apiVersion: 'latest' });
const doc = new AWS.DynamoDB.DocumentClient({
convertEmptyValues: true,
service: ddb
});
// Using 'request' for http POST and GET request.
// https://www.npmjs.com/package/requests
// npm install --save requests
const r = require('request');
//Handle Authorization. Call this method from your lambda handler whenever you get Alexa.Authorization message. You will get this message only when you select permission to
//send events in your Smart Home Skill.
//Access to Event gateway allows you to enable Proactive Device Discovery and
//Proactive State Reporting in your skill
//More information on Alexa.Authorization can be found on https://developer.amazon.com/docs/device-apis/alexa-authorization.html
function handleAuthorization(request, context, user) {
//Even when you are using your own authentication, the url below will still
//point to amazon OAuth token url. The token you obtain here has to be stored
//separately for this user. Whenever sending an event to alexa event gateway you will
//require this token.
//URL below is for EU server. Look at following documentation link to identify correct url
//for your system.
//https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html
var url = "https://api.amazon.com/auth/o2/token";
var body = {
grant_type : 'authorization_code',
code : request.directive.payload.grant.code,
client_id : 'your client id from permissions page on developer portal where you enable alexa events. This is id different than one you specify in account linking settings',
client_secret : 'client secret from permissions page'
}
//https://developer.amazon.com/docs/smarthome/authenticate-a-customer-permissions.html
r.post({
url: url,
form : body
}, function(error, response, b){
if (error) { return console.log(error); }
var body = JSON.parse(b);
var params = {
TableName: 'Devices',
Item: {
'id' : user,
'auth_token' : body.access_token,
'refresh_token' : body.refresh_token
}
}
log("DEBUG:", "Authorization Body", JSON.stringify(body));
log("DEBUG:", "Authorization Response", JSON.stringify(response));
log("DEBUG:", "Database Params", JSON.stringify(params));
// Call DynamoDB to add the item to the table
var putObjectPromise = doc.put(params).promise();
//Store auth_token and refresh_token in database. We will need these
//while sending events to event gateway.
//Send a success response.
putObjectPromise.then(function(data) {
var response = {
event: {
header: {
messageId: request.directive.header.messageId,
namespace: "Alexa.Authorization",
name: "AcceptGrant.Response",
payloadVersion: "3"
},
"payload": {
}
}
};
context.succeed(response);
}).catch(function(err) {
//TODO - Add a Authorization error response JSON here.
console.log(err);
});
});
}