@sphereon/oid4vci-issuer
Version:
OpenID 4 Verifiable Credential Issuance issuer REST endpoints
1,386 lines (1,375 loc) • 76.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// lib/index.ts
import { VCI_LOGGERS } from "@sphereon/oid4vci-common";
// lib/builder/CredentialSupportedBuilderV1_15.ts
import { TokenErrorResponse } from "@sphereon/oid4vci-common";
var CredentialSupportedBuilderV1_15 = class {
static {
__name(this, "CredentialSupportedBuilderV1_15");
}
format;
scope;
credentialName;
credentialDefinition;
cryptographicBindingMethodsSupported;
credentialSigningAlgValuesSupported;
proofTypesSupported;
display;
claims;
vct;
doctype;
withFormat(credentialFormat) {
this.format = credentialFormat;
return this;
}
withCredentialName(credentialName) {
this.credentialName = credentialName;
return this;
}
withCredentialDefinition(credentialDefinition) {
if (!credentialDefinition.type) {
throw new Error("credentialDefinition should contain a type array");
}
this.credentialDefinition = credentialDefinition;
return this;
}
withScope(scope) {
this.scope = scope;
return this;
}
// New in v15: VCT support for dc+sd-jwt format
withVct(vct) {
this.vct = vct;
return this;
}
// New in v15: Doctype support for mso_mdoc format
withDoctype(doctype) {
this.doctype = doctype;
return this;
}
addCryptographicBindingMethod(method) {
if (!Array.isArray(method)) {
this.cryptographicBindingMethodsSupported = this.cryptographicBindingMethodsSupported ? [
...this.cryptographicBindingMethodsSupported,
method
] : [
method
];
} else {
this.cryptographicBindingMethodsSupported = this.cryptographicBindingMethodsSupported ? [
...this.cryptographicBindingMethodsSupported,
...method
] : method;
}
return this;
}
withCryptographicBindingMethod(method) {
this.cryptographicBindingMethodsSupported = Array.isArray(method) ? method : [
method
];
return this;
}
addCredentialSigningAlgValuesSupported(algValues) {
if (!Array.isArray(algValues)) {
this.credentialSigningAlgValuesSupported = this.credentialSigningAlgValuesSupported ? [
...this.credentialSigningAlgValuesSupported,
algValues
] : [
algValues
];
} else {
this.credentialSigningAlgValuesSupported = this.credentialSigningAlgValuesSupported ? [
...this.credentialSigningAlgValuesSupported,
...algValues
] : algValues;
}
return this;
}
withCredentialSigningAlgValuesSupported(algValues) {
this.credentialSigningAlgValuesSupported = Array.isArray(algValues) ? algValues : [
algValues
];
return this;
}
addProofTypesSupported(keyProofType, proofType) {
if (!this.proofTypesSupported) {
this.proofTypesSupported = {};
}
this.proofTypesSupported[keyProofType] = proofType;
return this;
}
withProofTypesSupported(proofTypesSupported) {
this.proofTypesSupported = proofTypesSupported;
return this;
}
addCredentialSupportedDisplay(credentialDisplay) {
if (!Array.isArray(credentialDisplay)) {
this.display = this.display ? [
...this.display,
credentialDisplay
] : [
credentialDisplay
];
} else {
this.display = this.display ? [
...this.display,
...credentialDisplay
] : credentialDisplay;
}
return this;
}
withCredentialSupportedDisplay(credentialDisplay) {
this.display = Array.isArray(credentialDisplay) ? credentialDisplay : [
credentialDisplay
];
return this;
}
// New in v15: Claims description using path pointers
withClaims(claims) {
this.claims = claims;
return this;
}
addClaim(claim) {
if (!this.claims) {
this.claims = [];
}
this.claims.push(claim);
return this;
}
build() {
if (!this.format) {
throw new Error(TokenErrorResponse.invalid_request);
}
const credentialSupported = {
format: this.format
};
if (!this.credentialName) {
throw new Error("A unique credential name is required");
}
if (this.format === "dc+sd-jwt") {
if (!this.vct) {
throw new Error("vct is required for dc+sd-jwt format");
}
;
credentialSupported.vct = this.vct;
} else if (this.format === "mso_mdoc") {
if (!this.doctype) {
throw new Error("doctype is required for mso_mdoc format");
}
;
credentialSupported.doctype = this.doctype;
} else {
if (!this.credentialDefinition) {
throw new Error("credentialDefinition is required");
}
credentialSupported.credential_definition = this.credentialDefinition;
}
if (this.scope) {
credentialSupported.scope = this.scope;
}
if (this.credentialSigningAlgValuesSupported) {
credentialSupported.credential_signing_alg_values_supported = this.credentialSigningAlgValuesSupported;
}
if (this.cryptographicBindingMethodsSupported) {
credentialSupported.cryptographic_binding_methods_supported = this.cryptographicBindingMethodsSupported;
}
if (this.display) {
credentialSupported.display = this.display;
}
if (this.claims) {
;
credentialSupported.claims = this.claims;
}
const supportedConfiguration = {};
supportedConfiguration[this.credentialName] = credentialSupported;
return supportedConfiguration;
}
};
// lib/builder/VcIssuerBuilder.ts
import { TokenErrorResponse as TokenErrorResponse3 } from "@sphereon/oid4vci-common";
// lib/VcIssuer.ts
import { uuidv4 as uuidv42 } from "@sphereon/oid4vc-common";
import { ALG_ERROR, AUD_ERROR, CREDENTIAL_MISSING_ERROR, CredentialEventNames, CredentialOfferEventNames, DID_NO_DIDDOC_ERROR, EVENTS, IAT_ERROR, ISSUER_CONFIG_ERROR, IssueStatus, JWT_VERIFY_CONFIG_ERROR, KID_DID_NO_DID_ERROR, KID_JWK_X5C_ERROR, NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT, OpenId4VCIVersion, PRE_AUTH_GRANT_LITERAL as PRE_AUTH_GRANT_LITERAL2, TokenErrorResponse as TokenErrorResponse2, toUniformCredentialOfferRequest, TYP_ERROR } from "@sphereon/oid4vci-common";
import { CredentialMapper, InitiatorType, SubSystem, System } from "@sphereon/ssi-types";
import ShortUUID from "short-uuid";
// lib/functions/CredentialOfferUtils.ts
import { uuidv4 } from "@sphereon/oid4vc-common";
import { PIN_NOT_MATCH_ERROR, PRE_AUTH_GRANT_LITERAL } from "@sphereon/oid4vci-common";
function createCredentialOfferGrants(inputGrants) {
if (!inputGrants || Object.keys(inputGrants).length === 0) {
return void 0;
}
const grants = {};
if (inputGrants?.[PRE_AUTH_GRANT_LITERAL]) {
const grant = {
...inputGrants[PRE_AUTH_GRANT_LITERAL],
"pre-authorized_code": inputGrants[PRE_AUTH_GRANT_LITERAL]["pre-authorized_code"] ?? uuidv4()
};
if (grant.tx_code && !grant.tx_code.length) {
grant.tx_code.length = 4;
}
grants[PRE_AUTH_GRANT_LITERAL] = grant;
}
if (inputGrants?.authorization_code) {
grants.authorization_code = {
...inputGrants.authorization_code,
// TODO: it should be possible to create offer without issuer_state
// this is added to avoid breaking changes.
issuer_state: inputGrants.authorization_code.issuer_state ?? uuidv4()
};
}
return grants;
}
__name(createCredentialOfferGrants, "createCredentialOfferGrants");
function parseCredentialOfferSchemeAndBaseUri(scheme, baseUri, credentialIssuer) {
const newScheme = scheme?.replace("://", "") ?? (baseUri?.includes("://") ? baseUri.split("://")[0] : "openid-credential-offer");
let newBaseUri;
if (baseUri) {
newBaseUri = baseUri;
} else if (newScheme.startsWith("http")) {
if (credentialIssuer) {
newBaseUri = credentialIssuer;
if (!newBaseUri.startsWith(`${newScheme}://`)) {
throw Error(`scheme ${newScheme} is different from base uri ${newBaseUri}`);
}
} else {
throw Error(`A '${newScheme}' scheme requires a URI to be present as baseUri`);
}
} else {
newBaseUri = "";
}
newBaseUri = newBaseUri?.replace(`${newScheme}://`, "");
return {
scheme: newScheme,
baseUri: newBaseUri
};
}
__name(parseCredentialOfferSchemeAndBaseUri, "parseCredentialOfferSchemeAndBaseUri");
function createCredentialOfferObject(issuerMetadata, opts) {
if (!issuerMetadata && !opts?.credentialOffer && !opts?.credentialOfferUri) {
throw new Error("You have to provide issuerMetadata or credentialOffer object for creating a deeplink");
}
const grants = createCredentialOfferGrants(opts?.grants);
let credential_offer;
if (opts?.credentialOffer) {
credential_offer = {
...opts.credentialOffer
};
} else {
if (!issuerMetadata?.credential_configurations_supported) {
throw new Error("credential_configurations_supported is mandatory in the metadata");
}
credential_offer = {
credential_issuer: issuerMetadata.credential_issuer,
credential_configuration_ids: Object.keys(issuerMetadata.credential_configurations_supported)
};
}
if (grants) {
credential_offer.grants = grants;
}
if (opts?.client_id) {
credential_offer.client_id = opts.client_id;
}
return {
credential_offer,
credential_offer_uri: opts?.credentialOfferUri
};
}
__name(createCredentialOfferObject, "createCredentialOfferObject");
function createCredentialOfferURIFromObject(credentialOffer, offerMode, opts) {
const { scheme, baseUri } = parseCredentialOfferSchemeAndBaseUri(opts?.scheme, opts?.baseUri, credentialOffer.credential_offer?.credential_issuer);
if (offerMode === "REFERENCE") {
if (!credentialOffer.credential_offer_uri) {
throw Error(`credential_offer_uri must be set for offerMode ${offerMode}`);
}
if (credentialOffer.credential_offer_uri.includes("credential_offer_uri=")) {
return credentialOffer.credential_offer_uri;
}
return `${scheme}://${baseUri}?credential_offer_uri=${encodeURIComponent(credentialOffer.credential_offer_uri)}`;
} else if (offerMode === "VALUE") {
return `${scheme}://${baseUri}?credential_offer=${encodeURIComponent(JSON.stringify(credentialOffer.credential_offer))}`;
}
throw Error(`unsupported offerMode ${offerMode}`);
}
__name(createCredentialOfferURIFromObject, "createCredentialOfferURIFromObject");
function createCredentialOfferURI(offerMode, issuerMetadata, opts) {
const credentialOffer = createCredentialOfferObject(issuerMetadata, opts);
return createCredentialOfferURIFromObject(credentialOffer, offerMode, opts);
}
__name(createCredentialOfferURI, "createCredentialOfferURI");
var isPreAuthorizedCodeExpired = /* @__PURE__ */ __name((state, expirationDurationInSeconds) => {
const now = +/* @__PURE__ */ new Date();
const expirationTime = state.createdAt + expirationDurationInSeconds * 1e3;
return now >= expirationTime;
}, "isPreAuthorizedCodeExpired");
var assertValidPinNumber = /* @__PURE__ */ __name((pin, pinLength) => {
if (pin && !RegExp(`[\\d\\D]{${pinLength ?? 6}}`).test(pin)) {
throw Error(`${PIN_NOT_MATCH_ERROR}`);
}
}, "assertValidPinNumber");
var generateCredentialIdentifiers = /* @__PURE__ */ __name((authDetail, session) => {
if (typeof authDetail === "string") {
return [
uuidv4()
];
}
const identifiers = [];
if (authDetail.credential_configuration_id) {
const configId = authDetail.credential_configuration_id;
const hasConfig = session.credentialOffer.credential_offer.credential_configuration_ids?.includes(configId);
if (hasConfig) {
identifiers.push(`${configId}_${Date.now()}_${uuidv4()}`);
}
}
if (identifiers.length === 0 && authDetail.format) {
identifiers.push(`${authDetail.format}_${Date.now()}_${uuidv4()}`);
}
if (identifiers.length === 0) {
identifiers.push(uuidv4());
}
return identifiers;
}, "generateCredentialIdentifiers");
// lib/functions/ASOidcClient.ts
import { decodeJwt, decodeProtectedHeader } from "@sphereon/oid4vc-common";
import { oidcDiscoverIssuer, oidcGetClient } from "@sphereon/ssi-express-support";
function oidcAccessTokenVerifyCallback(opts) {
const clientMetadata = opts.clientMetadata ?? {
client_id: opts.credentialIssuer
};
return async (args) => {
const oidcIssuer = await oidcDiscoverIssuer({
issuerUrl: opts.authorizationServer
});
const oidcClient = await oidcGetClient(oidcIssuer.issuer, clientMetadata);
const introspection = await oidcClient.introspect(args.jwt);
if (!introspection.active) {
return Promise.reject(Error("Access token is not active or invalid"));
}
const jwt = {
header: decodeProtectedHeader(args.jwt),
payload: decodeJwt(args.jwt)
};
return {
jwt,
alg: jwt.header.alg,
...jwt.header.jwk && {
jwk: jwt.header.jwk
},
...jwt.header.x5c && {
x5c: jwt.header.x5c
},
...jwt.header.kid && {
kid: jwt.header.kid
}
};
};
}
__name(oidcAccessTokenVerifyCallback, "oidcAccessTokenVerifyCallback");
// lib/state-manager/MemoryStates.ts
import { STATE_MISSING_ERROR } from "@sphereon/oid4vci-common";
var MemoryStates = class {
static {
__name(this, "MemoryStates");
}
expiresInMS;
states;
cleanupIntervalId;
constructor(opts) {
this.expiresInMS = opts?.expiresInSec !== void 0 ? opts?.expiresInSec * 1e3 : 18e4;
this.states = /* @__PURE__ */ new Map();
}
async clearAll() {
this.states.clear();
}
async clearExpired(timestamp) {
const states = Array.from(this.states.entries());
const ts = timestamp ?? +/* @__PURE__ */ new Date();
for (const [id, state] of states) {
if (state.expiresAt && state.expiresAt < ts) {
this.states.delete(id);
} else if (!state.expiresAt) {
if (state.createdAt + this.expiresInMS < ts) {
this.states.delete(id);
}
}
}
}
async delete(id) {
if (!id) {
throw Error("No id supplied");
}
return this.states.delete(id);
}
async getAsserted(id) {
if (!id) {
throw Error("No id supplied");
}
let result;
if (await this.has(id)) {
result = await this.get(id);
}
if (!result) {
throw new Error(STATE_MISSING_ERROR + ` (${id})`);
}
return result;
}
async get(id) {
return this.states.get(id);
}
async has(id) {
if (!id) {
throw Error("No id supplied");
}
return this.states.has(id);
}
async set(id, stateValue) {
if (!id) {
throw Error("No id supplied");
}
this.states.set(id, stateValue);
}
async startCleanupRoutine(timeout) {
if (!this.cleanupIntervalId) {
this.cleanupIntervalId = setInterval(() => this.clearExpired(), timeout ?? 3e4);
}
}
async stopCleanupRoutine() {
if (this.cleanupIntervalId) {
clearInterval(this.cleanupIntervalId);
}
}
};
// lib/state-manager/LookupStateManager.ts
async function lookupStateManagerMultiGetAsserted(args) {
const value = await lookupStateManagerMultiGet(args);
if (value) {
return value;
}
return Promise.reject(Error(`no value found for id ${args.id}`));
}
__name(lookupStateManagerMultiGetAsserted, "lookupStateManagerMultiGetAsserted");
async function lookupStateManagerMultiGet({ id, lookups, keyValueMapper, valueStateManager }) {
for (const lookup of lookups) {
try {
const value = await new LookupStateManager(keyValueMapper, valueStateManager, lookup).get(id);
if (value) {
return value;
}
} catch (e) {
}
}
return valueStateManager.get(id);
}
__name(lookupStateManagerMultiGet, "lookupStateManagerMultiGet");
var LookupStateManager = class {
static {
__name(this, "LookupStateManager");
}
keyValueMapper;
valueStateManager;
lookup;
constructor(keyValueMapper, valueStateManager, lookup) {
this.keyValueMapper = keyValueMapper;
this.valueStateManager = valueStateManager;
this.lookup = lookup;
}
startCleanupRoutine(timeout) {
this.keyValueMapper.startCleanupRoutine(timeout);
return this.valueStateManager.startCleanupRoutine(timeout);
}
stopCleanupRoutine() {
this.keyValueMapper.stopCleanupRoutine();
return this.valueStateManager.stopCleanupRoutine();
}
async clearAll() {
this.keyValueMapper.clearAll();
this.valueStateManager.clearAll();
}
async clearExpired(timestamp) {
this.keyValueMapper.clearExpired(timestamp);
this.valueStateManager.clearExpired(timestamp);
}
async assertedValueId(key) {
const prop = this.lookup;
const valueId = await this.keyValueMapper.getAsserted(key).then((keyState) => keyState && prop in keyState ? keyState[prop] : void 0);
if (typeof valueId !== "string") {
throw Error("no value id could be derived for key" + key);
}
return valueId;
}
async valueId(key) {
const prop = this.lookup;
return await this.keyValueMapper.get(key).then((keyState) => keyState && prop in keyState ? keyState[prop] : void 0);
}
async delete(id) {
return await this.assertedValueId(id).then(async (value) => {
await this.keyValueMapper.delete(id);
return await this.valueStateManager.delete(value);
});
}
async get(id) {
return this.valueId(id).then((value) => value ? this.valueStateManager.get(value) : void 0);
}
async has(id) {
return this.valueId(id).then((value) => value ? this.valueStateManager.has(value) : false);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async set(_id, _stateValue) {
throw Error(`Please use the setMappedMethod that accepts both and id, value and object`);
}
async setMapped(valueKey, keyObject, stateValue) {
const keys = keyObject;
if (!(this.lookup in keys) || !keys[this.lookup]) {
return Promise.reject(new Error(`keyValue ${keyObject} does not contain the lookup property ${this.lookup}`));
}
const key = keys[this.lookup];
await this.keyValueMapper.set(key, keyObject);
await this.valueStateManager.set(valueKey, stateValue);
}
async getAsserted(id) {
return this.assertedValueId(id).then((value) => this.valueStateManager.getAsserted(value));
}
};
// lib/state-manager/CredentialOfferStateBuilder.ts
var CredentialOfferStateBuilder = class {
static {
__name(this, "CredentialOfferStateBuilder");
}
credentialOfferState;
constructor() {
this.credentialOfferState = {};
}
credentialOffer(credentialOffer) {
this.credentialOfferState.credentialOffer = credentialOffer;
return this;
}
createdAt(timestamp) {
this.credentialOfferState.createdAt = timestamp;
return this;
}
build() {
if (!this.credentialOfferState.createdAt) {
this.credentialOfferState.createdAt = +/* @__PURE__ */ new Date();
}
if (!this.credentialOfferState.credentialOffer) {
throw new Error("Not all properties are present to build an IssuerState object");
}
return this.credentialOfferState;
}
};
// lib/VcIssuer.ts
var shortUUID = ShortUUID();
var VcIssuer = class {
static {
__name(this, "VcIssuer");
}
_issuerMetadata;
_authorizationServerMetadata;
_defaultCredentialOfferBaseUri;
_credentialSignerCallback;
_jwtVerifyCallback;
_credentialDataSupplier;
_credentialOfferSessions;
_cNonces;
_uris;
_cNonceExpiresIn;
_asClientOpts;
constructor(issuerMetadata, authorizationServerMetadata, args) {
this._issuerMetadata = issuerMetadata;
this._authorizationServerMetadata = authorizationServerMetadata;
this._defaultCredentialOfferBaseUri = args.defaultCredentialOfferBaseUri;
this._credentialOfferSessions = args.credentialOfferSessions ?? new MemoryStates();
this._uris = args.uris ?? new MemoryStates();
this._cNonces = args.cNonces;
this._credentialSignerCallback = args?.credentialSignerCallback;
this._jwtVerifyCallback = args?.jwtVerifyCallback;
this._credentialDataSupplier = args?.credentialDataSupplier;
this._cNonceExpiresIn = args?.cNonceExpiresIn ?? (process.env.C_NONCE_EXPIRES_IN ? parseInt(process.env.C_NONCE_EXPIRES_IN) : 300);
this._asClientOpts = args?.asClientOpts;
}
async getCredentialOfferSessionById(id, lookups = [
"preAuthorizedCode",
"issuerState",
"correlationId"
]) {
if (Array.isArray(lookups) && lookups.length > 0) {
if (!this.uris) {
return Promise.reject(Error("Cannot lookup credential offer by id if URI state manager is not set"));
}
return lookupStateManagerMultiGetAsserted({
id,
keyValueMapper: this._uris,
valueStateManager: this._credentialOfferSessions,
lookups: [
"preAuthorizedCode",
"issuerState",
"correlationId"
]
});
}
const session = await this._credentialOfferSessions.get(id);
if (!session) {
return Promise.reject(Error(`No session found for id ${id}`));
}
return session;
}
async deleteCredentialOfferSessionById(id, lookups = [
"preAuthorizedCode",
"issuerState"
]) {
const session = await this.getCredentialOfferSessionById(id, lookups);
if (session) {
if (session.preAuthorizedCode && await this._credentialOfferSessions.has(session.preAuthorizedCode)) {
await this._credentialOfferSessions.delete(session.preAuthorizedCode);
}
if (session.issuerState && await this._credentialOfferSessions.has(session.issuerState)) {
await this._credentialOfferSessions.delete(session.issuerState);
}
}
return session;
}
async processNotification({ preAuthorizedCode, issuerState, notification }) {
const sessionId = preAuthorizedCode ?? issuerState;
const session = sessionId ? await this.getCredentialOfferSessionById(sessionId) : void 0;
if (!session || !sessionId) {
LOG.error(`No session or session id found ${sessionId}`);
return Error("invalid_notification_request");
}
if (notification.notification_id !== session.notification_id) {
LOG.error(`Notification id ${notification.notification_id} not found in session. session notification id ${session.notification_id}`);
return Error("invalid_notification_id");
} else if (session.notification) {
LOG.info(`Overwriting existing notification, as a new notification came in ${session.notification_id}`);
}
await this.updateSession({
preAuthorizedCode,
issuerState,
notification
});
LOG.info(`Processed notification ${notification} for ${session.notification_id}`);
return session;
}
async createCredentialOfferURI(opts) {
const { offerMode = "VALUE", correlationId = shortUUID.generate(), credential_configuration_ids, statusListOpts, credentialOfferUri, redirectUri } = opts;
if (offerMode === "REFERENCE" && !credentialOfferUri) {
return Promise.reject(Error("credentialOfferUri must be supplied for offerMode REFERENCE!"));
}
const grants = opts.grants ? {
...opts.grants
} : {};
if (opts.pinLength !== void 0) {
if (grants[PRE_AUTH_GRANT_LITERAL2]) {
grants[PRE_AUTH_GRANT_LITERAL2].tx_code = {
...grants[PRE_AUTH_GRANT_LITERAL2].tx_code,
length: grants[PRE_AUTH_GRANT_LITERAL2].tx_code?.length ?? opts.pinLength
};
}
}
if (grants[PRE_AUTH_GRANT_LITERAL2]?.tx_code && !grants[PRE_AUTH_GRANT_LITERAL2]?.tx_code?.length) {
grants[PRE_AUTH_GRANT_LITERAL2].tx_code.length = 4;
}
const baseUri = opts?.baseUri ?? this.defaultCredentialOfferBaseUri;
const credentialOfferObject = createCredentialOfferObject(this._issuerMetadata, {
...opts,
grants,
credentialOffer: credential_configuration_ids ? {
credential_issuer: this._issuerMetadata.credential_issuer,
credential_configuration_ids
} : void 0
});
const preAuthGrant = credentialOfferObject.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL2];
const authGrant = credentialOfferObject.credential_offer.grants?.authorization_code;
const preAuthorizedCode = preAuthGrant?.["pre-authorized_code"];
const issuerState = authGrant?.issuer_state;
const txCode = preAuthGrant?.tx_code;
let userPin;
if (preAuthGrant?.tx_code) {
const pinLength = preAuthGrant.tx_code.length ?? 4;
userPin = ("" + Math.round((Math.pow(10, pinLength) - 1) * Math.random())).padStart(pinLength, "0");
assertValidPinNumber(userPin, pinLength);
}
const createdAt = +/* @__PURE__ */ new Date();
const lastUpdatedAt = createdAt;
const expirationInMs = (opts.sessionLifeTimeInSec ?? 10 * 60) * 1e3;
const expiresAt = createdAt + Math.abs(expirationInMs);
if (offerMode === "REFERENCE") {
if (!this.uris) {
throw Error("No URI state manager set, whilst apparently credential offer by reference is being used");
}
const offerUri = opts.credentialOfferUri?.replace(":id", correlationId);
if (!offerUri) {
return Promise.reject(Error("credentialOfferUri must be supplied for offerMode REFERENCE!"));
}
credentialOfferObject.credential_offer_uri = offerUri;
await this.uris.set(correlationId, {
uri: offerUri,
createdAt,
expiresAt,
preAuthorizedCode,
issuerState,
correlationId
});
}
const credentialOffer = await toUniformCredentialOfferRequest({
credential_offer: credentialOfferObject.credential_offer,
credential_offer_uri: credentialOfferObject.credential_offer_uri
}, {
version: OpenId4VCIVersion.VER_1_0_15,
resolve: false
});
const status = IssueStatus.OFFER_CREATED;
const session = {
redirectUri,
preAuthorizedCode,
issuerState,
createdAt,
lastUpdatedAt,
expiresAt,
status,
notification_id: uuidv42(),
...opts.client_id && {
clientId: opts.client_id
},
...userPin && {
txCode: userPin
},
...opts.credentialDataSupplierInput && {
credentialDataSupplierInput: opts.credentialDataSupplierInput
},
credentialOffer,
statusLists: statusListOpts
};
const uri = createCredentialOfferURIFromObject(credentialOffer, offerMode, {
...opts,
baseUri
});
if (preAuthorizedCode) {
const lookupManager = new LookupStateManager(this.uris, this._credentialOfferSessions, "correlationId");
await lookupManager.setMapped(preAuthorizedCode, {
preAuthorizedCode,
uri,
createdAt,
expiresAt,
correlationId,
issuerState
}, session);
}
if (issuerState) {
const lookupManager = new LookupStateManager(this.uris, this._credentialOfferSessions, "correlationId");
await lookupManager.setMapped(issuerState, {
preAuthorizedCode,
uri,
createdAt,
expiresAt,
correlationId,
issuerState
}, session);
}
let qrCodeDataUri;
if (opts.qrCodeOpts) {
const { AwesomeQR } = await import("awesome-qr");
const qrCode = new AwesomeQR({
...opts.qrCodeOpts,
text: uri
});
qrCodeDataUri = `data:image/png;base64,${(await qrCode.draw()).toString("base64")}`;
}
const credentialOfferResult = {
session,
uri,
qrCodeDataUri,
correlationId,
txCode,
...userPin !== void 0 && {
userPin,
pinLength: userPin?.length ?? 0
}
};
EVENTS.emit(CredentialOfferEventNames.OID4VCI_OFFER_CREATED, {
eventName: CredentialOfferEventNames.OID4VCI_OFFER_CREATED,
id: correlationId,
data: credentialOfferResult,
initiator: "<Unknown>",
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
issuer: this.issuerMetadata.credential_issuer,
subsystem: SubSystem.API,
createdAt,
expiresAt
});
return credentialOfferResult;
}
/**
* issueCredentialFromIssueRequest
* @param opts issuerRequestParams
* - issueCredentialsRequest the credential request
* - issuerState the state of the issuer
* - jwtVerifyCallback callback that verifies the Proof of Possession JWT
* - issuerCallback callback to issue a Verifiable Credential
* - cNonce an existing c_nonce
*/
async issueCredential(opts) {
const credentialRequest = opts.credentialRequest;
const issuerCorrelation = opts.issuerCorrelation;
try {
if (!("credential_identifier" in credentialRequest) && !("credential_configuration_id" in credentialRequest)) {
throw Error("credential request should have either credential_identifier or credential_configuration_id");
}
if ("credential_configuration_id" in credentialRequest && credentialRequest.credential_configuration_id) {
if (!this._issuerMetadata.credential_configurations_supported?.[credentialRequest.credential_configuration_id]) {
throw Error(TokenErrorResponse2.invalid_request);
}
}
if ("credential_identifier" in credentialRequest && credentialRequest.credential_identifier && issuerCorrelation.authorizationDetails) {
const validIdentifiers = issuerCorrelation.authorizationDetails.flatMap((detail) => detail.credential_identifiers || []);
if (!validIdentifiers.includes(credentialRequest.credential_identifier)) {
throw Error("credential_identifier not found in authorization_details");
}
}
let format = this.lookupCredentialFormat(credentialRequest);
const validated = await this.validateCredentialRequestProof({
...opts,
format,
tokenExpiresIn: opts.tokenExpiresIn ?? 180
});
if (validated.preAuthorizedCode && !issuerCorrelation.preAuthorizedCode) {
issuerCorrelation.preAuthorizedCode = validated.preAuthorizedCode;
}
if (validated.issuerState && !issuerCorrelation.issuerState) {
issuerCorrelation.issuerState = validated.issuerState;
}
const { preAuthSession, authSession, cNonceState, jwtVerifyResult } = validated;
const did = jwtVerifyResult.did;
const jwk = jwtVerifyResult.jwk;
const kid = jwtVerifyResult.kid;
const newcNonce = opts.newCNonce ? opts.newCNonce : uuidv42();
const newcNonceState = {
cNonce: newcNonce,
createdAt: +/* @__PURE__ */ new Date(),
...authSession?.issuerState && {
issuerState: authSession.issuerState
},
...preAuthSession && {
preAuthorizedCode: preAuthSession.preAuthorizedCode
}
};
await this.cNonces.set(newcNonce, newcNonceState);
if (!opts.credential && this._credentialDataSupplier === void 0 && opts.credentialDataSupplier === void 0) {
throw Error(`Either a credential needs to be supplied or a credentialDataSupplier`);
}
let credential;
let signerCallback = opts.credentialSignerCallback;
const session = issuerCorrelation.preAuthorizedCode && preAuthSession ? preAuthSession : authSession;
if (opts.credential) {
credential = opts.credential;
} else {
const credentialDataSupplier = typeof opts.credentialDataSupplier === "function" ? opts.credentialDataSupplier : this._credentialDataSupplier;
if (typeof credentialDataSupplier !== "function") {
throw Error("Data supplier is mandatory if no credential is supplied");
}
if (!session) {
throw Error("Either a preAuth or Auth session is required, none found");
}
const credentialOffer = session.credentialOffer;
if (!credentialOffer) {
throw Error("Credential Offer missing");
}
const credentialDataSupplierInput = opts.credentialDataSupplierInput ?? session.credentialDataSupplierInput;
const result = await credentialDataSupplier({
...cNonceState ? {
...cNonceState
} : {
...authSession
},
credentialRequest: opts.credentialRequest,
credentialSupplierConfig: this._issuerMetadata.credential_supplier_config,
format,
credentialOffer,
...credentialDataSupplierInput && {
credentialDataSupplierInput
}
});
credential = result.credential;
if (result.format) {
format = result.format;
}
if (typeof result.signCallback === "function") {
signerCallback = result.signCallback;
}
}
if (!credential) {
throw Error("A credential needs to be supplied at this point");
}
if (CredentialMapper.isSdJwtDecodedCredentialPayload(credential) && (kid || jwk) && !credential.cnf) {
if (kid) {
credential.cnf = {
kid
};
}
if (jwk) {
credential.cnf = {
jwk
};
}
} else if (did && !CredentialMapper.isSdJwtDecodedCredentialPayload(credential) && credential.credentialSubject !== void 0) {
const credentialSubjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [
credential.credentialSubject
];
credentialSubjects.map((subject) => {
if (!subject.id) {
subject.id = did;
}
return subject;
});
credential.credentialSubject = Array.isArray(credential.credentialSubject) ? credentialSubjects : credentialSubjects[0];
} else {
}
let issuer = void 0;
if (credential.iss) {
issuer = credential.iss;
} else if (credential.issuer) {
if (typeof credential.issuer === "string") {
issuer = credential.issuer;
} else if (typeof credential.issuer === "object" && "id" in credential.issuer && typeof credential.issuer.id === "string") {
issuer = credential.issuer.id;
}
}
const verifiableCredential = await this.issueCredentialImpl({
credentialRequest: opts.credentialRequest,
format,
credential,
jwtVerifyResult,
issuer,
...session && {
statusLists: session.statusLists
}
}, signerCallback);
if (!verifiableCredential) {
throw new Error(CREDENTIAL_MISSING_ERROR);
}
if (cNonceState) {
await this.cNonces.delete(cNonceState.cNonce);
}
let notification_id;
if (issuerCorrelation.preAuthorizedCode && preAuthSession) {
preAuthSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
preAuthSession.status = IssueStatus.CREDENTIAL_ISSUED;
notification_id = preAuthSession.notification_id;
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession);
} else if (issuerCorrelation.issuerState && authSession) {
authSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
authSession.status = IssueStatus.CREDENTIAL_ISSUED;
notification_id = authSession.notification_id;
await this._credentialOfferSessions.set(issuerCorrelation.issuerState, authSession);
}
const response = {
credentials: [
{
credential: verifiableCredential
}
],
// format: credentialRequest.format,
c_nonce: newcNonce,
c_nonce_expires_in: this._cNonceExpiresIn,
...notification_id && {
notification_id
}
};
const experimentalSubjectIssuance = opts.credentialRequest.credential_subject_issuance;
if (experimentalSubjectIssuance?.subject_proof_mode) {
if (experimentalSubjectIssuance.subject_proof_mode !== "proof_replace") {
throw Error("Only proof replace is supported currently");
}
response.transaction_id = authSession?.issuerState;
response.credential_subject_issuance = experimentalSubjectIssuance;
}
return response;
} catch (error) {
await this.updateSession({
preAuthorizedCode: issuerCorrelation.preAuthorizedCode,
issuerState: issuerCorrelation.issuerState,
error
});
throw error;
}
}
lookupCredentialFormat(credentialRequest) {
let format;
if ("credential_configuration_id" in credentialRequest && credentialRequest.credential_configuration_id) {
const credentialConfig = this._issuerMetadata.credential_configurations_supported?.[credentialRequest.credential_configuration_id];
format = credentialConfig?.format;
} else if ("credential_identifier" in credentialRequest && credentialRequest.credential_identifier) {
const credentialIdentifier = credentialRequest.credential_identifier;
const matchedConfig = Object.values(this._issuerMetadata.credential_configurations_supported || {}).find((config) => credentialIdentifier === config.id || credentialIdentifier === config.vct);
return matchedConfig?.format;
}
return format;
}
async updateSession({ preAuthorizedCode, error, issuerState, notification }) {
let issueState = void 0;
if (error) {
issueState = IssueStatus.ERROR;
} else if (notification) {
if (notification.event == "credential_accepted") {
issueState = IssueStatus.NOTIFICATION_CREDENTIAL_ACCEPTED;
} else if (notification.event == "credential_deleted") {
issueState = IssueStatus.NOTIFICATION_CREDENTIAL_DELETED;
} else if (notification.event == "credential_failure") {
issueState = IssueStatus.NOTIFICATION_CREDENTIAL_FAILURE;
}
}
if (preAuthorizedCode) {
const preAuthSession = await this._credentialOfferSessions.get(preAuthorizedCode);
if (preAuthSession) {
preAuthSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
if (issueState) {
preAuthSession.status = issueState;
}
if (error) {
preAuthSession.error = error instanceof Error ? error.message : error?.toString();
}
preAuthSession.notification_id;
if (notification) {
preAuthSession.notification = notification;
}
await this._credentialOfferSessions.set(preAuthorizedCode, preAuthSession);
}
}
if (issuerState) {
const authSession = await this._credentialOfferSessions.get(issuerState);
if (authSession) {
authSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
if (issueState) {
authSession.status = issueState;
}
if (error) {
authSession.error = error instanceof Error ? error.message : error?.toString();
}
if (notification) {
authSession.notification = notification;
}
await this._credentialOfferSessions.set(issuerState, authSession);
}
}
}
/*
private async retrieveGrantsAndCredentialOfferSession(id: string): Promise<{
clientId?: string;
grants?: Grant,
session: CredentialOfferSession
}> {
const session: CredentialOfferSession | undefined = await this._credentialOfferSessions.getAsserted(id)
const clientId = session?.clientId
const grants = session?.credentialOffer?.credential_offer?.grants
if (!grants?.authorization_code?.issuer_state && !grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL]) {
throw new Error(GRANTS_MUST_NOT_BE_UNDEFINED)
}
return { session, clientId, grants }
}*/
async validateCredentialRequestProof({ credentialRequest, issuerCorrelation, format, jwtVerifyCallback, tokenExpiresIn }) {
let issuerState;
const supportedIssuanceFormats = [
"jwt_vc_json",
"jwt_vc_json-ld",
"dc+sd-jwt",
"ldp_vc",
"mso_mdoc"
];
try {
if (format && !supportedIssuanceFormats.includes(format)) {
throw Error(`Format ${format} not supported yet`);
}
const verifyFn = jwtVerifyCallback ?? this._jwtVerifyCallback;
if (typeof verifyFn !== "function") {
throw Error(JWT_VERIFY_CONFIG_ERROR);
}
const credReq = credentialRequest;
if (credReq.proof && credReq.proofs) {
throw Error("Credential request may not contain both proof and proofs parameters");
}
const proofCandidates = [];
if (credReq.proof) {
proofCandidates.push(credReq.proof);
} else if (credReq.proofs) {
if (Array.isArray(credReq.proofs.jwt)) {
for (const jwtProof of credReq.proofs.jwt) {
if (typeof jwtProof === "string") {
proofCandidates.push({
proof_type: "jwt",
jwt: jwtProof
});
} else if (jwtProof && typeof jwtProof === "object" && "jwt" in jwtProof) {
proofCandidates.push(jwtProof);
}
}
}
if (proofCandidates.length === 0) {
const availableTypes = Object.keys(credReq.proofs).join(", ");
throw Error(`No supported proof types found in request. Available: [${availableTypes}]`);
}
} else {
throw Error("Proof of possession is required. No proof or proofs value present in credential request");
}
let jwtVerifyResult;
const validationErrors = [];
for (const proof of proofCandidates) {
try {
jwtVerifyResult = await verifyFn({
jwt: proof.jwt
});
break;
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
validationErrors.push(msg);
}
}
if (!jwtVerifyResult) {
throw Error(`Unable to verify any provided proofs. Errors: ${validationErrors.join("; ")}`);
}
const { didDocument, did, jwt } = jwtVerifyResult;
const { header, payload } = jwt;
const { iss, aud, iat, nonce } = payload;
const issuer_state = "issuer_state" in credentialRequest && credentialRequest.issuer_state ? credentialRequest.issuer_state : issuerCorrelation.issuerState;
if (!nonce && !issuer_state) {
throw Error("No nonce or issuer_state was found in the Proof of Possession");
}
let createdAt = +/* @__PURE__ */ new Date();
let cNonceState;
if (nonce) {
cNonceState = await this.cNonces.getAsserted(nonce);
createdAt = cNonceState.createdAt;
}
if (issuer_state) {
const session = await this._credentialOfferSessions.getAsserted(issuer_state);
issuerState = issuer_state;
createdAt = session.createdAt;
}
const alg = jwtVerifyResult.alg ?? header.alg;
const kid = jwtVerifyResult.kid ?? header.kid;
const jwk = jwtVerifyResult.jwk ?? header.jwk;
const x5c = jwtVerifyResult.x5c ?? header.x5c;
const typ = header.typ;
if (typ !== "openid4vci-proof+jwt") {
throw Error(TYP_ERROR);
} else if (!alg) {
throw Error(ALG_ERROR);
} else if (x5c && (kid || jwk)) {
throw Error(KID_JWK_X5C_ERROR);
} else if (kid && !did) {
if (!jwk && !x5c) {
throw Error(KID_DID_NO_DID_ERROR);
} else {
console.log(`KID present but no DID, using JWK or x5c`);
}
} else if (did && !didDocument) {
throw Error(DID_NO_DIDDOC_ERROR);
}
const preAuthSession = issuerCorrelation.preAuthorizedCode ? await this.credentialOfferSessions.get(issuerCorrelation.preAuthorizedCode) : void 0;
const authSession = issuerState ? await this.credentialOfferSessions.get(issuerState) : void 0;
if (!preAuthSession && !authSession) {
throw Error("Either a pre-authorized code or issuer state needs to be present");
}
if (preAuthSession) {
if (!preAuthSession.preAuthorizedCode || preAuthSession.preAuthorizedCode !== issuerCorrelation.preAuthorizedCode) {
throw Error("Invalid pre-authorized code");
}
preAuthSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
preAuthSession.status = IssueStatus.CREDENTIAL_REQUEST_RECEIVED;
await this._credentialOfferSessions.set(issuerCorrelation.preAuthorizedCode, preAuthSession);
}
if (authSession) {
if (!authSession.issuerState || authSession.issuerState !== issuerState) {
throw Error("Invalid issuer state");
}
authSession.lastUpdatedAt = +/* @__PURE__ */ new Date();
authSession.status = IssueStatus.CREDENTIAL_REQUEST_RECEIVED;
}
if (!iss && authSession?.credentialOffer.credential_offer?.grants?.authorization_code) {
throw new Error(NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT);
}
if (!aud || aud !== this._issuerMetadata.credential_issuer) {
throw new Error(AUD_ERROR);
}
if (!iat) {
throw new Error(IAT_ERROR);
} else if (iat > Math.round(createdAt / 1e3) + tokenExpiresIn) {
throw new Error(IAT_ERROR);
}
return {
jwtVerifyResult,
preAuthorizedCode: issuerCorrelation.preAuthorizedCode,
preAuthSession,
issuerState,
authSession,
cNonceState
};
} catch (error) {
await this.updateSession({
preAuthorizedCode: issuerCorrelation.preAuthorizedCode,
issuerState,
error
});
throw error;
}
}
async issueCredentialImpl(opts, issuerCallback) {
if (!opts.credential && !opts.credentialRequest || !this._credentialSignerCallback) {
throw new Error(ISSUER_CONFIG_ERROR);
}
const credential = issuerCallback ? await issuerCallback(opts) : await this._credentialSignerCallback(opts);
EVENTS.emit(CredentialEventNames.OID4VCI_CREDENTIAL_ISSUED, {
eventName: CredentialEventNames.OID4VCI_CREDENTIAL_ISSUED,
id: uuidv42(),
data: credential,
// TODO: Format, request etc
initiator: opts.issuer ?? "<unknown>",
initiatorType: InitiatorType.EXTERNAL,
system: System.OID4VCI,
subsystem: SubSystem.VC_ISSUER
});
return credential;
}
get credentialSignerCallback() {
return this._credentialSignerCallback;
}
get jwtVerifyCallback() {
return this._jwtVerifyCallback;
}
get credentialDataSupplier() {
return this._credentialDataSupplier;
}
get uris() {
return this._uris;
}
get cNonceExpiresIn() {
return this._cNonceExpiresIn;
}
get credentialOfferSessions() {
return this._credentialOfferSessions;
}
get cNonces() {
return this._cNonces;
}
get defaultCredentialOfferBaseUri() {
return this._defaultCredentialOfferBaseUri;
}
get issuerMetadata() {
return this._issuerMetadata;
}
// TODO SSISDK-87 create proper solution to update issuer metadata
set issuerMetadata(value) {
this._issuerMetadata = value;
}
get authorizationServerMetadata() {
return this._authorizationServerMetadata;
}
get asClientOpts() {
return this._asClientOpts;
}
};
// lib/builder/VcIssuerBuilder.ts
var VcIssuerBuilder = class {
static {
__name(this, "VcIssuerBuilder");
}
issuerMetadataBuilder;
issuerMetadata = {};
authorizationServerMetadata = {};
asClientOpts;
txCode;
defaultCredentialOfferBaseUri;
userPinRequired;
cNonceExpiresIn;
credentialOfferStateManager;
credentialOfferURIManager;
cNonceStateManager;
credentialSignerCallback;
jwtVerifyCallback;
credentialDataSupplier;
withIssuerMetadata(issuerMetadata) {
if (!issuerMetadata.credential_configurations_supported) {
throw new Error("IssuerMetadata should be from type v1_0_15 or higher.");
}
this.issuerMetadata = issuerMetadata;
return this;
}
withASClientMetadata(clientMetadata) {
this.asClientOpts = clientMetadata;
return this;
}
withASClientMetadataParams({ client_id, client_secret, redirect_uris, response_types, ...other }) {
this.asClientOpts = {
...other,
client_id,
client_secret,
redirect_uris,
response_types
};
return this;
}
withAuthorizationMetadata(authorizationServerMetadata) {
this.authorizationServerMetadata = authorizationServerMetadata;
return this;
}
withIssuerMetadataBuilder(builder) {
this.issuerMetadataBuilder = builder;
return this;
}
withDefaultCredentialOfferBaseUri(baseUri) {
this.defaultCredentialOfferBaseUri = baseUri;
return this;
}
withCredentialIssuer(issuer) {
this.issuerMetadata.credential_issuer = issuer;
return this;
}
withAuthorizationServers(authorizationServers) {
this.issuerMetadata.authorization_servers = typeof authorizationServers === "string" ? [
authorizationServers
] : authorizationServers;
return this;
}
withCredentialEndpoint(credentialEndpoint) {
this.issuerMetadata.credential_endpoint = credentialEndpoint;
return this;
}
withTokenEndpoint(tokenEndpoint) {
this.issuerMetadata.token_endpoint = tokenEndpoint;
return this;
}
withNonceEndpoint(nonceEndpoint) {
this.issuerMetadata.nonce_endpoint = nonceEndpoint;
return this;
}
withIssuerDisplay(issuerDisplay) {
this.issuerMetadata.display = Array.isArray(issuerDisplay) ? issuerDisplay : [
issuerDisplay
];
return this;
}
addIssuerDisplay(issuerDisplay) {
this.issuerMetadata.display = [
...this.issuerMetadata.display ?? [],
issuerDisplay
];
return this;
}
withCredentialConfigurationsSupported(credentialConfigurationsSupported) {
this.issuerMetadata.credential_configurations_supported = credentialConfigurationsSupported;
return this;
}
addCredentialConfigurationsSupported(id, supportedCredential) {
if (!this.issuerMetadata.credential_configurations_supported) {
this.issuerMetadata.credential_configurations_supported = {};
}
this.issuerMetadata.credential_configurations_supported[id] = supportedCredential;
return this;
}
withTXCode(txCode) {
this.txCode = txCode;
return this;
}
withCredentialOfferURIStateManager(credentialOfferURIManager) {
this.credentialOfferURIManager = credentialOfferURIManager;
retur