diff --git a/.data/basicIdentity.claims.example.json b/.data/basicIdentity.claims.example.json new file mode 100644 index 0000000..9faab11 --- /dev/null +++ b/.data/basicIdentity.claims.example.json @@ -0,0 +1,5 @@ +{ + "given_name": "Ken", + "family_name": "Tanaka", + "birthdate": "1979-02-09" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a263b7..efc40e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ +## [1.3.2] + +add [-v --verbose] flags to `claim` - to print out credential to terminal +Add Support for `organisation-wallet` version `0.0.10` +Drop Support for `organisation-wallet` version `<=0.0.9` + ## [1.3.1] (2023.12.18) Add support to call `claim` and `present` commands with `--url` or `-u` flag to accept url from stdin +## [1.3.0] (2023.12.18) + +new format of payload for `/credential` endpoint +Add error messages on failing to fetch credential-offer or claiming credential + ## [1.2.1] (2023.12.15) Bugfix: trim credential-offer and presentation-request input files diff --git a/README.md b/README.md index 4972dd4..ad007d5 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,43 @@ -meeco-wallet-cli -================= +# meeco-wallet-cli -* [Configuration](#configuration) -* [Usage](#usage) -* [Commands](#commands) + +- [Configuration](#configuration) +- [Usage](#usage) +- [Commands](#commands) + # Configuration ## Holder Details + ``` # config/holder.json uri: # Holder identifier to be attached to the credential / presentation jwk: # private secp256r1 Key as JWK ``` + # Usage + + ```sh-session $ npm install $ chmod +x ./bin/dev.js $ ./bin/dev.js COMMAND ``` + + # Commands + + - [meeco-wallet-cli](#meeco-wallet-cli) - [Configuration](#configuration) - [Holder Details](#holder-details) @@ -46,11 +55,12 @@ Claim Credential Offer ``` USAGE - $ ./bin/dev.js claim [-f ] + $ ./bin/dev.js claim [-f ] [-v] FLAGS -f, --file= credential offer filename in ".data" folder -u, --url= direct URL for the credential offer + -v, --verbose Print out credential at end of command DESCRIPTION Issue a credential by claiming the provided Credential Offer. @@ -90,7 +100,8 @@ EXAMPLE FILES ``` # Meeco Organisation Wallet -Compatible version `<=0.0.8` + +Compatible version `>=0.0.10` ## `meeco-wallet-cli create-credential-offer` @@ -133,4 +144,4 @@ ARGUMENTS DESCRIPTION Create a Presentation Request. Will prompt for a filename to save the created Presentation Request URI. -``` \ No newline at end of file +``` diff --git a/package.json b/package.json index fa2c010..95d3967 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "prepare": "npm run build", "version": "oclif readme && git add README.md" }, - "version": "1.3.1", + "version": "1.3.2", "keywords": [ "oclif", "sd-jwt", diff --git a/src/commands/claim/claim.ts b/src/commands/claim/claim.ts index 989bdfc..9dac108 100644 --- a/src/commands/claim/claim.ts +++ b/src/commands/claim/claim.ts @@ -26,6 +26,10 @@ export default class Claim extends Command { char: 'u', description: 'direct URL for the credential offer', }), + verbose: Flags.boolean({ + char: 'v', + description: 'Print out credential at end of command', + }) }; async run(): Promise { @@ -49,7 +53,18 @@ export default class Claim extends Command { } const verifiableCredential = await claimCredentialOffer(credentialOfferURI); + + if (!verifiableCredential) { + return; + } + const vcFilename = await prompt('Save Credential as', { default: prependTS('credential.jwt') }); await writeFile(`${DATA_FOLDER}/${vcFilename}`, verifiableCredential); + + if (flags.verbose) { + console.log("=============================="); + console.log(verifiableCredential); + console.log("=============================="); + } } } diff --git a/src/commands/create-credential-offer/create-credential-offer.ts b/src/commands/create-credential-offer/create-credential-offer.ts index 86cdd63..6a285c5 100644 --- a/src/commands/create-credential-offer/create-credential-offer.ts +++ b/src/commands/create-credential-offer/create-credential-offer.ts @@ -69,12 +69,20 @@ export default class CreateCredentialOffer extends Command { const claims = await readFile(claimsFile).then((data) => JSON.parse(data.toString())); + const credentialInformation = selectedCredential.credentialIdentifier + ? { + credentialIdentifiers: [selectedCredential.credentialIdentifier] + } + : { + format: selectedCredential.format, + types: selectedCredential.types, + } + const response = await createCredentialOffer({ + ...credentialInformation, claims, - format: selectedCredential.format, grantType: selectedGrantType, pinRequired, - types: selectedCredential.types, url: args.url, }).catch((error) => { this.log('Failed to Create Credential offer:', error.message); diff --git a/src/types/create-credential-offer.types.ts b/src/types/create-credential-offer.types.ts index 44dffc0..4b037d6 100644 --- a/src/types/create-credential-offer.types.ts +++ b/src/types/create-credential-offer.types.ts @@ -10,6 +10,7 @@ export type CredentialMetadata = { } export type Credential = { + credentialIdentifier: string; format: string; id: string; name: string; diff --git a/src/types/openid.types.ts b/src/types/openid.types.ts index 344b3df..c50792e 100644 --- a/src/types/openid.types.ts +++ b/src/types/openid.types.ts @@ -25,11 +25,17 @@ export type SupportedCredential = { types?: string[]; } +export type SupportedCredentialMap = { + [identifier: string]: SupportedCredential; +} + +export type CredentialOfferDetails = string[]; + export type IssuerMetadata = { [metadata: string]: unknown; authorization_endpoint?: string; // TODO: remove when no longer supporting org-wallet < 0.0.8 credential_endpoint: string; - credentials_supported: SupportedCredential[]; + credentials_supported: SupportedCredential[] | SupportedCredentialMap; grant_types_supported?: string[]; // TODO: remove when no longer supporting org-wallet < 0.0.8 issuer: string; pushed_authorization_endpoint?: string; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 6c35bf8..295a9b6 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -25,6 +25,17 @@ export async function printFetchError(res: Response, message = 'HTTP request fai console.log(`Response: ${body}`); } +export async function parseFetchResponse(res: Response) { + if (res.status === 200) { + return res.json(); + } + + console.log(`==============================================`); + console.log(`Status code: ${res.status}`); + console.log(await res.text()); + console.log(`==============================================`); +} + export function isVcSdJwt({ format }: { format: string }) { return format === JWT_TYPE.VC_SD_JWT; } \ No newline at end of file diff --git a/src/utils/meeco-org-wallet/credential-offer.ts b/src/utils/meeco-org-wallet/credential-offer.ts index 2457292..8107b69 100644 --- a/src/utils/meeco-org-wallet/credential-offer.ts +++ b/src/utils/meeco-org-wallet/credential-offer.ts @@ -1,19 +1,21 @@ -import { generateRandomCode } from '../helpers.js'; import { GRANT_TYPES } from '../../types/openid.types.js'; +import { generateRandomCode, parseFetchResponse } from '../helpers.js'; import { CREDENTIAL_OFFER_ENDPOINT } from './constants.js'; export type createCredentialOfferArgs = { claims: unknown; - format: string; + credentialIdentifiers?: string[]; + format?: string; grantType: string; pinRequired: boolean; - types: string[]; + types?: string[]; url: string; userPin?: string; } export async function createCredentialOffer({ claims, + credentialIdentifiers, format, grantType, pinRequired, @@ -40,16 +42,13 @@ export async function createCredentialOffer({ }; } + const credentials = credentialIdentifiers ?? [ { format, types } ]; + const payload = { credentialDataSupplierInput: { claims, }, - credentials: [ - { - format, - types, - }, - ], + credentials, grants, } @@ -60,11 +59,5 @@ export async function createCredentialOffer({ }, method: 'POST', }) - .then((res) => { - if (res.status !== 200) { - throw new Error(res.statusText); - } - - return res.json() - }); + .then((res) => parseFetchResponse(res)); } \ No newline at end of file diff --git a/src/utils/openid/vci.ts b/src/utils/openid/vci.ts index 2dcf92d..3db4067 100644 --- a/src/utils/openid/vci.ts +++ b/src/utils/openid/vci.ts @@ -4,8 +4,8 @@ import { randomUUID } from 'node:crypto'; import { TokenSet } from 'openid-client'; import { Credential, CredentialMetadata } from '../../types/create-credential-offer.types.js'; -import { GRANT_TYPES, IssuerMetadata, WELL_KNOWN } from '../../types/openid.types.js'; -import { isVcSdJwt, printFetchError } from '../helpers.js'; +import { CredentialOfferDetails, GRANT_TYPES, IssuerMetadata, SupportedCredentialMap, WELL_KNOWN } from '../../types/openid.types.js'; +import { isVcSdJwt, parseFetchResponse, printFetchError } from '../helpers.js'; import { getOpenidConfiguration } from './openid-config.js'; import { getTokenFromAuthorizationCode } from './vci.auth-code.js'; import { getJwtVcJsonProof, getSdJwtVcJsonProof } from './vci.proof-jwt.js'; @@ -28,10 +28,17 @@ type CredentialChoice = { value: Credential; } -function parseSupportedCredential(credential: CredentialMetadata): CredentialChoice { +class CredentialOfferError extends Error { + constructor(message = "") { + super(message); + } +} + +function parseSupportedCredential(credentialIdentifier: string, credential: CredentialMetadata): CredentialChoice { const credentialTypes = credential.types ?? credential.credential_definition.types ?? [credential.credential_definition.vct as string]; const vc: Credential = { + credentialIdentifier, format: credential.format, id: credential.id, name: credential.display[0].name, @@ -48,13 +55,13 @@ function parseSupportedCredential(credential: CredentialMetadata): CredentialCho export function getCredentialsSupportedAsChoices(metadata: Array | supportedCredentialMetadata): CredentialChoice[] { if (Array.isArray(metadata)) { - return metadata.map((credential) => parseSupportedCredential(credential)); + return metadata.map((credential) => parseSupportedCredential('', credential)); } if (typeof metadata === 'object') { return Object.keys(metadata).map((key) => { const credentialMetadata = (metadata as supportedCredentialMetadata)[key]; - return parseSupportedCredential(credentialMetadata); + return parseSupportedCredential(key, credentialMetadata); }); } @@ -78,14 +85,15 @@ export async function claimCredentialOffer(credentialOfferURL: string) { ux.action.stop(); const { credential_issuer: issuer, credentials, grants } = result; - const metadata = credentials[0]; - ux.action.start('get issuer metadata'); + ux.action.start(`get issuer metadata from ${issuer}`); const issuerMetadata = await getIssuerMetadata(issuer); + const credentialMetadata = getCredentialInfo(credentials, issuerMetadata); ux.action.stop(); - ux.action.start('get openid config'); - const openidConfig = await getOpenidConfiguration(issuer); + const authorizationServer = issuerMetadata.authorization_server ?? issuer; + ux.action.start(`get openid config from ${authorizationServer}`); + const openidConfig = await getOpenidConfiguration(authorizationServer); ux.action.stop(); const preAuthGrant = grants[GRANT_TYPES.PREAUTHORIZED_CODE]; @@ -123,7 +131,7 @@ export async function claimCredentialOffer(credentialOfferURL: string) { throw new Error('could not find a supported grant type'); } - return issueVC(issuer, issuerMetadata.credential_endpoint, token, metadata); + return issueVC(issuer, issuerMetadata.credential_endpoint, token, credentialMetadata); } async function exchangePreauthCodeWithToken(endpoint: string, code: string, userPin?: string) { @@ -156,8 +164,11 @@ async function exchangePreauthCodeWithToken(endpoint: string, code: string, user } type CredentialOfferMetadata = { + credential_definition?: { + vct: string; + }; format: string; - types: string[]; + types?: string[]; // TODO: move this types into credential_definition to support jwt_vc_json } export async function issueVC(issuer: string, endpoint: string, token: TokenSet, metadata: CredentialOfferMetadata) { @@ -182,7 +193,28 @@ export async function issueVC(issuer: string, endpoint: string, token: TokenSet, 'Content-Type': 'application/json', }, method: 'post', - }).then((res) => res.json()); + }).then((res) => parseFetchResponse(res)); - return result.credential; + return result?.credential; } + +export function getCredentialInfo(credentials: Array, issuerMetadata: IssuerMetadata): CredentialOfferMetadata { + if (typeof credentials[0] !== 'string') { + throw new CredentialOfferError('Unsupported format of credential_offer.credentials'); + } + + const credentialIdentifier = credentials[0]; + const credentialMetadata = (issuerMetadata.credentials_supported as SupportedCredentialMap)[credentialIdentifier]; + + return isVcSdJwt(credentialMetadata) + ? { + 'credential_definition': { + vct: credentialMetadata.credential_definition.vct as string, + }, + format: credentialMetadata.format, + } + : { + format: credentialMetadata.format, + types: credentialMetadata.credential_definition?.types ?? credentialMetadata.types + } +} \ No newline at end of file