import { CompactEncrypt, importJWK, JWK } from 'jose';
const KNOT_BASE = 'https://development.knotapi.com';
function basicAuthHeader(clientId: string, secret: string): string {
const creds = Buffer.from(`${clientId}:${secret}`, 'utf8').toString('base64');
return `Basic ${creds}`;
}
/**
* GetKey gets the JWK associated with your client ID from the Knot API.
* The implementation follows the steps outlined in the KnotAPI documentation:
* https://docs.knotapi.com/api-reference/products/card-switcher/retrieve-jwk
*/
export async function getKey(): Promise<JWK> {
const clientId = process.env.KNOT_CLIENT_ID || '';
const secret = process.env.KNOT_SECRET || '';
if (!clientId || !secret) {
throw new Error('KNOT_CLIENT_ID and KNOT_SECRET must be set in the environment.');
}
const resp = await fetch(`${KNOT_BASE}/jwe/key`, {
method: 'GET',
headers: {
Authorization: basicAuthHeader(clientId, secret),
Accept: 'application/json',
},
});
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`Failed to fetch JWK (${resp.status}): ${text}`);
}
const jwk = (await resp.json()) as JWK;
return jwk;
}
/**
* encryptData encrypts the card data using the provided JWK, the JWE must contain the JWK's "kid" and "alg" parameters to be considered valid.
* The implementation follows the steps outlined in the KnotAPI documentation:
* https://docs.knotapi.com/api-reference/products/card-switcher/retrieve-jwk#building-the-jwe
* You can also opt to implement these RFC's manually instead of using a library:
* https://datatracker.ietf.org/doc/html/rfc7516
* https://datatracker.ietf.org/doc/html/rfc7517
* https://datatracker.ietf.org/doc/html/rfc7518
* https://datatracker.ietf.org/doc/html/rfc7638
*/
export async function encryptData(cardData: unknown, jwk: JWK): Promise<string> {
const alg = jwk.alg;
if (!alg) {
throw new Error('JWK is missing "alg" (required).');
}
// Import the public key from JWK for encryption
// jose infers the right key type/algorithm from JWK fields
const publicKey = await importJWK(jwk, alg);
const plaintext = new TextEncoder().encode(JSON.stringify(cardData));
const jwe = await new CompactEncrypt(plaintext)
.setProtectedHeader({
alg,
enc: 'A256GCM',
...(jwk.kid ? { kid: jwk.kid } : {}),
})
.encrypt(publicKey);
return jwe;
}
/**
* submitJWE Submits the JWE to the Knot API for an active task.
* The implementation follows the steps outlined in the KnotAPI documentation:
* https://docs.knotapi.com/api-reference/products/card-switcher/switch-card-jwe
*/
export async function submitJWE(taskId: string, jwe: string): Promise<void> {
const clientId = process.env.KNOT_CLIENT_ID || '';
const secret = process.env.KNOT_SECRET || '';
if (!clientId || !secret) {
throw new Error('KNOT_CLIENT_ID and KNOT_SECRET must be set in the environment.');
}
console.log(`Submitting JWE: ${jwe}`);
const resp = await fetch(`${KNOT_BASE}/card`, {
method: 'POST',
headers: {
Authorization: basicAuthHeader(clientId, secret),
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ task_id: taskId, jwe }),
});
const text = await resp.text().catch(() => '');
// Try parse JSON if possible for error_message
let parsed: Record<string, unknown> | null = null;
try { parsed = text ? JSON.parse(text) : null; } catch {}
if (!resp.ok) {
const msg = parsed?.error_message ?? (text || `HTTP ${resp.status}`);
throw new Error(String(msg));
}
const errorMessage = (parsed?.error_message as string | undefined) ?? undefined;
if (errorMessage) {
throw new Error(errorMessage);
}
}
async function main() {
/**
* Hardcoded card data for demonstration purposes.
* You should use your own card data.
* See the KnotAPI documentation for more information:
* https://docs.knotapi.com/api-reference/products/card-switcher/retrieve-jwk#building-the-jwe
*/
const cardData = {
user: {
name: {
first_name: 'Ada', // Max length: 255
last_name: 'Lovelace', // Max length: 255
},
address: {
street: '100 Main Street', // Max length: 46
street2: '#100', // Max length: 46
city: 'NEW YORK', // Max length: 32
region: 'NY', // Must be an ISO 3166-2 sub-division code
postal_code: '12345', // Min length: 5, Max length: 10
country: 'US', // Must be an ISO 3166-1 alpha-2 code
},
phone_number: '+11234567890', // Must be in E.164 format
},
card: {
number: '4242424242424242', // Card number
expiration: '08/2030', // MM/YYYY or MM/YY format
cvv: '012', // Max length: 4
},
};
const jwk = await getKey();
const jwe = await encryptData(cardData, jwk);
await submitJWE('123456', jwe);
}
if (require.main === module) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}