@sphereon/oid4vci-common
Version:
OpenID 4 Verifiable Credential Issuance Common Types
91 lines (81 loc) • 3.5 kB
text/typescript
import { CredentialResponse, OpenIDResponse } from '../types';
import { post } from './HttpUtils';
export function isDeferredCredentialResponse(credentialResponse: OpenIDResponse<CredentialResponse>) {
const orig = credentialResponse.successBody;
// Specs mention 202, but some implementations like EBSI return 200
return credentialResponse.origResponse.status % 200 <= 2 && !!orig && !orig.credential && (!!orig.acceptance_token || !!orig.transaction_id);
}
function assertNonFatalError(credentialResponse: OpenIDResponse<CredentialResponse>) {
if (credentialResponse.origResponse.status === 400 && credentialResponse.errorBody?.error) {
if (credentialResponse.errorBody.error === 'invalid_transaction_id' || credentialResponse.errorBody.error.includes('acceptance_token')) {
throw Error('Invalid transaction id. Probably the deferred credential request expired');
}
}
}
export function isDeferredCredentialIssuancePending(credentialResponse: OpenIDResponse<CredentialResponse>) {
if (isDeferredCredentialResponse(credentialResponse)) {
return !!credentialResponse?.successBody?.transaction_id ?? !!credentialResponse?.successBody?.acceptance_token;
}
if (credentialResponse.origResponse.status === 400 && credentialResponse.errorBody?.error) {
if (credentialResponse.errorBody.error === 'issuance_pending') {
return true;
} else if (credentialResponse.errorBody.error_description?.toLowerCase().includes('not available yet')) {
return true;
}
}
return false;
}
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export async function acquireDeferredCredential({
bearerToken,
transactionId,
deferredCredentialEndpoint,
deferredCredentialIntervalInMS,
deferredCredentialAwait,
}: {
bearerToken: string;
transactionId?: string;
deferredCredentialIntervalInMS?: number;
deferredCredentialAwait?: boolean;
deferredCredentialEndpoint: string;
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
let credentialResponse: OpenIDResponse<CredentialResponse> & { access_token: string } = await acquireDeferredCredentialImpl({
bearerToken,
transactionId,
deferredCredentialEndpoint,
});
const DEFAULT_SLEEP_IN_MS = 5000;
while (!credentialResponse.successBody?.credential && deferredCredentialAwait) {
assertNonFatalError(credentialResponse);
const pending = isDeferredCredentialIssuancePending(credentialResponse);
console.log(`Issuance still pending?: ${pending}`);
if (!pending) {
throw Error(`Issuance isn't pending anymore: ${credentialResponse}`);
}
await sleep(deferredCredentialIntervalInMS ?? DEFAULT_SLEEP_IN_MS);
credentialResponse = await acquireDeferredCredentialImpl({ bearerToken, transactionId, deferredCredentialEndpoint });
}
return credentialResponse;
}
async function acquireDeferredCredentialImpl({
bearerToken,
transactionId,
deferredCredentialEndpoint,
}: {
bearerToken: string;
transactionId?: string;
deferredCredentialEndpoint: string;
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const response: OpenIDResponse<CredentialResponse> = await post(
deferredCredentialEndpoint,
JSON.stringify(transactionId ? { transaction_id: transactionId } : ''),
{ bearerToken },
);
console.log(JSON.stringify(response, null, 2));
assertNonFatalError(response);
return { ...response, access_token: bearerToken };
}