diff --git a/.dockerignore b/.dockerignore index 28fda52..f2651f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ **/*.env -node_modules \ No newline at end of file +node_modules +compose-health-test.yaml +compose-v2-test.yaml +.env.healthcheck.testing \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8078a01..93c629f 100644 --- a/.gitignore +++ b/.gitignore @@ -108,4 +108,8 @@ dist .tern-port # vscode -.vscode \ No newline at end of file +.vscode + +compose-health-test.yaml +compose-v2-test.yaml +.env.healthcheck.testing \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c03c091..9093f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # issuer-coordinator Changelog -## 0.3.0 - TBD +## 0.3.0 - 2024-09-06 ### Changed - Convert Status List 2021 to Bitstring Status List. - Differentiate between database status service and Git status service. - Rename environment variables. - Update revocation and suspension instructions. +- Add endpoint to retrieve status list from underlying database status service. ## 0.2.0 - 2024-04-22 diff --git a/README.md b/README.md index 84388d0..7302af6 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,16 @@ Use this service to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data Implements two [VC-API](https://w3c-ccg.github.io/vc-api/) HTTP endpoints: + * [POST /credentials/issue](https://w3c-ccg.github.io/vc-api/#issue-credential) * [POST /credentials/status](https://w3c-ccg.github.io/vc-api/#update-status) +Also implements an endpoint that returns a status list if the underlying status service itself returns the list - essentially then just acts as a public proxy within the docker compose, forwarding the request for the list to the status service and returning the result. For the moment, our [mongo-backed status service](https://github.com/digitalcredentials/status-service-db) is the only status service (we know of) that returns a list. The endpoint then is: + + * [POST /status/:listId](https://w3c-ccg.github.io/vc-api/#update-status) + +Where the :listId is the identifier of the list + We've tried hard to make this simple to install and maintain, and correspondingly easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. In particular, we've separated the discrete parts of an issuer into smaller self-contained apps that are consequently easier to understand and evaluate, and easier to *wire* together to compose functionality. The apps are wired together in a simple Docker Compose network that pulls images from Docker Hub. @@ -95,7 +102,7 @@ curl --location 'http://localhost:4005/instance/test/credentials/issue' \ --data-raw '{ "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" ], "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", "type": [ @@ -145,7 +152,7 @@ This should return a fully formed and signed credential printed to the terminal, { "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", "https://w3id.org/security/suites/ed25519-2020/v1" ], "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", @@ -403,7 +410,7 @@ curl --location 'http://localhost:4005/instance/econ101/credentials/issue' \ --data-raw '{ "@context": [ "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" ], "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", "type": [ diff --git a/src/app.js b/src/app.js index 2dd1f6e..de8b483 100644 --- a/src/app.js +++ b/src/app.js @@ -133,6 +133,22 @@ export async function build (opts = {}) { } }) + app.get('/status/:statusCredentialId', async function (req, res, next) { + if (!enableStatusService) next({ code: 405, message: 'The status service has not been enabled.' }) + const statusCredentialId = req.params.statusCredentialId + try { + const { data: statusCredential } = await axios.get(`http://${statusService}/${statusCredentialId}`) + return res.status(200).json(statusCredential) + } catch (error) { + if (error.response.status === 404) { + next({ code: 404, message: 'No status credential found for that id.' }) + } else { + next(error) + } + } + return res.status(500).send({ message: 'Server error.' }) + }) + // Attach the error handling middleware calls, in order they should run app.use(errorLogger) app.use(errorHandler) diff --git a/src/app.test.js b/src/app.test.js index 29ad245..5f0c2eb 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -7,6 +7,8 @@ import protectedNock from './test-fixtures/nocks/protected_status_signing.js' import unprotectedStatusUpdateNock from './test-fixtures/nocks/unprotected_status_update.js' import unknownStatusIdNock from './test-fixtures/nocks/unknown_status_id_nock.js' import protectedStatusUpdateNock from './test-fixtures/nocks/protected_status_update.js' +import unknownStatusListNock from './test-fixtures/nocks/unknown_status_list_nock.js' +import statusListNock from './test-fixtures/nocks/status_list_nock.js' import { build } from './app.js' @@ -227,4 +229,25 @@ describe('api', () => { expect(response.status).to.equal(200) }) }) + + describe('GET /status/:statusCredentialId', () => { + it('returns 404 for unknown status credential id', async () => { + unknownStatusListNock() + const response = await request(app) + .get('/status/9898u') + expect(response.header['content-type']).to.have.string('json') + expect(response.status).to.equal(404) + }) + + it('returns credential status list from status service', async () => { + statusListNock() + const response = await request(app) + .get('/status/slAwJe6GGR6mBojlGW5U') + expect(response.header['content-type']).to.have.string('json') + expect(response.status).to.equal(200) + const returnedList = JSON.parse(JSON.stringify(response.body)) + // this proof value comes from the nock: + expect(returnedList.proof.proofValue).to.equal('z4y3GawinQg1aCqbYqZM8dmDpbmtFa3kE6tFefdXvLi5iby25dvmVwLNZrfcFPyhpshrhCWB76pdSZchVve3K1Znr') + }) + }) }) diff --git a/src/test-fixtures/nocks/status_list_nock.js b/src/test-fixtures/nocks/status_list_nock.js new file mode 100644 index 0000000..df6725e --- /dev/null +++ b/src/test-fixtures/nocks/status_list_nock.js @@ -0,0 +1,34 @@ +import nock from 'nock' + +const theList = `{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "https://sincere-bonefish-currently.ngrok-free.app/slAwJe6GGR6mBojlGW5U", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "credentialSubject": { + "id": "https://sincere-bonefish-currently.ngrok-free.app/slAwJe6GGR6mBojlGW5U#list", + "type": "BitstringStatusList", + "encodedList": "uH4sIAAAAAAAAA-3BIQEAAAACICf4f60vTEADAAAAAAAAAAAAAADwN_wEBkHUMAAA", + "statusPurpose": "revocation" + }, + "issuer": "did:key:z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR", + "validFrom": "2024-09-03T15:24:19.685Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-09-03T15:24:19Z", + "verificationMethod": "did:key:z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR#z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR", + "proofPurpose": "assertionMethod", + "proofValue": "z4y3GawinQg1aCqbYqZM8dmDpbmtFa3kE6tFefdXvLi5iby25dvmVwLNZrfcFPyhpshrhCWB76pdSZchVve3K1Znr" + } +}` + +export default () => { + nock('http://localhost:4008') + .get('/slAwJe6GGR6mBojlGW5U') + .reply(200, theList) +} diff --git a/src/test-fixtures/nocks/unknown_status_list_nock.js b/src/test-fixtures/nocks/unknown_status_list_nock.js new file mode 100644 index 0000000..6bb349a --- /dev/null +++ b/src/test-fixtures/nocks/unknown_status_list_nock.js @@ -0,0 +1,7 @@ +import nock from 'nock' + +export default () => { + nock('http://localhost:4008') + .get('/9898u') + .reply(404, { code: 404, message: 'No status credential found for that id.' }) +}