credential-status
Advanced tools
Comparing version 1.2.4 to 2.0.0
{ | ||
"name": "credential-status", | ||
"version": "1.2.4", | ||
"version": "2.0.0", | ||
"description": "credential status aggregator for did-jwt", | ||
@@ -16,3 +16,2 @@ "main": "lib/index.js", | ||
"lint": "eslint --ignore-pattern \"src/**/*.test.[jt]s\" \"src/**/*.[jt]s\"", | ||
"prepare": "yarn build", | ||
"prepublishOnly": "npm test && npm run lint", | ||
@@ -37,19 +36,18 @@ "release": "semantic-release --debug" | ||
"devDependencies": { | ||
"@babel/preset-typescript": "^7.16.7", | ||
"@babel/preset-typescript": "7.16.7", | ||
"@semantic-release/changelog": "6.0.1", | ||
"@semantic-release/git": "10.0.1", | ||
"@types/jest": "27.4.0", | ||
"@types/node": "14.18.8", | ||
"@typescript-eslint/eslint-plugin": "^5.10.0", | ||
"@typescript-eslint/parser": "^5.10.0", | ||
"@types/node": "16.11.21", | ||
"@typescript-eslint/eslint-plugin": "5.10.0", | ||
"@typescript-eslint/parser": "5.10.0", | ||
"codecov": "3.8.3", | ||
"eslint": "^8.7.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-jest": "^25.7.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint": "8.7.0", | ||
"eslint-config-prettier": "8.3.0", | ||
"eslint-plugin-jest": "26.1.0", | ||
"eslint-plugin-prettier": "4.0.0", | ||
"jest": "27.4.7", | ||
"jest-extended": "0.11.5", | ||
"microbundle": "^0.14.2", | ||
"microbundle": "0.14.2", | ||
"prettier": "2.5.1", | ||
"semantic-release": "18.0.1", | ||
"semantic-release": "19.0.2", | ||
"typescript": "4.5.4" | ||
@@ -56,0 +54,0 @@ }, |
@@ -1,7 +0,5 @@ | ||
import 'jest-extended' | ||
import { CredentialJwtOrJSON, Status, StatusMethod, StatusResolver } from '../index' | ||
import { Status, StatusMethod, StatusResolver } from '../index' | ||
import { DIDDocument } from 'did-resolver' | ||
import { SimpleSigner, createJWT } from 'did-jwt' | ||
import { ES256KSigner, createJWT } from 'did-jwt' | ||
@@ -11,3 +9,3 @@ const privateKey = 'a285ab66393c5fdda46d6fbad9e27fafd438254ab72ad5acb681a0e9f20f5d7b' | ||
const issuer = `did:ethr:${signerAddress}` | ||
const signer = SimpleSigner(privateKey) | ||
const signer = ES256KSigner(privateKey) | ||
@@ -26,61 +24,274 @@ const referenceDoc = { | ||
test('should be able to instantiate Status', () => { | ||
expect(new Status()).not.toBeNil() | ||
}) | ||
describe('credential-status', () => { | ||
describe('API', () => { | ||
it('should be able to instantiate Status', () => { | ||
expect(new Status()).not.toBeNull() | ||
}) | ||
test('should be able to call checkStatus', () => { | ||
const checker = new Status() | ||
expect(checker.checkStatus).toBeFunction() | ||
}) | ||
it('should be able to call checkStatus', () => { | ||
const checker = new Status() | ||
expect(typeof checker.checkStatus).toEqual('function') | ||
}) | ||
test('should reject unknown status method', async () => { | ||
const checker = new Status() | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'UnknownMethod', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ | ||
error: 'Credential status method UnknownMethod unknown. Validity can not be determined.', | ||
it('should pass through credential with no status requirement', async () => { | ||
expect.assertions(1) | ||
const token = await createJWT({}, { issuer, signer }) | ||
const checker = new Status() | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toEqual({ | ||
revoked: false, | ||
message: 'credentialStatus property was not set on the original credential', | ||
}) | ||
}) | ||
it('sample StatusResolver with easy registration', async () => { | ||
class CustomStatusChecker implements StatusResolver { | ||
checkStatus: StatusMethod = async (credential: CredentialJwtOrJSON, doc: DIDDocument) => { | ||
return { revoked: false } | ||
} | ||
asStatusMethod = { CustomStatusChecker: this.checkStatus } | ||
} | ||
const checker = new Status({ | ||
...new CustomStatusChecker().asStatusMethod, | ||
}) | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false }) | ||
}) | ||
}) | ||
}) | ||
test('should pass through credential with no status requirement', async () => { | ||
const token = await createJWT({}, { issuer, signer }) | ||
const checker = new Status() | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({}) | ||
}) | ||
describe('with unknown status method', () => { | ||
it('in legacy JWT payload', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'UnknownMethod', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
await expect(checker.checkStatus(token, referenceDoc)).rejects.toThrow( | ||
/unknown_method: credentialStatus method UnknownMethod unknown. Validity can not be determined./ | ||
) | ||
}) | ||
test('should check status according to registered method', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ 'custom method works': true }) | ||
}) | ||
it('in JWT.vc payload', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
const token = await createJWT( | ||
{ vc: { credentialStatus: { type: 'UnknownMethod', id: 'something something' } } }, | ||
{ issuer, signer } | ||
) | ||
await expect(checker.checkStatus(token, referenceDoc)).rejects.toThrow( | ||
/unknown_method: credentialStatus method UnknownMethod unknown. Validity can not be determined./ | ||
) | ||
}) | ||
test('sample StatusResolver with easy registration', async () => { | ||
class CustomStatusChecker implements StatusResolver { | ||
checkStatus: StatusMethod = async (credential: string, doc: DIDDocument) => { | ||
return { revoked: false } | ||
} | ||
asStatusMethod = { CustomStatusChecker: this.checkStatus } | ||
} | ||
it('in JWT.vp payload', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
const token = await createJWT( | ||
{ vp: { credentialStatus: { type: 'UnknownMethod', id: 'something something' } } }, | ||
{ issuer, signer } | ||
) | ||
await expect(checker.checkStatus(token, referenceDoc)).rejects.toThrow( | ||
/unknown_method: credentialStatus method UnknownMethod unknown. Validity can not be determined./ | ||
) | ||
}) | ||
const checker = new Status({ | ||
...new CustomStatusChecker().asStatusMethod, | ||
it('in plain JSON credential', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
await expect( | ||
checker.checkStatus( | ||
{ | ||
credentialStatus: { | ||
type: 'UnknownMethod', | ||
id: 'something something', | ||
}, | ||
}, | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/unknown_method: credentialStatus method UnknownMethod unknown. Validity can not be determined./ | ||
) | ||
}) | ||
it('in serialized JSON credential', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
await expect( | ||
checker.checkStatus( | ||
JSON.stringify({ | ||
credentialStatus: { | ||
type: 'UnknownMethod', | ||
id: 'something something', | ||
}, | ||
}), | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/unknown_method: credentialStatus method UnknownMethod unknown. Validity can not be determined./ | ||
) | ||
}) | ||
}) | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false }) | ||
describe('with malformed input', () => { | ||
it('should reject credentialStatus entry with bad type', async () => { | ||
expect.assertions(2) | ||
const checker = new Status() | ||
await expect( | ||
checker.checkStatus( | ||
{ | ||
credentialStatus: true, | ||
} as any, | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/bad_request: credentialStatus entry is not formatted correctly. Validity can not be determined./ | ||
) | ||
await expect( | ||
checker.checkStatus( | ||
{ | ||
credentialStatus: 'this is not revoked, believe me', | ||
} as any, | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/bad_request: credentialStatus entry is not formatted correctly. Validity can not be determined./ | ||
) | ||
}) | ||
it('should reject malformed credentialStatus entry', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
await expect( | ||
checker.checkStatus( | ||
{ | ||
credentialStatus: { | ||
revoked: false, | ||
message: 'believe me', | ||
}, | ||
} as any, | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/bad_request: credentialStatus entry is not formatted correctly. Validity can not be determined./ | ||
) | ||
}) | ||
it('should reject credentialStatus entry with no type', async () => { | ||
expect.assertions(1) | ||
const checker = new Status() | ||
await expect( | ||
checker.checkStatus( | ||
{ | ||
credentialStatus: { | ||
notMyType: 'foo', | ||
id: 'bar', | ||
}, | ||
} as any, | ||
referenceDoc | ||
) | ||
).rejects.toThrow( | ||
/bad_request: credentialStatus entry is not formatted correctly. Validity can not be determined./ | ||
) | ||
}) | ||
}) | ||
describe('with known status method', () => { | ||
it('for legacy JWT.payload.credentialStatus', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const token = await createJWT( | ||
{ credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
it('for JWT.payload.vc.credentialStatus', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const token = await createJWT( | ||
{ vc: { credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
it('for JWT.payload.vp.credentialStatus', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const token = await createJWT( | ||
{ vp: { credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } } }, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
it('for plain JSON with credentialStatus', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const statusEntry = await checker.checkStatus( | ||
{ | ||
credentialStatus: { | ||
type: 'CustomStatusChecker', | ||
id: 'something something', | ||
}, | ||
}, | ||
referenceDoc | ||
) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
it('for serialized JSON with credentialStatus', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const statusEntry = await checker.checkStatus( | ||
JSON.stringify({ | ||
credentialStatus: { | ||
type: 'CustomStatusChecker', | ||
id: 'something something', | ||
}, | ||
}), | ||
referenceDoc | ||
) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
it('should prefer vc.credentialStatus for JWT credential', async () => { | ||
const checkStatus: StatusMethod = async () => { | ||
return { revoked: false, 'custom method works': true } | ||
} | ||
const checker = new Status({ CustomStatusChecker: checkStatus }) | ||
const token = await createJWT( | ||
{ | ||
credentialStatus: { type: 'Unknown', id: 'nope' }, | ||
vc: { credentialStatus: { type: 'CustomStatusChecker', id: 'something something' } }, | ||
}, | ||
{ issuer, signer } | ||
) | ||
const statusEntry = await checker.checkStatus(token, referenceDoc) | ||
expect(statusEntry).toStrictEqual({ revoked: false, 'custom method works': true }) | ||
}) | ||
}) | ||
}) |
106
src/index.ts
@@ -5,3 +5,9 @@ import { decodeJWT } from 'did-jwt' | ||
/** | ||
* Represents the result of a status check | ||
* Represents the result of a status check. | ||
* | ||
* Implementations should populate the `revoked` boolean property, but they can return additional metadata that is | ||
* method specific. | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
@@ -21,4 +27,11 @@ export interface CredentialStatus { | ||
* ```json | ||
* status : { type: "EthrStatusRegistry2019", id: "rinkeby:0xregistryAddress" } | ||
* credentialStatus: { | ||
* type: "EthrStatusRegistry2019", | ||
* id: "rinkeby:0xregistryAddress" | ||
* } | ||
* ``` | ||
* See https://www.w3.org/TR/vc-data-model/#status | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
@@ -34,3 +47,3 @@ export interface StatusEntry { | ||
/** | ||
* [draft] The interface expected for status resolvers. | ||
* The interface expected for status resolvers. | ||
* `checkStatus` should be called with a raw credential and it should Promise a [[CredentialStatus]] result. | ||
@@ -49,2 +62,5 @@ * It is advisable that classes that implement this interface also provide a way to easily register the correct | ||
* ``` | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
@@ -56,23 +72,31 @@ export interface StatusResolver { | ||
/** | ||
* The method signature expected to be implemented by credential status resolvers | ||
* The Verifiable Credential or Presentation to be verified in either JSON/JSON-LD or JWT format. | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
export type StatusMethod = (credential: string, didDoc: DIDDocument) => Promise<null | CredentialStatus> | ||
export type CredentialJwtOrJSON = string | { credentialStatus?: StatusEntry } | ||
interface JWTPayloadWithStatus { | ||
credentialStatus?: StatusEntry | ||
/** | ||
* The method signature expected to be implemented by credential status resolvers. | ||
* | ||
* @param credential The credential whose status will be verified | ||
* @param didDoc The DID document of the issuer. | ||
* | ||
* @return a Promise resolving to a `CredentialStatus` object or rejecting with a reason. | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
export type StatusMethod = (credential: CredentialJwtOrJSON, didDoc: DIDDocument) => Promise<CredentialStatus> | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[x: string]: any | ||
} | ||
interface StatusMethodRegistry { | ||
[type: string]: StatusMethod | ||
} | ||
/** | ||
* [draft] An implementation of a StatusMethod that can aggregate multiple other methods. | ||
* It calls the appropriate method based on the `status.type` specified in the credential. | ||
* It calls the appropriate method based on the `credentialStatus.type` specified in the credential. | ||
* | ||
* @alpha This API is still being developed and may be updated. Please follow progress or suggest improvements at | ||
* [https://github.com/uport-project/credential-status] | ||
*/ | ||
export class Status implements StatusResolver { | ||
private registry: StatusMethodRegistry | ||
private registry: Record<string, StatusMethod> | ||
@@ -92,26 +116,48 @@ /** | ||
*/ | ||
constructor(registry: StatusMethodRegistry = {}) { | ||
constructor(registry: Record<string, StatusMethod> = {}) { | ||
this.registry = registry | ||
} | ||
async checkStatus(credential: string, didDoc: DIDDocument): Promise<null | CredentialStatus> { | ||
// TODO: validate the credential to be VerifiableCredential or VerifiablePresentation | ||
const decoded = decodeJWT(credential) | ||
const statusEntry = (decoded.payload as JWTPayloadWithStatus).credentialStatus | ||
async checkStatus(credential: CredentialJwtOrJSON, didDoc: DIDDocument): Promise<CredentialStatus> { | ||
let statusEntry: StatusEntry | undefined = undefined | ||
if (typeof statusEntry === 'undefined') { | ||
return {} | ||
if (typeof credential === 'string') { | ||
try { | ||
const decoded = decodeJWT(credential) | ||
statusEntry = | ||
decoded?.payload?.vc?.credentialStatus || // JWT Verifiable Credential payload | ||
decoded?.payload?.vp?.credentialStatus || // JWT Verifiable Presentation payload | ||
decoded?.payload?.credentialStatus // legacy JWT payload | ||
} catch (e1: unknown) { | ||
// not a JWT credential or presentation | ||
try { | ||
const decoded = JSON.parse(credential) | ||
statusEntry = decoded?.credentialStatus | ||
} catch (e2: unknown) { | ||
// not a JSON either. | ||
} | ||
} | ||
} else { | ||
statusEntry = credential.credentialStatus | ||
} | ||
if (!statusEntry) { | ||
return { | ||
revoked: false, | ||
message: 'credentialStatus property was not set on the original credential', | ||
} | ||
} else if (typeof statusEntry !== 'object' || !statusEntry?.type) { | ||
throw new Error('bad_request: credentialStatus entry is not formatted correctly. Validity can not be determined.') | ||
} | ||
const method = this.registry[statusEntry.type] | ||
if (typeof method !== 'undefined' && method != null) { | ||
if (!method) { | ||
throw new Error( | ||
`unknown_method: credentialStatus method ${statusEntry.type} unknown. Validity can not be determined.` | ||
) | ||
} else { | ||
return method(credential, didDoc) | ||
} else { | ||
return { | ||
// Once the credential status mechanisms in W3C get more stable, perhaps this can become a `reject` | ||
error: `Credential status method ${statusEntry.type} unknown. Validity can not be determined.`, | ||
} | ||
} | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17
413
30863
5
1