@animo-id/expo-digital-credentials-api
Version:
Expo wrapper around Android Digital Credentials API
120 lines • 4.98 kB
JavaScript
import { decodeBase64, encodeBase64 } from './util';
function recursivelyMapSdJwtDc(claims, display, path) {
const result = {};
for (const [key, value] of Object.entries(claims)) {
if (!value || Array.isArray(value) || typeof value !== 'object') {
const claimDisplay = display.claims?.find((claim) => claim.path.join('.') === [...path, key].join('.'));
const displayName = claimDisplay?.displayName ?? key;
result[key] = {
display: displayName,
// Do not allow matching based on array claims for now
value: Array.isArray(value) ? undefined : value ?? undefined,
};
}
else {
result[key] = recursivelyMapSdJwtDc(value, display, [...path, key]);
}
}
return result;
}
// TODO: we should allow registering a custom matcher and thus custom credential bytes structure
export function getEncodedCredentialsBase64(items, { debug }) {
const textEncoder = new TextEncoder();
const chunks = [];
// Create icon map
const iconRecord = {};
for (const item of items) {
const iconBytes = item.display.iconDataUrl
? decodeBase64(item.display.iconDataUrl.replace('data:image/png;base64,', '').replace('data:image/jpg;base64,', ''))
: new Uint8Array(0);
iconRecord[item.id] = { iconValue: iconBytes, iconOffset: 0 };
}
// Calculate total icon size
const totalIconSize = Object.values(iconRecord).reduce((sum, icon) => sum + icon.iconValue.length, 0);
// Calculate and write JSON offset (4 bytes, little endian)
const jsonOffset = 4 + totalIconSize;
const offsetBuffer = new ArrayBuffer(4);
new DataView(offsetBuffer).setInt32(0, jsonOffset, true);
chunks.push(new Uint8Array(offsetBuffer));
// Write icons and update offsets
let currentOffset = 4;
for (const icon of Object.values(iconRecord)) {
icon.iconOffset = currentOffset;
chunks.push(icon.iconValue);
currentOffset += icon.iconValue.length;
}
// Create credential JSON structure
// Mapping of doctype/vct => credentials[]
const mdocCredentials = {};
const sdJwtCredentials = {};
for (const item of items) {
const icon = iconRecord[item.id];
const credentialJson = {
id: item.id,
title: item.display.title,
subtitle: item.display.subtitle,
icon: icon.iconValue.length > 0
? {
start: iconRecord[item.id].iconOffset,
length: iconRecord[item.id].iconValue.length,
}
: null,
};
if (item.credential.format === 'mso_mdoc') {
const pathsJson = {};
for (const [namespace, elements] of Object.entries(item.credential.namespaces)) {
pathsJson[namespace] = {};
for (const [element, value] of Object.entries(elements)) {
const claimDisplay = item.display.claims?.find((claim) => claim.path.join('.') === [namespace, element].join('.'));
const displayName = claimDisplay?.displayName ?? element;
pathsJson[namespace][element] = {
value: value ?? undefined,
display: displayName,
};
}
}
// Add to doctype array
if (!mdocCredentials[item.credential.doctype]) {
mdocCredentials[item.credential.doctype] = [];
}
mdocCredentials[item.credential.doctype].push({
...credentialJson,
paths: pathsJson,
});
}
else if (item.credential.format === 'dc+sd-jwt') {
const pathsJson = recursivelyMapSdJwtDc(item.credential.claims, item.display, []);
// Add to vct array
if (!sdJwtCredentials[item.credential.vct]) {
sdJwtCredentials[item.credential.vct] = [];
}
sdJwtCredentials[item.credential.vct].push({
...credentialJson,
paths: pathsJson,
});
}
else {
throw new Error('Unsupported format. Only mso_mdoc supported');
}
}
// Create final JSON structure
const registryJson = {
debug,
credentials: {
mso_mdoc: mdocCredentials,
'dc+sd-jwt': sdJwtCredentials,
},
};
// Convert JSON to bytes and add to chunks
chunks.push(textEncoder.encode(JSON.stringify(registryJson)));
// Combine all chunks
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return encodeBase64(result);
}
//# sourceMappingURL=encodeCredentials.js.map