Skip to content

Commit

Permalink
feat: FTL-16681 add commands to manage credential issuance configurat…
Browse files Browse the repository at this point in the history
…ion (#400)
  • Loading branch information
rbrazhnyk authored Sep 2, 2024
1 parent 8c86faf commit 7092f76
Show file tree
Hide file tree
Showing 11 changed files with 621 additions and 1 deletion.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"oclif"
],
"dependencies": {
"@affinidi-tdk/credential-issuance-client": "^1.21.0",
"@affinidi-tdk/iota-client": "^1.11.0",
"@affinidi-tdk/wallets-client": "^1.14.0",
"@inquirer/prompts": "^5.3.4",
Expand Down Expand Up @@ -136,6 +137,9 @@
},
"wallet": {
"description": "Use these commands to manage your wallets"
},
"issuance": {
"description": "Use these commands to manage credential issuance configurations"
}
},
"warn-if-update-available": {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/iota/delete-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class DeleteIotaConfig extends BaseCommand<typeof DeleteIotaConfig> {
})
const validatedFlags = schema.parse(promptFlags)

ux.action.start('Deleting Affinidi Iota Framework configurations')
ux.action.start('Deleting Affinidi Iota Framework configuration')
await iotaService.deleteIotaConfigById(validatedFlags.id)
ux.action.stop('Deleted successfully!')

Expand Down
179 changes: 179 additions & 0 deletions src/commands/issuance/create-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { readFile } from 'fs/promises'
import {
IssuanceConfigDto,
CreateIssuanceConfigInput,
CredentialSupportedObject,
} from '@affinidi-tdk/credential-issuance-client'
import { WalletDto, CreateWalletInput } from '@affinidi-tdk/wallets-client'
import { input, select } from '@inquirer/prompts'
import { ux, Flags } from '@oclif/core'
import { CLIError } from '@oclif/core/errors'
import z from 'zod'
import { BaseCommand } from '../../common/base-command.js'
import { DidMethods } from '../../common/constants.js'
import { promptRequiredParameters } from '../../common/prompts.js'
import { INPUT_LIMIT, validateInputLength } from '../../common/validators.js'
import { issuanceService } from '../../services/affinidi/cis/service.js'
import { cweService } from '../../services/affinidi/cwe/service.js'

export class CreateIssuanceConfig extends BaseCommand<typeof CreateIssuanceConfig> {
static summary = 'Creates credential issuance configuration in your active project'
static examples = [
'<%= config.bin %> <%= command.id %> -n <value> -w <value> -f credentialSchemas.json',
'<%= config.bin %> <%= command.id %> --name <value> --wallet-id <value> --description <value> --credential-offer-duration <value> --file credentialSchemas.json',
]
static flags = {
name: Flags.string({
char: 'n',
summary: 'Name of the credential issuance configuration',
}),
description: Flags.string({
char: 'd',
summary: 'Description of the credential issuance configuration',
}),
'wallet-id': Flags.string({
char: 'w',
summary: 'ID of the wallet',
}),
'credential-offer-duration': Flags.integer({
summary: 'Credential offer duration in seconds',
}),
file: Flags.string({
char: 'f',
summary:
'Location of a json file containing the list of allowed schemas for creating a credential offer. One or more schemas can be added to the issuance. The credential type ID must be unique',
}),
}

public async run(): Promise<IssuanceConfigDto> {
const { flags } = await this.parse(CreateIssuanceConfig)

const promptFlags = await promptRequiredParameters(['wallet-id', 'name', 'file'], flags)

const flagsSchema = z.object({
'wallet-id': z.string().max(INPUT_LIMIT),
name: z.string().min(3).max(INPUT_LIMIT),
description: z.string().max(INPUT_LIMIT).optional(),
'credential-offer-duration': z.number().optional(),
file: z.string(),
})
const validatedFlags = flagsSchema.parse(promptFlags)

ux.action.start('Fetching wallets')
const { wallets } = await cweService.listWallets()
ux.action.stop('Fetched successfully!')

let issuerWalletId = validatedFlags['wallet-id']

const walletIds = wallets?.map((wallet: WalletDto) => wallet.id) || []
const wrongAriProvided = !walletIds.includes(issuerWalletId)

if (!issuerWalletId || wallets?.length === 0 || wrongAriProvided) {
const walletChoices =
wallets?.map((wallet: WalletDto) => ({
name: wallet.name || wallet.id,
value: wallet.id,
})) || []

const CREATE_NEW_WALLET = 'Create new wallet'
walletChoices.push({ name: CREATE_NEW_WALLET, value: CREATE_NEW_WALLET })

const selectedWallet = await select({
message: 'Select the wallet used for signing credentials that will be issued',
choices: walletChoices,
})

if (selectedWallet === CREATE_NEW_WALLET) {
const walletDidMethodChoices = Object.values(DidMethods).map((method: string) => ({
name: method,
value: method,
default: DidMethods.KEY,
}))

const name = validateInputLength(await input({ message: 'Enter wallet name' }), INPUT_LIMIT)
const description = validateInputLength(
await input({ message: 'Enter wallet description (optional)' }),
INPUT_LIMIT,
)
const didMethod = await select({ message: 'Select DID method of wallet', choices: walletDidMethodChoices })

const isDidWeb = didMethod === DidMethods.WEB
const didWebUrl = isDidWeb
? validateInputLength(await input({ message: 'Enter did:web URL (your applications domain)' }), INPUT_LIMIT)
: undefined

const newWalletData = {
name,
description,
didMethod,
...(isDidWeb && { didWebUrl }),
}

const walletSchema = z
.object({
name: z.string().min(3).max(INPUT_LIMIT).optional(),
description: z.string().max(INPUT_LIMIT).optional(),
didMethod: z.nativeEnum(DidMethods).optional(),
didWebUrl: z.string().min(3).max(INPUT_LIMIT).optional(),
})
.refine((wallet) => {
if (wallet.didMethod === DidMethods.WEB && !wallet.didWebUrl) {
return false
}
return true
})

const createWalletInput = walletSchema.parse(newWalletData)

ux.action.start('Creating wallet')
const output = await cweService.createWallet(createWalletInput as CreateWalletInput)
ux.action.stop('Created successfully!')

issuerWalletId = output.wallet?.id || ''
} else {
issuerWalletId = selectedWallet || ''
}
}

let credentialSupported: CredentialSupportedObject[]

try {
const rawData = await readFile(validatedFlags.file, 'utf8')
credentialSupported = JSON.parse(rawData)
} catch (error) {
throw new CLIError(`Provided file is not a valid JSON\n${(error as Error).message}`)
}

const data: CreateIssuanceConfigInput = {
name:
flags.name ??
validateInputLength(await input({ message: 'Enter credential issuance configuration name' }), INPUT_LIMIT),
description: flags.description ?? '',
issuerWalletId,
credentialOfferDuration: flags['credential-offer-duration'] ?? undefined,
credentialSupported: credentialSupported ?? [],
}

const credentialSupportedSchema = z.object({
credentialTypeId: z.string(),
jsonSchemaUrl: z.string(),
jsonLdContextUrl: z.string(),
})

const schema = z.object({
name: z.string().min(3).max(INPUT_LIMIT),
issuerWalletId: z.string().min(1).max(INPUT_LIMIT),
description: z.string().max(INPUT_LIMIT).optional(),
credentialOfferDuration: z.number().min(1).optional(),
credentialSupported: z.array(credentialSupportedSchema),
})
const configInput = schema.parse(data)

ux.action.start('Creating credential issuance configuration')
const output = await issuanceService.createIssuanceConfig(configInput)
ux.action.stop('Created successfully!')

if (!this.jsonEnabled()) this.logJson(output)
return output
}
}
37 changes: 37 additions & 0 deletions src/commands/issuance/delete-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ux, Flags } from '@oclif/core'
import z from 'zod'
import { BaseCommand } from '../../common/base-command.js'
import { promptRequiredParameters } from '../../common/prompts.js'
import { INPUT_LIMIT } from '../../common/validators.js'
import { issuanceService } from '../../services/affinidi/cis/service.js'

export class DeleteIssuanceConfig extends BaseCommand<typeof DeleteIssuanceConfig> {
static summary = 'Deletes credential issuance configuration from your active project'
static examples = [
'<%= config.bin %> <%= command.id %> -i <value>',
'<%= config.bin %> <%= command.id %> --id <value>',
]
static flags = {
id: Flags.string({
char: 'i',
summary: 'ID of the credential issuance configuration',
}),
}

public async run(): Promise<{ id: string }> {
const { flags } = await this.parse(DeleteIssuanceConfig)
const promptFlags = await promptRequiredParameters(['id'], flags)

const schema = z.object({
id: z.string().max(INPUT_LIMIT),
})
const validatedFlags = schema.parse(promptFlags)

ux.action.start('Deleting credential issuance configuration')
await issuanceService.deleteIssuanceConfigById(validatedFlags.id)
ux.action.stop('Deleted successfully!')

if (!this.jsonEnabled()) this.logJson({ id: validatedFlags.id })
return { id: validatedFlags.id }
}
}
38 changes: 38 additions & 0 deletions src/commands/issuance/get-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IssuanceConfigDto } from '@affinidi-tdk/credential-issuance-client'
import { ux, Flags } from '@oclif/core'
import z from 'zod'
import { BaseCommand } from '../../common/base-command.js'
import { promptRequiredParameters } from '../../common/prompts.js'
import { INPUT_LIMIT } from '../../common/validators.js'
import { issuanceService } from '../../services/affinidi/cis/service.js'

export class GetIssuanceConfig extends BaseCommand<typeof GetIssuanceConfig> {
static summary = 'Gets the details of the credential issuance configuration in your active project'
static examples = [
'<%= config.bin %> <%= command.id %> -i <value>',
'<%= config.bin %> <%= command.id %> --id <value>',
]
static flags = {
id: Flags.string({
char: 'i',
summary: 'ID of the credential issuance configuration',
}),
}

public async run(): Promise<IssuanceConfigDto> {
const { flags } = await this.parse(GetIssuanceConfig)
const promptFlags = await promptRequiredParameters(['id'], flags)

const schema = z.object({
id: z.string().max(INPUT_LIMIT),
})
const validatedFlags = schema.parse(promptFlags)

ux.action.start('Fetching credential issuance configuration')
const output = await issuanceService.getIssuanceConfigById(validatedFlags.id)
ux.action.stop('Fetched successfully!')

if (!this.jsonEnabled()) this.logJson(output)
return output
}
}
18 changes: 18 additions & 0 deletions src/commands/issuance/list-configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IssuanceConfigListResponse } from '@affinidi-tdk/credential-issuance-client'
import { ux } from '@oclif/core'
import { BaseCommand } from '../../common/base-command.js'
import { issuanceService } from '../../services/affinidi/cis/service.js'

export class ListIssuanceConfigs extends BaseCommand<typeof ListIssuanceConfigs> {
static summary = 'Lists credential issuance configurations in your active project'
static examples = ['<%= config.bin %> <%= command.id %>']

public async run(): Promise<IssuanceConfigListResponse> {
ux.action.start('Fetching credential issuance configurations')
const output = await issuanceService.listIssuanceConfigs()
ux.action.stop('Fetched successfully!')

if (!this.jsonEnabled()) this.logJson(output)
return output
}
}
Loading

0 comments on commit 7092f76

Please sign in to comment.