Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 6 additions & 74 deletions lib/OID4Client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/*!
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
*/
import {generateDIDProofJWT, robustDiscoverIssuer} from './util.js';
import {createCredentialRequestsFromOffer} from './oid4vci/credentialOffer.js';
import {generateDIDProofJWT} from './oid4vci/proofs.js';
import {httpClient} from '@digitalbazaar/http-client';
import {robustDiscoverIssuer} from './oid4vci/discovery.js';

const GRANT_TYPES = new Map([
['preAuthorizedCode', 'urn:ietf:params:oauth:grant-type:pre-authorized_code']
Expand Down Expand Up @@ -66,7 +68,7 @@ export class OID4Client {
if(!offer) {
throw new TypeError('"credentialDefinition" must be an object.');
}
requests = _createCredentialRequestsFromOffer({
requests = createCredentialRequestsFromOffer({
issuerConfig, offer, format
});
if(requests.length > 1) {
Expand Down Expand Up @@ -97,7 +99,7 @@ export class OID4Client {

const {issuerConfig, offer} = this;
if(requests === undefined && offer) {
requests = _createCredentialRequestsFromOffer({
requests = createCredentialRequestsFromOffer({
issuerConfig, offer, format
});
} else if(!(Array.isArray(requests) && requests.length > 0)) {
Expand Down Expand Up @@ -464,73 +466,3 @@ function _isPresentationRequired(error) {
const errorType = error.data?.error;
return error.status === 400 && errorType === 'presentation_required';
}

function _createCredentialRequestsFromOffer({
issuerConfig, offer, format
}) {
// get any supported credential configurations from issuer config
const supported = _createSupportedCredentialsMap({issuerConfig});

// build requests from credentials identified in `offer` and remove any
// that do not match the given format
const credentials = offer.credential_configuration_ids ?? offer.credentials;
const requests = credentials.map(c => {
if(typeof c === 'string') {
// use supported credential config
return _getSupportedCredentialById({id: c, supported});
}
return c;
}).filter(r => r.format === format);

if(requests.length === 0) {
throw new Error(
`No supported credential(s) with format "${format}" found.`);
}

return requests;
}

function _createSupportedCredentialsMap({issuerConfig}) {
const {
credential_configurations_supported,
credentials_supported
} = issuerConfig;

let supported;
if(credential_configurations_supported &&
typeof credential_configurations_supported === 'object') {
supported = new Map(Object.entries(
issuerConfig.credential_configurations_supported));
} else if(Array.isArray(credentials_supported)) {
// handle legacy `credentials_supported` array
supported = new Map();
for(const entry of issuerConfig.credentials_supported) {
supported.set(entry.id, entry);
}
} else {
// no supported credentials from issuer config
supported = new Map();
}

return supported;
}

function _getSupportedCredentialById({id, supported}) {
const meta = supported.get(id);
if(!meta) {
throw new Error(`No supported credential "${id}" found.`);
}
const {format, credential_definition} = meta;
if(typeof format !== 'string') {
throw new Error(
`Invalid supported credential "${id}"; "format" not specified.`);
}
if(!(Array.isArray(credential_definition?.['@context']) &&
(Array.isArray(credential_definition?.types) ||
Array.isArray(credential_definition?.type)))) {
throw new Error(
`Invalid supported credential "${id}"; "credential_definition" not ` +
'fully specified.');
}
return {format, credential_definition};
}
2 changes: 1 addition & 1 deletion lib/convert.js → lib/convert/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {JSONPath} from 'jsonpath-plus';
import jsonpointer from 'jsonpointer';
import {
validate as validateAuthorizationRequest
} from './authorizationRequest.js';
} from '../oid4vp/authorizationRequest.js';

// converts a VPR to partial "authorization request"
export function fromVpr({
Expand Down
12 changes: 8 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/*!
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
*/
export * as oid4vp from './oid4vp.js';
export * as oid4vp from './oid4vp/index.js';
export {
discoverIssuer,
generateDIDProofJWT,
robustDiscoverIssuer
} from './oid4vci/discovery.js';
export {
getCredentialOffer,
parseCredentialOfferUrl,
robustDiscoverIssuer,
parseCredentialOfferUrl
} from './oid4vci/credentialOffer.js';
export {
signJWT,
selectJwk
} from './util.js';
export {generateDIDProofJWT} from './oid4vci/proofs.js';
export {OID4Client} from './OID4Client.js';
138 changes: 138 additions & 0 deletions lib/oid4vci/credentialOffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*!
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
*/
import {assert, fetchJSON} from '../util.js';

export function createCredentialRequestsFromOffer({
issuerConfig, offer, format
} = {}) {
// get any supported credential configurations from issuer config
const supported = _createSupportedCredentialsMap({issuerConfig});

// build requests from credentials identified in `offer` and remove any
// that do not match the given format
const credentials = offer.credential_configuration_ids ?? offer.credentials;
const requests = credentials.map(c => {
if(typeof c === 'string') {
// use supported credential config
return _getSupportedCredentialById({id: c, supported});
}
return c;
}).filter(r => r.format === format);

if(requests.length === 0) {
throw new Error(
`No supported credential(s) with format "${format}" found.`);
}

return requests;
}

export async function getCredentialOffer({url, agent} = {}) {
const {protocol, searchParams} = new URL(url);
if(protocol !== 'openid-credential-offer:') {
throw new SyntaxError(
'"url" must express a URL with the ' +
'"openid-credential-offer" protocol.');
}
const offer = searchParams.get('credential_offer');
if(offer) {
return JSON.parse(offer);
}

// try to fetch offer from URL
const offerUrl = searchParams.get('credential_offer_uri');
if(!offerUrl) {
throw new SyntaxError(
'OID4VCI credential offer must have "credential_offer" or ' +
'"credential_offer_uri".');
}

if(!offerUrl.startsWith('https://')) {
const error = new Error(
`"credential_offer_uri" (${offerUrl}) must start with "https://".`);
error.name = 'NotSupportedError';
throw error;
}

const response = await fetchJSON({url: offerUrl, agent});
if(!response.data) {
const error = new Error(
`Credential offer fetched from "${offerUrl}" is not JSON.`);
error.name = 'DataError';
throw error;
}
return response.data;
}

export function parseCredentialOfferUrl({url} = {}) {
assert(url, 'url', 'string');

/* Parse URL, e.g.:

'openid-credential-offer://?' +
'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
'%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
'%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
'%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
'%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
*/
const {protocol, searchParams} = new URL(url);
if(protocol !== 'openid-credential-offer:') {
throw new SyntaxError(
'"url" must express a URL with the ' +
'"openid-credential-offer" protocol.');
}
return JSON.parse(searchParams.get('credential_offer'));
}

function _createSupportedCredentialsMap({issuerConfig}) {
const {
credential_configurations_supported,
credentials_supported
} = issuerConfig;

let supported;
if(credential_configurations_supported &&
typeof credential_configurations_supported === 'object') {
supported = new Map(Object.entries(
issuerConfig.credential_configurations_supported));
} else if(Array.isArray(credentials_supported)) {
// handle legacy `credentials_supported` array
supported = new Map();
for(const entry of issuerConfig.credentials_supported) {
supported.set(entry.id, entry);
}
} else {
// no supported credentials from issuer config
supported = new Map();
}

return supported;
}

function _getSupportedCredentialById({id, supported}) {
const meta = supported.get(id);
if(!meta) {
throw new Error(`No supported credential "${id}" found.`);
}
const {format, credential_definition} = meta;
if(typeof format !== 'string') {
throw new Error(
`Invalid supported credential "${id}"; "format" not specified.`);
}
if(!(Array.isArray(credential_definition?.['@context']) &&
(Array.isArray(credential_definition?.types) ||
Array.isArray(credential_definition?.type)))) {
throw new Error(
`Invalid supported credential "${id}"; "credential_definition" not ` +
'fully specified.');
}
return {format, credential_definition};
}
Loading