// App Server
import { ConnectClient } from "@narval-xyz/armory-sdk";
interface ConnectResponse {
interact: {
redirect: string; // The redirect url to start the interaction.
finish: string; // The finish nonce
};
continue: {
access_token: {
value: string; // The continue token value
};
uri: string; // The uri to continue the interaction.
};
}
const apiUserPrivateKey = process.env.NARVAL_API_USER_PRIVATE_KEY;
const clientId = process.env.NARVAL_CLIENT_ID;
async function initiateNarvalConnection() {
const connectClient = await ConnectClient.init({
host: "<https://auth.armory.narval.xyz>",
clientId: "xxx",
signer: apiUserPrivateKey,
});
const connectRequest = {
label: `DEV - ${userId} connect token`,
userId: userId, // Optional, reference for your app's internal userIds
actions: ["connection:read", "connection:delete", "connection:stake"],
providers: ["bitgo"],
finish: {
uri: `my-local-app/${crypto.randomUUID()}`, // Must be unique; can be any value; only needs to be a real uri if doing a redirect callback
nonce: crypto.randomUUID(), // Random nonce, you'll double-check this at the end as a security precaution
},
};
const data: ConnectResponse = await connectClient.initiateConnect(
connectRequest
);
// Persist the request & response to use at the end of the flow.
await db.connectRequest.create({
data: {
finishUri: connectRequest.finish.uri,
nonce: connectRequest.finish.nonce,
grantResponse: data,
},
});
return {
connectUrl: data.interact.redirect,
};
}
3. Open Widget
Implement this in your Client-side application.
// index.html
<script src="<https://console.narval.xyz/code/connect-widget.min.js>"></script>
// ConnectComponent.tsx
const widget = new NarvalConnectWidget({
onSuccess: function (data: { interactRef: string; hash: string; finishUri: string }) {
// call success handler
},
onError: function (error) {
console.error('Narval Widget error:', error)
},
onClose: function () {
console.log('Narval Widget closed')
}
})
const handleClick = async () => {
// Call the server to initiate the connection
const { connectUrl } = await fetch(/*Route to Initiate Connect handler*/)
widget.open(connectUrl)
}
// ...
<button onClick={() => handleClick()}>Create Connection</button>
4. Get the Access Token
Implement this in your Server-side code.
interface ConnectCompleteResponse {
connectionId: string;
accessToken: {
label: string;
value: string;
};
}
async function completeNarvalConnection(data: {
interactRef: string, hash: string, finishUri: string
}) {
// 1. Look up persisted connectRequest
const connectRequest = await db.connectRequest.findUnique({
where: {
finishUri: data.finishUri,
},
});
// 2. Validate the hash matches your expected hash
const verificationHash = ConnectClient.hash({
nonce: connectRequest.nonce,
finish: connectRequest.grantResponse.interact.finish,
interactRef: data.interactRef,
host: "<https://auth.armory.narval.xyz>",
});
if (data.hash !== verificationHash)
throw new Error("Hash verification failed");
// 3. Get your Access Token
const response: ConnectCompleteResponse = await connectClient.completeConnect(
{
continueToken: connectRequest.grantResponse.continue.access_token.value,
interactRef: data.interactRef,
}
);
// Persist the response data and use `response.accessToken.value`
// to make API calls with the Connection!
}