diff --git a/docs/api/classes/ClaimsService.md b/docs/api/classes/ClaimsService.md index 73b39064..0766537e 100644 --- a/docs/api/classes/ClaimsService.md +++ b/docs/api/classes/ClaimsService.md @@ -27,7 +27,6 @@ - [publishPublicClaim](ClaimsService.md#publishpublicclaim) - [registerOnchain](ClaimsService.md#registeronchain) - [rejectClaimRequest](ClaimsService.md#rejectclaimrequest) -- [validatePublishPublicClaimRequest](ClaimsService.md#validatepublishpublicclaimrequest) - [create](ClaimsService.md#create) ## Constructors @@ -53,7 +52,7 @@ **`description`** allows subject to request for credential -**`deprecated`** fields - use requestorFields instead +**`field`** { claim: fields } - @deprecated - use requestorFields instead #### Parameters @@ -367,20 +366,22 @@ ___ ### publishPublicClaim -▸ **publishPublicClaim**(`__namedParameters`): `Promise`<`undefined` \| `string`\> +▸ **publishPublicClaim**(`token`): `Promise`<`undefined` \| `string`\> publishPublicClaim -**`description`** store claim data in ipfs and save url to DID document services +**`description`** publishes claim off-chain (by storing claim data in ipfs and save url to DID document services #### Parameters -| Name | Type | -| :------ | :------ | -| `__namedParameters` | `Object` | -| `__namedParameters.claimType?` | `string` | -| `__namedParameters.registrationTypes?` | [`RegistrationTypes`](../enums/RegistrationTypes.md)[] | -| `__namedParameters.token?` | `string` | +| Name | Type | Description | +| :------ | :------ | :------ | +| `token` | `Object` | @deprecated - use claim with claimType instead | +| `token.claim` | `Object` | - | +| `token.claim.claimType?` | `string` | - | +| `token.claim.token?` | `string` | - | +| `token.registrationTypes?` | [`RegistrationTypes`](../enums/RegistrationTypes.md)[] | - | +| `token.token?` | `string` | - | #### Returns @@ -398,9 +399,16 @@ ___ #### Parameters -| Name | Type | -| :------ | :------ | -| `claim` | `Pick`<[`Claim`](../interfaces/Claim.md), ``"token"`` \| ``"subjectAgreement"`` \| ``"onChainProof"`` \| ``"acceptedBy"``\> | +| Name | Type | Description | +| :------ | :------ | :------ | +| `claim` | `Object` | id of signed onchain claim. | +| `claim.acceptedBy?` | `string` | - | +| `claim.claimType?` | `string` | - | +| `claim.claimTypeVersion?` | `string` | - | +| `claim.onChainProof?` | `string` | - | +| `claim.subject?` | `string` | - | +| `claim.subjectAgreement?` | `string` | - | +| `claim.token?` | `string` | - | #### Returns @@ -427,24 +435,6 @@ ___ ___ -### validatePublishPublicClaimRequest - -▸ **validatePublishPublicClaimRequest**(`registrationTypes`, `token?`, `claimType?`): `void` - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `registrationTypes` | [`RegistrationTypes`](../enums/RegistrationTypes.md)[] | -| `token?` | `string` | -| `claimType?` | `string` | - -#### Returns - -`void` - -___ - ### create ▸ `Static` **create**(`signerService`, `domainsService`, `cacheClient`, `didRegistry`): `Promise`<[`ClaimsService`](ClaimsService.md)\> diff --git a/docs/api/enums/ERROR_MESSAGES.md b/docs/api/enums/ERROR_MESSAGES.md index b0232564..4fbebdea 100644 --- a/docs/api/enums/ERROR_MESSAGES.md +++ b/docs/api/enums/ERROR_MESSAGES.md @@ -6,6 +6,8 @@ - [APP\_WITH\_ROLES](ERROR_MESSAGES.md#app_with_roles) - [CAN\_NOT\_UPDATE\_NOT\_CONTROLLED\_DOCUMENT](ERROR_MESSAGES.md#can_not_update_not_controlled_document) +- [CLAIM\_NOT\_FOUND](ERROR_MESSAGES.md#claim_not_found) +- [CLAIM\_TYPE\_REQUIRED\_FOR\_ON\_CHAIN\_REGISTRATION](ERROR_MESSAGES.md#claim_type_required_for_on_chain_registration) - [CLAIM\_WAS\_NOT\_ISSUED](ERROR_MESSAGES.md#claim_was_not_issued) - [ENS\_OWNER\_NOT\_VALID\_ADDRESS](ERROR_MESSAGES.md#ens_owner_not_valid_address) - [ENS\_TYPE\_NOT\_SUPPORTED](ERROR_MESSAGES.md#ens_type_not_supported) @@ -23,6 +25,7 @@ - [ROLE\_NOT\_EXISTS](ERROR_MESSAGES.md#role_not_exists) - [ROLE\_PREREQUISITES\_NOT\_MET](ERROR_MESSAGES.md#role_prerequisites_not_met) - [STAKE\_WAS\_NOT\_PUT](ERROR_MESSAGES.md#stake_was_not_put) +- [TOKEN\_REQUIRED\_FOR\_OFF\_CHAIN\_REGISTRATION](ERROR_MESSAGES.md#token_required_for_off_chain_registration) - [UNKNOWN\_PROVIDER](ERROR_MESSAGES.md#unknown_provider) - [WALLET\_PROVIDER\_NOT\_SUPPORTED](ERROR_MESSAGES.md#wallet_provider_not_supported) - [WITHDRAWAL\_WAS\_NOT\_REQUESTED](ERROR_MESSAGES.md#withdrawal_was_not_requested) @@ -41,6 +44,18 @@ ___ ___ +### CLAIM\_NOT\_FOUND + +• **CLAIM\_NOT\_FOUND** = `"Claim not found"` + +___ + +### CLAIM\_TYPE\_REQUIRED\_FOR\_ON\_CHAIN\_REGISTRATION + +• **CLAIM\_TYPE\_REQUIRED\_FOR\_ON\_CHAIN\_REGISTRATION** = `"claimType required for on-chain registration"` + +___ + ### CLAIM\_WAS\_NOT\_ISSUED • **CLAIM\_WAS\_NOT\_ISSUED** = `"Claim was not issued"` @@ -143,6 +158,12 @@ ___ ___ +### TOKEN\_REQUIRED\_FOR\_OFF\_CHAIN\_REGISTRATION + +• **TOKEN\_REQUIRED\_FOR\_OFF\_CHAIN\_REGISTRATION** = `"token required for off-chain registration"` + +___ + ### UNKNOWN\_PROVIDER • **UNKNOWN\_PROVIDER** = `"Unknown provider type"` diff --git a/docs/api/modules.md b/docs/api/modules.md index e85497dc..e0e7d899 100644 --- a/docs/api/modules.md +++ b/docs/api/modules.md @@ -653,17 +653,17 @@ ___ ### readyToBeRegisteredOnchain -▸ `Const` **readyToBeRegisteredOnchain**(`claim`): claim is Required\> +▸ `Const` **readyToBeRegisteredOnchain**(`claim`): `Required`<`Pick`<[`Claim`](interfaces/Claim.md), ``"subject"`` \| ``"claimTypeVersion"`` \| ``"claimType"`` \| ``"onChainProof"`` \| ``"acceptedBy"`` \| ``"subjectAgreement"``\>\> #### Parameters | Name | Type | | :------ | :------ | -| `claim` | `object` | +| `claim` | `any` | #### Returns -claim is Required\> +`Required`<`Pick`<[`Claim`](interfaces/Claim.md), ``"subject"`` \| ``"claimTypeVersion"`` \| ``"claimType"`` \| ``"onChainProof"`` \| ``"acceptedBy"`` \| ``"subjectAgreement"``\>\> ___ diff --git a/e2e/claims.service.e2e.ts b/e2e/claims.service.e2e.ts index 2bb54a63..eaf00861 100644 --- a/e2e/claims.service.e2e.ts +++ b/e2e/claims.service.e2e.ts @@ -221,7 +221,6 @@ describe('Enrollment claim tests', () => { } async function enrolAndIssueWithoutRequest( - requestSigner: Required, issueSigner: Required, { subjectDID, @@ -278,16 +277,17 @@ describe('Enrollment claim tests', () => { } if (registrationTypes.includes(RegistrationTypes.OnChain)) { expect(onChainProof).toHaveLength(132); - await signerService.connect(requestSigner, ProviderType.PrivateKey); + const mockedClaim = { claimType, isApproved: true, onChainProof, claimTypeVersion: version, acceptedBy: issuerDID, + subject: requesterDID, }; mockGetClaimsBySubject.mockReset().mockImplementationOnce(() => [mockedClaim]); - await claimsService.publishPublicClaim({ claimType, registrationTypes }); + } expect(requester).toEqual(subjectDID); expect(claimIssuer).toEqual([issuerDID]); @@ -397,8 +397,8 @@ describe('Enrollment claim tests', () => { returnSteps: false, }); - mockGetClaimsBySubject.mockImplementationOnce(() => []); - mockGetClaimsBySubject.mockImplementationOnce(() => []); + mockGetClaimsBySubject.mockImplementation(() => []); + //mockGetClaimsBySubject.mockImplementationOnce(() => []); return expect( enrolAndIssue(rootOwner, staticIssuer, { @@ -413,7 +413,9 @@ describe('Enrollment claim tests', () => { const claimType = `${roleName1}.${root}`; const role1Claim = { claimType, + claimTypeVersion: version, isAccepted: true, + subject: rootOwnerDID, }; test('should be able to issue and publish onchain', async () => { @@ -429,18 +431,20 @@ describe('Enrollment claim tests', () => { }); test('should be able to issue and publish onchain without request', async () => { - mockGetClaimsBySubject.mockImplementationOnce(() => [role1Claim]); // to verify requesting + mockGetClaimsBySubject.mockImplementation(() => [role1Claim]); // to verify requesting - await enrolAndIssueWithoutRequest(rootOwner, staticIssuer, { + await enrolAndIssueWithoutRequest(staticIssuer, { subjectDID: rootOwnerDID, claimType, registrationTypes, }); + await signerService.connect(rootOwner, ProviderType.PrivateKey); + await claimsService.publishPublicClaim({ claim: {claimType} , registrationTypes }); expect(await claimsService.hasOnChainRole(rootOwnerDID, claimType, version)).toBe(true); }); test('should be able to issue without publishing onchain', async () => { - mockGetClaimsBySubject.mockImplementationOnce(() => [role1Claim]); + mockGetClaimsBySubject.mockImplementation(() => [role1Claim]); await enrolAndIssue(rootOwner, staticIssuer, { subjectDID: rootOwnerDID, @@ -489,7 +493,7 @@ describe('Enrollment claim tests', () => { mockCachedDocument.mockResolvedValueOnce(subjectDoc); await signerService.connect(subject, ProviderType.PrivateKey); - const claimUrl = await claimsService.publishPublicClaim({ token: issuedToken }); + const claimUrl = await claimsService.publishPublicClaim({ claim: { token: issuedToken } }); subjectDoc = await didRegistry.getDidDocument({ did: subjectDID, includeClaims: true }); expect( diff --git a/src/errors/ErrorMessages.ts b/src/errors/ErrorMessages.ts index 785539b6..d21eae8a 100644 --- a/src/errors/ErrorMessages.ts +++ b/src/errors/ErrorMessages.ts @@ -19,6 +19,9 @@ export enum ERROR_MESSAGES { ERROR_IN_AZURE_PROVIDER = 'Error in Azure Provider', JWT_ALGORITHM_NOT_SUPPORTED = 'Jwt algorithm not supported', CLAIM_WAS_NOT_ISSUED = 'Claim was not issued', + CLAIM_NOT_FOUND = 'Claim not found', + CLAIM_TYPE_REQUIRED_FOR_ON_CHAIN_REGISTRATION = 'claimType required for on-chain registration', + TOKEN_REQUIRED_FOR_OFF_CHAIN_REGISTRATION = 'token required for off-chain registration', ENS_OWNER_NOT_VALID_ADDRESS = 'Provided owner is not a valid address. Owner of ENS domain must be an address', IS_ETH_SIGNER_NOT_SET = 'Can not determine if signer is conformant with eth_sign specification', } diff --git a/src/modules/claims/claims.service.ts b/src/modules/claims/claims.service.ts index 6a1124f5..02a3d565 100644 --- a/src/modules/claims/claims.service.ts +++ b/src/modules/claims/claims.service.ts @@ -26,7 +26,6 @@ import { erc712_type_hash, proof_type_hash, typedMsgPrefix, - Claim, } from './claims.types'; import { DidRegistry } from '../didRegistry/didRegistry.service'; import { ClaimData } from '../didRegistry/did.types'; @@ -146,11 +145,11 @@ export class ClaimsService { async getClaimById(claimId: string) { return this._cacheClient.getClaimById(claimId); } + /** * @description allows subject to request for credential - * @deprecated fields - use requestorFields instead + * @field { claim: fields } - @deprecated - use requestorFields instead */ - async createClaimRequest({ claim, subject = this._signerService.did, @@ -231,6 +230,8 @@ export class ClaimsService { requester, claimIssuer: [this._signerService.did], acceptedBy: this._signerService.did, + claimType: claimData.claimType, + claimTypeVersion: claimData.claimTypeVersion.toString(), }; const strippedClaimData = this.stripClaimData(claimData); if (registrationTypes.includes(RegistrationTypes.OffChain)) { @@ -262,53 +263,56 @@ export class ClaimsService { } /** - * @description Registers issued onchain claim with Claim manager * - * @param claimId - id of signed onchain claim + * @param token optional token containing claimType, version and subject + * @returns claim params obtained from token */ - async registerOnchain( - claim: Pick - ) { - if (!readyToBeRegisteredOnchain(claim) || !claim.token) { - throw new Error(ERROR_MESSAGES.CLAIM_WAS_NOT_ISSUED); + private getClaimTypeFromToken(token?: string) { + if (token) { + const { claimData, sub } = this._didRegistry.jwt.decode(token) as { + claimData: { claimType: string; claimTypeVersion: string }; + sub: string; + }; + return { ...claimData, subject: sub }; } - - const { claimData, sub } = this._didRegistry.jwt.decode(claim.token) as { - claimData: { claimType: string; claimTypeVersion: number }; - sub: string; - }; - - await this.performRegisterOnchain({ - ...claim, - claimType: claimData.claimType, - claimTypeVersion: claimData.claimTypeVersion.toString(), - subject: sub, - }); + return {}; } /** * @description Registers issued onchain claim with Claim manager * - * @param claimId - id of signed onchain claim + * @param claim - id of signed onchain claim. + * @param token - @deprecated use subject, claimType, claimTypeVersion instead. Token should get removed and subject, claimType, claimTypeVersion should be required. */ - private async performRegisterOnchain( - claim: Pick< - Claim, - | 'subject' - | 'claimType' - | 'claimTypeVersion' - | 'subjectAgreement' - | 'onChainProof' - | 'acceptedBy' - > - ) { - if (!readyToBeRegisteredOnchain(claim)) { - throw new Error(ERROR_MESSAGES.CLAIM_WAS_NOT_ISSUED + ':' + JSON.stringify(claim)); + async registerOnchain(claim: { + claimType?: string; + claimTypeVersion?: string; + token?: string; + subjectAgreement?: string; + onChainProof?: string; + acceptedBy?: string; + subject?: string; + }) { + // backward compatibility with token + claim = { ...claim, ...this.getClaimTypeFromToken(claim.token) }; + + if ( + !claim.subjectAgreement && + claim.subject === this._signerService.did && + claim.claimType && + claim.claimTypeVersion + ) { + claim.subjectAgreement = await this.approveRolePublishing({ + subject: this._signerService.did, + role: claim.claimType as string, + version: +claim.claimTypeVersion, + }); } - const { subjectAgreement, onChainProof, acceptedBy, subject, claimType, claimTypeVersion } = - claim; const expiry = defaultClaimExpiry; + const { subject, claimTypeVersion, claimType, acceptedBy, subjectAgreement, onChainProof } = + readyToBeRegisteredOnchain(claim); + const data = this._claimManagerInterface.encodeFunctionData('register', [ addressOf(subject), namehash(claimType), @@ -413,64 +417,60 @@ export class ClaimsService { return v4(); } - validatePublishPublicClaimRequest( + /** + * + * @description validates publish public claim parameters depending on off or on chain registration type. Throws relevant error on invalid data. + * + */ + private validatePublishPublicClaimRequest( registrationTypes: RegistrationTypes[], - token?: string, - claimType?: string + claim: { token?: string; claimType?: string } ) { - if (registrationTypes.includes(RegistrationTypes.OnChain) && !claimType) { - throw new Error('claimType required for on-chain registration'); + if (registrationTypes.includes(RegistrationTypes.OnChain) && !claim.claimType) { + throw new Error(ERROR_MESSAGES.CLAIM_TYPE_REQUIRED_FOR_ON_CHAIN_REGISTRATION); } - if (registrationTypes.includes(RegistrationTypes.OffChain) && !token) { - throw new Error('Token required for on-chain registration'); + if (registrationTypes.includes(RegistrationTypes.OffChain) && !claim.token) { + throw new Error(ERROR_MESSAGES.TOKEN_REQUIRED_FOR_OFF_CHAIN_REGISTRATION); } } /** * publishPublicClaim * - * @description store claim data in ipfs and save url to DID document services + * @description publishes claim off-chain (by storing claim data in ipfs and save url to DID document services * @returns ulr to ipfs + * @param token - @deprecated - use claim with claimType instead * */ async publishPublicClaim({ - token, - claimType, + token, // backward compatibility registrationTypes = [RegistrationTypes.OffChain], + claim, }: { token?: string; - claimType?: string; registrationTypes?: RegistrationTypes[]; + claim: { token?: string; claimType?: string }; }) { - this.validatePublishPublicClaimRequest(registrationTypes, token, claimType); + claim.token = claim.token || token; + this.validatePublishPublicClaimRequest(registrationTypes, claim); let url: string | undefined = undefined; if (registrationTypes.includes(RegistrationTypes.OnChain)) { const claims = await this.getClaimsBySubject({ did: this._signerService.did, - namespace: claimType, + namespace: claim.claimType, isAccepted: true, }); if (claims.length < 1) { - throw new Error('Claim not found'); + throw new Error(ERROR_MESSAGES.CLAIM_NOT_FOUND); } const claimData = claims[0]; - const subjectAgreement = await this.approveRolePublishing({ - subject: this._signerService.did, - role: claimData.claimType, - version: +claimData.claimTypeVersion, - }); - await this.performRegisterOnchain({ - claimType: claimData.claimType, - claimTypeVersion: claimData.claimTypeVersion, - subject: this._signerService.did, - subjectAgreement, - onChainProof: claimData.onChainProof, - acceptedBy: claimData.acceptedBy, - }); + await this.registerOnchain(claimData); } + // add scenario for offchain without request based on claimType instead of token + // can we break API so that register on chain required only claim type and claim type version and subject if (registrationTypes.includes(RegistrationTypes.OffChain)) { - token = token as string; + const token = claim.token as string; const payload = (await this._didRegistry.decodeJWTToken({ token })) as { iss: string; sub: string; @@ -516,7 +516,7 @@ export class ClaimsService { */ async createSelfSignedClaim({ data, subject }: { data: ClaimData; subject?: string }) { const token = await this._didRegistry.createPublicClaim({ data, subject }); - return this.publishPublicClaim({ token }); + return this.publishPublicClaim({ claim: { token } }); } /** diff --git a/src/modules/claims/claims.types.ts b/src/modules/claims/claims.types.ts index 6dc464e5..3ac5e579 100644 --- a/src/modules/claims/claims.types.ts +++ b/src/modules/claims/claims.types.ts @@ -55,12 +55,34 @@ export interface Claim { } export const readyToBeRegisteredOnchain = ( - claim: object -): claim is Required> => { - const requiredProps = ['onChainProof', 'acceptedBy', 'subjectAgreement']; + claim: any +): Required< + Pick< + Claim, + | 'claimType' + | 'claimTypeVersion' + | 'subject' + | 'onChainProof' + | 'acceptedBy' + | 'subjectAgreement' + > +> => { + const requiredProps = [ + 'claimType', + 'claimTypeVersion', + 'subject', + 'onChainProof', + 'acceptedBy', + 'subjectAgreement', + ]; const claimProps = Object.keys(claim); - return requiredProps.every((p) => claimProps.includes(p)); + const missingProps = requiredProps.filter((p) => !claimProps.includes(p)); + if (missingProps.length > 0) + throw new Error( + 'CANNOT REGISTER ON CHAIN. THE FOLLOWING PROPS ARE MISSING: ' + JSON.stringify(missingProps) + ); + return claim; }; export const typedMsgPrefix = '1901';