@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
215 lines • 8.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateJwt = generateJwt;
exports.generateWalletJwt = generateWalletJwt;
const jose_1 = require("jose");
const uncrypto_1 = require("uncrypto");
const hash_js_1 = require("./hash.js");
const errors_js_1 = require("../../errors.js");
const sortKeys_js_1 = require("../../utils/sortKeys.js");
const errors_js_2 = require("../errors.js");
/**
* Generates a JWT (also known as a Bearer token) for authenticating with Coinbase's REST APIs.
* Supports both EC (ES256) and Ed25519 (EdDSA) keys. Also supports JWTs meant for
* websocket connections by allowing requestMethod, requestHost, and requestPath to all be
* null, in which case the 'uris' claim is omitted from the JWT.
*
* @param options - The configuration options for generating the JWT
* @returns The generated JWT (Bearer token) string
* @throws {Error} If required parameters are missing, invalid, or if JWT signing fails
*/
async function generateJwt(options) {
// Validate required parameters
if (!options.apiKeyId) {
throw new Error("Key name is required");
}
if (!options.apiKeySecret) {
throw new Error("Private key is required");
}
// Check if we have a REST API request or a websocket connection
const hasAllRequestParams = Boolean(options.requestMethod && options.requestHost && options.requestPath);
const hasNoRequestParams = (options.requestMethod === undefined || options.requestMethod === null) &&
(options.requestHost === undefined || options.requestHost === null) &&
(options.requestPath === undefined || options.requestPath === null);
// Ensure we either have all request parameters or none (for websocket)
if (!hasAllRequestParams && !hasNoRequestParams) {
throw new Error("Either all request details (method, host, path) must be provided, or all must be null for JWTs intended for websocket connections");
}
const now = Math.floor(Date.now() / 1000);
const expiresIn = options.expiresIn || 120; // Default to 120 seconds if not specified
// Prepare the JWT payload
const claims = {
sub: options.apiKeyId,
iss: "cdp",
aud: options.audience || ["cdp_service"],
};
// Add the uris claim only for REST API requests
if (hasAllRequestParams) {
claims.uris = [`${options.requestMethod} ${options.requestHost}${options.requestPath}`];
}
// Generate random nonce for the header
const randomNonce = nonce();
// Determine if we're using EC or Edwards key based on the key format
if (await isValidECKey(options.apiKeySecret)) {
return await buildECJWT(options.apiKeySecret, options.apiKeyId, claims, now, expiresIn, randomNonce);
}
else if (isValidEd25519Key(options.apiKeySecret)) {
return await buildEdwardsJWT(options.apiKeySecret, options.apiKeyId, claims, now, expiresIn, randomNonce);
}
else {
throw new errors_js_1.UserInputValidationError("Invalid key format - must be either PEM EC key or base64 Ed25519 key");
}
}
/**
* Generates a wallet authentication JWT for the given API endpoint URL.
* Used for authenticating with specific endpoints that require wallet authentication.
*
* @param options - The configuration options for generating the JWT
* @returns The generated JWT (Bearer token) string
* @throws {UndefinedWalletSecretError} If the Wallet Secret is not defined.
* @throws {InvalidWalletSecretFormatError} If the private key is not in the correct format or signing fails.
*/
async function generateWalletJwt(options) {
if (!options.walletSecret) {
throw new errors_js_2.UndefinedWalletSecretError("Wallet Secret is not defined");
}
const uri = `${options.requestMethod} ${options.requestHost}${options.requestPath}`;
const now = Math.floor(Date.now() / 1000);
const claims = {
uris: [uri],
};
if (Object.keys(options.requestData).length > 0) {
const sortedData = (0, sortKeys_js_1.sortKeys)(options.requestData);
claims.reqHash = await (0, hash_js_1.authHash)(Buffer.from(JSON.stringify(sortedData)));
}
try {
// Convert base64 DER to PEM format for jose
const derBuffer = Buffer.from(options.walletSecret, "base64");
const pemKey = `-----BEGIN PRIVATE KEY-----\n${derBuffer
.toString("base64")
.match(/.{1,64}/g)
?.join("\n")}\n-----END PRIVATE KEY-----`;
const ecKey = await (0, jose_1.importPKCS8)(pemKey, "ES256");
return await new jose_1.SignJWT(claims)
.setProtectedHeader({ alg: "ES256", typ: "JWT" })
.setIssuedAt(now)
.setNotBefore(now)
.setJti(nonce())
.sign(ecKey);
}
catch (error) {
throw new errors_js_2.InvalidWalletSecretFormatError("Could not create the EC key: " + error);
}
}
/**
* Determines if a string could be a valid Ed25519 key
*
* @param str - The string to test
* @returns True if the string could be a valid Ed25519 key, false otherwise
*/
function isValidEd25519Key(str) {
try {
const decoded = Buffer.from(str, "base64");
return decoded.length === 64;
}
catch {
return false;
}
}
/**
* Determines if a string is a valid EC private key in PEM format
*
* @param str - The string to test
* @returns True if the string is a valid EC private key in PEM format
*/
async function isValidECKey(str) {
try {
// Try to import the key with jose - if it works, it's a valid EC key
await (0, jose_1.importPKCS8)(str, "ES256");
return true;
}
catch {
return false;
}
}
/**
* Builds a JWT using an EC key.
*
* @param privateKey - The EC private key in PEM format
* @param keyName - The key name/ID
* @param claims - The JWT claims
* @param now - Current timestamp in seconds
* @param expiresIn - Number of seconds until the token expires
* @param nonce - Random nonce for the JWT header
* @returns A JWT token signed with an EC key
* @throws {Error} If key conversion, import, or signing fails
*/
async function buildECJWT(privateKey, keyName, claims, now, expiresIn, nonce) {
try {
// Import the key directly with jose
const ecKey = await (0, jose_1.importPKCS8)(privateKey, "ES256");
// Sign and return the JWT
return await new jose_1.SignJWT(claims)
.setProtectedHeader({ alg: "ES256", kid: keyName, typ: "JWT", nonce })
.setIssuedAt(Math.floor(now))
.setNotBefore(Math.floor(now))
.setExpirationTime(Math.floor(now + expiresIn))
.sign(ecKey);
}
catch (error) {
throw new Error(`Failed to generate EC JWT: ${error.message}`);
}
}
/**
* Builds a JWT using an Ed25519 key.
*
* @param privateKey - The Ed25519 private key in base64 format
* @param keyName - The key name/ID
* @param claims - The JWT claims
* @param now - Current timestamp in seconds
* @param expiresIn - Number of seconds until the token expires
* @param nonce - Random nonce for the JWT header
* @returns A JWT token using an Ed25519 key
* @throws {Error} If key parsing, import, or signing fails
*/
async function buildEdwardsJWT(privateKey, keyName, claims, now, expiresIn, nonce) {
try {
// Decode the base64 key (expecting 64 bytes: 32 for seed + 32 for public key)
const decoded = Buffer.from(privateKey, "base64");
if (decoded.length !== 64) {
throw new errors_js_1.UserInputValidationError("Invalid Ed25519 key length");
}
const seed = decoded.subarray(0, 32);
const publicKey = decoded.subarray(32);
// Create JWK from the key components
const jwk = {
kty: "OKP",
crv: "Ed25519",
d: seed.toString("base64url"),
x: publicKey.toString("base64url"),
};
// Import the key for signing
const key = await (0, jose_1.importJWK)(jwk, "EdDSA");
// Sign and return the JWT
return await new jose_1.SignJWT(claims)
.setProtectedHeader({ alg: "EdDSA", kid: keyName, typ: "JWT", nonce })
.setIssuedAt(Math.floor(now))
.setNotBefore(Math.floor(now))
.setExpirationTime(Math.floor(now + expiresIn))
.sign(key);
}
catch (error) {
throw new Error(`Failed to generate Ed25519 JWT: ${error.message}`);
}
}
/**
* Generates a random nonce for the JWT.
*
* @returns {string} The generated nonce.
*/
function nonce() {
const bytes = new Uint8Array(16);
(0, uncrypto_1.getRandomValues)(bytes);
return Buffer.from(bytes).toString("hex");
}
//# sourceMappingURL=jwt.js.map