@velocitycareerlabs/server-webwallet 1.26.0-dev-build.10a394937 → 1.26.0-dev-build.1675a0ab5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velocitycareerlabs/server-webwallet",
3
- "version": "1.26.0-dev-build.10a394937",
3
+ "version": "1.26.0-dev-build.1675a0ab5",
4
4
  "description": "Web Wallet application",
5
5
  "repository": "https://github.com/velocitycareerlabs/packages",
6
6
  "engines": {
@@ -33,7 +33,7 @@
33
33
  "@fastify/swagger": "^9.0.0",
34
34
  "@fastify/swagger-ui": "^5.0.0",
35
35
  "@spencejs/spence-mongo-repos": "^0.10.2",
36
- "@velocitycareerlabs/migrations": "1.26.0-dev-build.10a394937",
36
+ "@velocitycareerlabs/migrations": "1.26.0-dev-build.1675a0ab5",
37
37
  "@verii/auth": "1.0.0-pre.1754372712",
38
38
  "@verii/common-functions": "1.0.0-pre.1754372712",
39
39
  "@verii/common-schemas": "1.0.0-pre.1754372712",
@@ -73,5 +73,5 @@
73
73
  "nodemon": "3.1.10",
74
74
  "prettier": "2.8.8"
75
75
  },
76
- "gitHead": "6cbb2d0baa2246c90789b0d84e86d874590cf46e"
76
+ "gitHead": "3b801a773c372d2d73b124d9463cc11dd324ecce"
77
77
  }
@@ -5,6 +5,7 @@ const {
5
5
  VCLDeepLink,
6
6
  VCLPresentationRequestDescriptor,
7
7
  VCLDidJwk,
8
+ VCLAuthTokenDescriptor,
8
9
  } = require('@verii/vnf-nodejs-wallet-sdk');
9
10
  const { getAppConfig } = require('../../fetchers/career-wallet/get-app-config');
10
11
  const {
@@ -119,7 +120,9 @@ const disclosureController = async (fastify) => {
119
120
 
120
121
  let authToken = null;
121
122
  if (presentationRequest.feed) {
122
- authToken = await vclSdk.getAuthToken({ presentationRequest });
123
+ authToken = await vclSdk.getAuthToken(
124
+ new VCLAuthTokenDescriptor(presentationRequest)
125
+ );
123
126
  }
124
127
 
125
128
  const { payload: presentation } = jwtDecode(
@@ -148,6 +151,20 @@ const disclosureController = async (fastify) => {
148
151
  type: body.type,
149
152
  });
150
153
 
154
+ if (presentationRequest.feed) {
155
+ await repos.feeds.insert({
156
+ auth0UserId: user.sub,
157
+ authToken,
158
+ deeplink: body.link,
159
+ disclosureId: presentation.presentation_definition.id.split('.')[1],
160
+ inspectorDid: presentation.iss,
161
+ credentialTypes:
162
+ presentation.presentation_definition.input_descriptors.map(
163
+ (descriptor) => descriptor.id
164
+ ),
165
+ });
166
+ }
167
+
151
168
  return { disclosure };
152
169
  }
153
170
  );
@@ -0,0 +1,20 @@
1
+ const { oauthPlugin } = require('@verii/auth');
2
+ const { values, forEach } = require('lodash/fp');
3
+
4
+ const controllerSchemas = require('./schemas');
5
+ const disclosureSchemas = require('../../entities/disclosures/schemas');
6
+
7
+ module.exports = async (fastify) => {
8
+ forEach(
9
+ (schema) => fastify.addSchema(schema),
10
+ [...values(disclosureSchemas), ...values(controllerSchemas)]
11
+ );
12
+ fastify
13
+ .autoSchemaPreset({
14
+ tags: ['webwallet'],
15
+ })
16
+ .register(oauthPlugin, {
17
+ domain: fastify.config.auth0Domain,
18
+ audience: [fastify.config.auth0WebWalletAudience],
19
+ });
20
+ };
@@ -0,0 +1,181 @@
1
+ const newError = require('http-errors');
2
+ const { jwtDecode } = require('@verii/jwt');
3
+ const {
4
+ VCLPresentationSubmission,
5
+ VCLDeepLink,
6
+ VCLPresentationRequestDescriptor,
7
+ VCLDidJwk,
8
+ VCLAuthTokenDescriptor,
9
+ } = require('@verii/vnf-nodejs-wallet-sdk');
10
+
11
+ /**
12
+ * @param {import('fastify').FastifyInstance} fastify
13
+ */
14
+ const feedsController = async (fastify) => {
15
+ fastify.get(
16
+ '/',
17
+ {
18
+ preValidation: fastify.verifyAccessToken(),
19
+ schema: fastify.autoSchema({
20
+ tags: ['webwallet'],
21
+ response: {
22
+ 200: {
23
+ $ref: 'https://velocitycareerlabs.io/webwallet-get-feeds-response.schema.json',
24
+ },
25
+ '4xx': { $ref: 'error#' },
26
+ },
27
+ }),
28
+ },
29
+ async ({ repos, user }) => {
30
+ const feeds = await repos.feeds.find({
31
+ filter: { auth0UserId: user.sub },
32
+ });
33
+
34
+ return feeds;
35
+ }
36
+ );
37
+ fastify.delete(
38
+ '/:feedId',
39
+ {
40
+ preValidation: fastify.verifyAccessToken(),
41
+ schema: fastify.autoSchema({
42
+ params: {
43
+ type: 'object',
44
+ properties: {
45
+ feedId: { type: 'string' },
46
+ },
47
+ },
48
+ response: {
49
+ 204: { type: 'null' },
50
+ },
51
+ }),
52
+ },
53
+ async (req, reply) => {
54
+ const {
55
+ repos: { feeds: feedsRepo },
56
+ params,
57
+ user,
58
+ } = req;
59
+ const feed = await feedsRepo.findOne({
60
+ filter: { _id: params.feedId, auth0UserId: user.sub },
61
+ });
62
+
63
+ if (!feed) {
64
+ throw newError.NotFound(`Feed ${params.feedId} not found`);
65
+ }
66
+
67
+ await feedsRepo.update(params.feedId, {
68
+ revokedAt: new Date(),
69
+ });
70
+
71
+ reply.code(204);
72
+ }
73
+ );
74
+ fastify.post(
75
+ '/share-feed',
76
+ {
77
+ preValidation: fastify.verifyAccessToken(),
78
+ schema: fastify.autoSchema({
79
+ tags: ['webwallet'],
80
+ body: {
81
+ $ref: 'https://velocitycareerlabs.io/webwalletshare-feed-request-body.schema.json',
82
+ },
83
+ response: {
84
+ 200: {
85
+ $ref: 'https://velocitycareerlabs.io/webwallet-share-feed-response.schema.json',
86
+ },
87
+ '4xx': { $ref: 'error#' },
88
+ },
89
+ }),
90
+ },
91
+ async ({
92
+ body,
93
+ vclSdk,
94
+ repos,
95
+ user,
96
+ config: { careerWalletAdminAccessToken },
97
+ }) => {
98
+ const { didKeyMetadatum } = await repos.accounts.findOne({
99
+ filter: { userId: user.sub },
100
+ });
101
+ const didJwk =
102
+ didKeyMetadatum &&
103
+ Array.isArray(didKeyMetadatum) &&
104
+ didKeyMetadatum.length > 0
105
+ ? VCLDidJwk.fromJSON(didKeyMetadatum[0])
106
+ : null;
107
+
108
+ const { did: userDid } = await repos.accounts.findOne({
109
+ filter: { userId: user.sub },
110
+ });
111
+
112
+ const feeds = await repos.feeds.find({
113
+ filter: {
114
+ auth0UserId: user.sub,
115
+ },
116
+ });
117
+
118
+ const disclosurePromises = feeds.map(async (feed) => {
119
+ const credentialsMatch = body.credentials.filter((credential) =>
120
+ feed.credentialTypes.includes(credential.inputDescriptor)
121
+ );
122
+ if (credentialsMatch.length === 0) {
123
+ return null;
124
+ }
125
+
126
+ const presentationRequest = await vclSdk.getPresentationRequest(
127
+ new VCLPresentationRequestDescriptor(
128
+ new VCLDeepLink(feed.deeplink),
129
+ null,
130
+ didJwk,
131
+ careerWalletAdminAccessToken
132
+ )
133
+ );
134
+
135
+ const { payload: presentation } = jwtDecode(
136
+ presentationRequest.jwt.encodedJwt
137
+ );
138
+
139
+ let { authToken } = feed;
140
+
141
+ const authTokenValid =
142
+ feed.authToken.accessToken.jwtValue.payload.exp >
143
+ Math.floor(Date.now() / 1000);
144
+
145
+ if (!authTokenValid) {
146
+ authToken = await vclSdk.getAuthToken(
147
+ new VCLAuthTokenDescriptor(presentationRequest),
148
+ feed.refreshToken
149
+ );
150
+ }
151
+
152
+ const submissionResult = await vclSdk.submitPresentation(
153
+ new VCLPresentationSubmission(presentationRequest, credentialsMatch),
154
+ authToken
155
+ );
156
+
157
+ const disclosure = await repos.disclosures.insert({
158
+ auth0UserId: user.sub,
159
+ userDid,
160
+ presentation,
161
+ encodedJwt: presentationRequest.jwt.encodedJwt,
162
+ jti: submissionResult.jti,
163
+ submissionId: submissionResult.submissionId,
164
+ presentationDefinitionId: presentation.presentation_definition.id,
165
+ token: submissionResult.sessionToken.value,
166
+ sharedCredentials: credentialsMatch.map(({ id }) => id),
167
+ type: body.type,
168
+ feed: true,
169
+ });
170
+
171
+ return disclosure;
172
+ });
173
+
174
+ const disclosures = await Promise.all(disclosurePromises);
175
+
176
+ return { disclosures };
177
+ }
178
+ );
179
+ };
180
+
181
+ module.exports = feedsController;
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ ...require('./webwallet-get-feeds-response.schema'),
3
+ ...require('./webwallet-share-feed-request-body.schema'),
4
+ ...require('./webwallet-share-feed-response-body.schema'),
5
+ };
@@ -0,0 +1,39 @@
1
+ const webWalletGetFeedsResponseSchema = {
2
+ $id: 'https://velocitycareerlabs.io/webwallet-get-feeds-response.schema.json',
3
+ title: 'webwallet-get-feeds-response',
4
+ description: 'an array of feed objects returned by webwallet get feeds',
5
+ type: 'array',
6
+ items: {
7
+ type: 'object',
8
+ properties: {
9
+ id: { type: 'string' },
10
+ auth0UserId: { type: 'string' },
11
+ authToken: { type: 'object', additionalProperties: true },
12
+ deeplink: { type: 'string' },
13
+ disclosureId: { type: 'string' },
14
+ inspectorDid: { type: 'string' },
15
+ credentialTypes: {
16
+ type: 'array',
17
+ items: { type: 'string' },
18
+ },
19
+ createdAt: { type: 'string' },
20
+ updatedAt: { type: 'string' },
21
+ revokedAt: { type: 'string' },
22
+ },
23
+ required: [
24
+ 'auth0UserId',
25
+ 'authToken',
26
+ 'deeplink',
27
+ 'disclosureId',
28
+ 'inspectorDid',
29
+ 'credentialTypes',
30
+ 'createdAt',
31
+ 'updatedAt',
32
+ ],
33
+ additionalProperties: false,
34
+ },
35
+ };
36
+
37
+ module.exports = {
38
+ webWalletGetFeedsResponseSchema,
39
+ };
@@ -0,0 +1,24 @@
1
+ const webWalletShareFeedRequestBodySchema = {
2
+ $id: 'https://velocitycareerlabs.io/webwalletshare-feed-request-body.schema.json',
3
+ title: 'webwallet-accept-presentation-request',
4
+ description: 'the request schema for webwallet accept presentation request',
5
+ type: 'object',
6
+ properties: {
7
+ credentials: {
8
+ type: 'array',
9
+ items: {
10
+ type: 'object',
11
+ properties: {
12
+ id: { type: 'string' },
13
+ inputDescriptor: { type: 'string' },
14
+ jwtVc: { type: 'string' },
15
+ },
16
+ },
17
+ },
18
+ },
19
+ required: ['credentials'],
20
+ };
21
+
22
+ module.exports = {
23
+ webWalletShareFeedRequestBodySchema,
24
+ };
@@ -0,0 +1,19 @@
1
+ const webWalletShareFeedResponseBodySchema = {
2
+ $id: 'https://velocitycareerlabs.io/webwallet-share-feed-response.schema.json',
3
+ title: 'webwallet-share-feed-response',
4
+ description: 'the response schema for webwallet accept presentation request',
5
+ type: 'object',
6
+ properties: {
7
+ disclosures: {
8
+ type: 'array',
9
+ items: {
10
+ $ref: 'https://velocitycareerlabs.io/webwallet-disclosure.schema.json',
11
+ },
12
+ },
13
+ },
14
+ required: ['disclosures'],
15
+ };
16
+
17
+ module.exports = {
18
+ webWalletShareFeedResponseBodySchema,
19
+ };
@@ -9,7 +9,7 @@ const {
9
9
  VCLVerifiedProfile,
10
10
  VCLDidJwk,
11
11
  } = require('@verii/vnf-nodejs-wallet-sdk');
12
- const { omit, map } = require('lodash/fp');
12
+ const { omit, map, intersection, isEmpty } = require('lodash/fp');
13
13
  const {
14
14
  loadAdditionalRenderInfo,
15
15
  getCredentialsFromOffers,
@@ -173,6 +173,11 @@ const issuingController = async (fastify) => {
173
173
  // eslint-disable-next-line no-unused-vars
174
174
  savedCredentials = await req.repos.credentials.insertMany(credentials);
175
175
  }
176
+ const feeds = await req.repos.feeds.find({
177
+ filter: {
178
+ auth0UserId: req.user.sub,
179
+ },
180
+ });
176
181
 
177
182
  return {
178
183
  passedCredentials:
@@ -180,6 +185,7 @@ const issuingController = async (fastify) => {
180
185
  (credential) => ({
181
186
  ...omit(['_id'], credential),
182
187
  id: credential._id,
188
+ feed: shouldShareViaFeeds(feeds, credential.type),
183
189
  }),
184
190
  savedCredentials
185
191
  ) || [],
@@ -195,6 +201,12 @@ const issuingController = async (fastify) => {
195
201
  ? VCLDidJwk.fromJSON(account.didKeyMetadatum[0])
196
202
  : null;
197
203
  };
204
+
205
+ const shouldShareViaFeeds = (feeds, credentialType) =>
206
+ feeds.some(
207
+ ({ credentialTypes }) =>
208
+ !isEmpty(intersection(credentialTypes, credentialType))
209
+ );
198
210
  };
199
211
 
200
212
  module.exports = issuingController;
@@ -21,6 +21,7 @@ module.exports = (app, _, next = () => {}) => {
21
21
  token: 1,
22
22
  sharedCredentials: 1,
23
23
  type: 1,
24
+ feed: 1,
24
25
  createdAt: 1,
25
26
  updatedAt: 1,
26
27
  deletedAt: 1,
@@ -18,6 +18,7 @@ const webWalletDisclosureSchema = {
18
18
  items: { type: 'string' },
19
19
  },
20
20
  type: { type: 'string', enum: ['public', 'linkedin'] },
21
+ feed: { type: 'boolean' },
21
22
  createdAt: { type: 'string' },
22
23
  updatedAt: { type: 'string' },
23
24
  },
@@ -64,6 +64,9 @@ const webWalletPresentationRequestSchema = {
64
64
  },
65
65
  progress_uri: { type: 'string', format: 'uri' },
66
66
  submit_presentation_uri: { type: 'string', format: 'uri' },
67
+ feed: {
68
+ type: 'boolean',
69
+ },
67
70
  },
68
71
  required: ['client_name', 'logo_uri', 'tos_uri'],
69
72
  },
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ feedsRepoPlugin: require('./repos/feeds.repo'),
3
+ ...require('./schemas'),
4
+ };
@@ -0,0 +1,40 @@
1
+ const {
2
+ autoboxIdsExtension,
3
+ repoFactory,
4
+ } = require('@spencejs/spence-mongo-repos');
5
+
6
+ module.exports = (app, _, next = () => {}) => {
7
+ next();
8
+ return repoFactory(
9
+ {
10
+ name: 'feeds',
11
+ entityName: 'feeds',
12
+ defaultProjection: {
13
+ _id: 1,
14
+ auth0UserId: 1,
15
+ authToken: 1,
16
+ deeplink: 1,
17
+ disclosureId: 1,
18
+ inspectorDid: 1,
19
+ credentialTypes: 1,
20
+ createdAt: 1,
21
+ updatedAt: 1,
22
+ revokedAt: 1,
23
+ },
24
+ extensions: [autoboxIdsExtension, filterRevokedFeedsExtension],
25
+ },
26
+ app
27
+ );
28
+ };
29
+
30
+ const filterRevokedFeedsExtension = (parent) => {
31
+ return {
32
+ prepFilter: (filter) => {
33
+ return parent.prepFilter({
34
+ ...filter,
35
+ revokedAt: filter.revokedAt ?? { $exists: false },
36
+ });
37
+ },
38
+ extensions: parent.extensions.concat(['filterRevokedFeedsExtension']),
39
+ };
40
+ };
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ ...require('./feeds.repo'),
3
+ };
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ ...require('./webwallet-feed.schema'),
3
+ };
@@ -0,0 +1,39 @@
1
+ const webWalletFeedSchema = {
2
+ $id: 'https://velocitycareerlabs.io/webwallet-feed.schema.json',
3
+ title: 'webwallet-feed',
4
+ description: 'The schema of the accepted feed',
5
+ type: 'object',
6
+ properties: {
7
+ id: { type: 'string' },
8
+ auth0UserId: {
9
+ type: 'string',
10
+ },
11
+ authToken: { type: 'object', additionalProperties: true },
12
+ deeplink: { type: 'string' },
13
+ disclosureId: { type: 'string' },
14
+ inspectorDid: { type: 'string' },
15
+ credentialTypes: {
16
+ type: 'array',
17
+ items: { type: 'string' },
18
+ },
19
+ createdAt: { type: 'string' },
20
+ updatedAt: { type: 'string' },
21
+ revokedAt: { type: 'string' },
22
+ },
23
+ required: [
24
+ 'id',
25
+ 'auth0UserId',
26
+ 'authToken',
27
+ 'deeplink',
28
+ 'disclosureId',
29
+ 'inspectorDid',
30
+ 'credentialTypes',
31
+ 'createdAt',
32
+ 'updatedAt',
33
+ ],
34
+ additionalProperties: true,
35
+ };
36
+
37
+ module.exports = {
38
+ webWalletFeedSchema,
39
+ };
@@ -3,4 +3,5 @@ module.exports = {
3
3
  ...require('./credentials'),
4
4
  ...require('./disclosures'),
5
5
  ...require('./issuing'),
6
+ ...require('./feeds'),
6
7
  };
@@ -32,7 +32,11 @@ jest.mock('@verii/vnf-nodejs-wallet-sdk', () => {
32
32
  .fn()
33
33
  .mockResolvedValue(getCredentialManifestMock),
34
34
  submitPresentation: jest.fn().mockResolvedValue(submissionResultMock),
35
- getAuthToken: jest.fn(),
35
+ getAuthToken: jest.fn().mockResolvedValue({
36
+ value: 'mock_auth_token',
37
+ accessToken: { value: 'mock_access_token' },
38
+ refreshToken: { value: 'mock_refresh_token' },
39
+ }),
36
40
  }),
37
41
  },
38
42
  };
@@ -77,6 +81,7 @@ describe('Test disclosure credentials controller', () => {
77
81
  nock.cleanAll();
78
82
  jest.clearAllMocks();
79
83
  await mongoDb().collection('disclosures').deleteMany();
84
+ await mongoDb().collection('feeds').deleteMany();
80
85
  });
81
86
 
82
87
  afterAll(async () => {
@@ -99,8 +104,8 @@ describe('Test disclosure credentials controller', () => {
99
104
 
100
105
  expect(response.statusCode).toBe(200);
101
106
 
102
- expect(vclSdk.getPresentationRequest).toBeCalledTimes(1);
103
- expect(vclSdk.getPresentationRequest).toBeCalledWith(
107
+ expect(vclSdk.getPresentationRequest).toHaveBeenCalledTimes(1);
108
+ expect(vclSdk.getPresentationRequest).toHaveBeenCalledWith(
104
109
  new VCLPresentationRequestDescriptor(
105
110
  new VCLDeepLink(inspectionDeepLink)
106
111
  )
@@ -188,8 +193,8 @@ describe('Test disclosure credentials controller', () => {
188
193
 
189
194
  expect(response.statusCode).toBe(200);
190
195
 
191
- expect(vclSdk.getPresentationRequest).toBeCalledTimes(1);
192
- expect(vclSdk.getPresentationRequest).toBeCalledWith(
196
+ expect(vclSdk.getPresentationRequest).toHaveBeenCalledTimes(1);
197
+ expect(vclSdk.getPresentationRequest).toHaveBeenCalledWith(
193
198
  new VCLPresentationRequestDescriptor(
194
199
  new VCLDeepLink(inspectionDeepLink),
195
200
  null,
@@ -202,8 +207,8 @@ describe('Test disclosure credentials controller', () => {
202
207
  presentationRequestMock.jwt.encodedJwt
203
208
  );
204
209
 
205
- expect(vclSdk.submitPresentation).toBeCalledTimes(1);
206
- expect(vclSdk.submitPresentation).toBeCalledWith(
210
+ expect(vclSdk.submitPresentation).toHaveBeenCalledTimes(1);
211
+ expect(vclSdk.submitPresentation).toHaveBeenCalledWith(
207
212
  expect.objectContaining({
208
213
  exchangeId: submissionResultMock.exchange.id,
209
214
  submitUri: presentationSubmissionMock.submitUri,
@@ -247,6 +252,81 @@ describe('Test disclosure credentials controller', () => {
247
252
  }).toEqual(response.json);
248
253
  });
249
254
 
255
+ it('should accept presentation and create feed when disclosure has feed', async () => {
256
+ const presentationRequestFeedMock = {
257
+ ...require('./mocks').presentationRequestMock,
258
+ feed: true,
259
+ };
260
+
261
+ vclSdk.getPresentationRequest.mockResolvedValueOnce(
262
+ presentationRequestFeedMock
263
+ );
264
+
265
+ const response = await fastify.injectJson({
266
+ method: 'POST',
267
+ url: '/disclosures/accept-presentation-request',
268
+ payload: {
269
+ link: inspectionDeepLink,
270
+ credentials: [
271
+ {
272
+ id: 'credential_id_feed',
273
+ inputDescriptor: 'EducationDegreeGraduationV1.1',
274
+ jwtVc: CredentialMocks.JwtCredentialEmail,
275
+ },
276
+ ],
277
+ },
278
+ headers: {
279
+ authorization: `Bearer ${idToken}`,
280
+ },
281
+ });
282
+
283
+ expect(response.statusCode).toBe(200);
284
+
285
+ expect(vclSdk.getAuthToken).toHaveBeenCalledTimes(1);
286
+
287
+ const savedDisclosure = await mongoDb()
288
+ .collection('disclosures')
289
+ .findOne();
290
+
291
+ expect(savedDisclosure).toEqual({
292
+ _id: expect.any(ObjectId),
293
+ auth0UserId: userId,
294
+ userDid: did,
295
+ presentation: expect.any(Object),
296
+ encodedJwt: presentationRequestFeedMock.jwt.encodedJwt,
297
+ jti: submissionResultMock.jti,
298
+ submissionId: submissionResultMock.submissionId,
299
+ presentationDefinitionId: expect.any(String),
300
+ token: submissionResultMock.sessionToken.value,
301
+ sharedCredentials: ['credential_id_feed'],
302
+ createdAt: expect.any(Date),
303
+ updatedAt: expect.any(Date),
304
+ });
305
+
306
+ const savedFeed = await mongoDb().collection('feeds').findOne();
307
+
308
+ expect(savedFeed).toEqual({
309
+ _id: expect.any(ObjectId),
310
+ auth0UserId: userId,
311
+ authToken: expect.any(Object),
312
+ deeplink: inspectionDeepLink,
313
+ disclosureId: expect.any(String),
314
+ inspectorDid: expect.any(String),
315
+ credentialTypes: expect.any(Array),
316
+ createdAt: expect.any(Date),
317
+ updatedAt: expect.any(Date),
318
+ });
319
+
320
+ expect(response.json).toEqual({
321
+ disclosure: {
322
+ ...omit(['_id', 'auth0UserId', 'userDid'], savedDisclosure),
323
+ id: savedDisclosure._id.toString(),
324
+ updatedAt: savedDisclosure.updatedAt.toISOString(),
325
+ createdAt: savedDisclosure.createdAt.toISOString(),
326
+ },
327
+ });
328
+ });
329
+
250
330
  it('should return 401 if no access token is provided', async () => {
251
331
  const response = await fastify.injectJson({
252
332
  method: 'POST',
@@ -0,0 +1,96 @@
1
+ /* eslint-disable max-len */
2
+ const { register } = require('@spencejs/spence-factories');
3
+ const { feedsRepoPlugin } = require('../../src/entities/feeds');
4
+
5
+ module.exports = (app) => {
6
+ return register(
7
+ 'feeds',
8
+ feedsRepoPlugin(app)({ config: app.config }),
9
+ async (overrides) => {
10
+ return {
11
+ auth0UserId: 'auth0|6798e0e98aea20c2a606d1ca',
12
+ authToken: {
13
+ payload: {
14
+ access_token:
15
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiIjZXhjaGFuZ2Uta2V5LTEifQ.eyJuYmYiOjE3NTI0OTAyMDksImp0aSI6IlJGYXR4Z1VCN2hqOEJrbmxYSXpjRyIsImlzcyI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImF1ZCI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImV4cCI6MTc1MzA5NTAwOSwic3ViIjoiNjM4NmY4MjRlNzc0Nzg5YzQwM2M5NmEwIiwiaWF0IjoxNzUyNDkwMjA5fQ.ajqMpwmuIWzgHN-RsMpp7MFSA9EHVPtYST68TTexUCfGthX2oX-ocT2j0SrKJO931OH4DyXzDzAE6oZGrAtCnw',
16
+ token_type: 'Bearer',
17
+ refresh_token:
18
+ '211c1835e9845272f85bff7b2d3700f10e6dcedf926688bcea9b356059aeb99078d8155aca156df6a059d5b88ae66fdd6a979dd04767b120e3f94eb28352703e',
19
+ },
20
+ authTokenUri:
21
+ 'https://devagent.velocitycareerlabs.io/api/holder/v0.6/org/did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu/oauth/token',
22
+ walletDid:
23
+ 'did:jwk:eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJCbnhJcVBBUFB5RTNOVTJEejhaUnhIQUhIQW9aWmFUem95Ry1ONW1va2pjIiwieSI6ImQ3aEhUNlNqbVdOYkdpYnhzVnI3UThEVU1sbVNSY245bGFyUG04R3haTXMifQ',
24
+ relyingPartyDid:
25
+ 'did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu',
26
+ accessToken: {
27
+ value:
28
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiIjZXhjaGFuZ2Uta2V5LTEifQ.eyJuYmYiOjE3NTI0OTAyMDksImp0aSI6IlJGYXR4Z1VCN2hqOEJrbmxYSXpjRyIsImlzcyI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImF1ZCI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImV4cCI6MTc1MzA5NTAwOSwic3ViIjoiNjM4NmY4MjRlNzc0Nzg5YzQwM2M5NmEwIiwiaWF0IjoxNzUyNDkwMjA5fQ.ajqMpwmuIWzgHN-RsMpp7MFSA9EHVPtYST68TTexUCfGthX2oX-ocT2j0SrKJO931OH4DyXzDzAE6oZGrAtCnw',
29
+ jwtValue: {
30
+ signedJwt: {
31
+ header:
32
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiIjZXhjaGFuZ2Uta2V5LTEifQ',
33
+ payload:
34
+ 'eyJuYmYiOjE3NTI0OTAyMDksImp0aSI6IlJGYXR4Z1VCN2hqOEJrbmxYSXpjRyIsImlzcyI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImF1ZCI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImV4cCI6MTc1MzA5NTAwOSwic3ViIjoiNjM4NmY4MjRlNzc0Nzg5YzQwM2M5NmEwIiwiaWF0IjoxNzUyNDkwMjA5fQ',
35
+ signature:
36
+ 'ajqMpwmuIWzgHN-RsMpp7MFSA9EHVPtYST68TTexUCfGthX2oX-ocT2j0SrKJO931OH4DyXzDzAE6oZGrAtCnw',
37
+ },
38
+ encodedJwt:
39
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksiLCJraWQiOiIjZXhjaGFuZ2Uta2V5LTEifQ.eyJuYmYiOjE3NTI0OTAyMDksImp0aSI6IlJGYXR4Z1VCN2hqOEJrbmxYSXpjRyIsImlzcyI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImF1ZCI6ImRpZDp3ZWI6ZGV2cmVnaXN0cmFyLnZlbG9jaXR5bmV0d29yay5mb3VuZGF0aW9uOmQ6d3d3LnVtYXNzLmVkdSIsImV4cCI6MTc1MzA5NTAwOSwic3ViIjoiNjM4NmY4MjRlNzc0Nzg5YzQwM2M5NmEwIiwiaWF0IjoxNzUyNDkwMjA5fQ.ajqMpwmuIWzgHN-RsMpp7MFSA9EHVPtYST68TTexUCfGthX2oX-ocT2j0SrKJO931OH4DyXzDzAE6oZGrAtCnw',
40
+ header: {
41
+ typ: 'JWT',
42
+ alg: 'ES256K',
43
+ kid: '#exchange-key-1',
44
+ },
45
+ payload: {
46
+ nbf: 1752490209,
47
+ jti: 'RFatxgUB7hj8BknlXIzcG',
48
+ iss: 'did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu',
49
+ aud: 'did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu',
50
+ exp: 1753095009,
51
+ sub: '6386f824e774789c403c96a0',
52
+ iat: 1752490209,
53
+ },
54
+ signature:
55
+ 'ajqMpwmuIWzgHN-RsMpp7MFSA9EHVPtYST68TTexUCfGthX2oX-ocT2j0SrKJO931OH4DyXzDzAE6oZGrAtCnw',
56
+ },
57
+ },
58
+ refreshToken: {
59
+ value:
60
+ '211c1835e9845272f85bff7b2d3700f10e6dcedf926688bcea9b356059aeb99078d8155aca156df6a059d5b88ae66fdd6a979dd04767b120e3f94eb28352703e',
61
+ jwtValue: {
62
+ signedJwt: {
63
+ header: '',
64
+ payload: '',
65
+ signature: '',
66
+ },
67
+ encodedJwt: '',
68
+ header: {},
69
+ payload: {},
70
+ signature: '',
71
+ },
72
+ },
73
+ tokenType: 'Bearer',
74
+ },
75
+ deeplink:
76
+ 'velocity-network-devnet://inspect?request_uri=https://devagent.velocitycareerlabs.io/api/holder/v0.6/org/did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu/inspect/get-presentation-request?id=68736ac609c41e44796d040e%26inspectorDid%3Ddid%3Aweb%3Adevregistrar.velocitynetwork.foundation%3Ad%3Awww.umass.edu%26vendorOriginContext%3DAdam',
77
+ disclosureId: '68736ac609c41e44796d040e',
78
+ inspectorDid:
79
+ 'did:web:devregistrar.velocitynetwork.foundation:d:www.umass.edu',
80
+ credentialTypes: [
81
+ 'CertificationV1.1',
82
+ 'LicenseV1.1',
83
+ 'AssessmentV1.1',
84
+ 'OpenBadgeCredential',
85
+ ],
86
+ createdAt: {
87
+ $date: '2025-07-14T10:52:35.522Z',
88
+ },
89
+ updatedAt: {
90
+ $date: '2025-07-14T10:52:35.522Z',
91
+ },
92
+ ...overrides(),
93
+ };
94
+ }
95
+ );
96
+ };
@@ -0,0 +1,124 @@
1
+ const { mongoDb } = require('@spencejs/spence-mongo-repos');
2
+ const nock = require('nock');
3
+ const buildFastify = require('./helpers/webwallet-build-fastify');
4
+ const initFeedsFactory = require('./factories/feeds');
5
+
6
+ const auth0Token =
7
+ // eslint-disable-next-line max-len
8
+ 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0LyIsImF1ZCI6ImZvbyIsInNjb3BlIjoicmVhZDp1c2VycyJ9.VwIIUqx9T-AxqbfL_FyNRAeOxTwiC2JpcwtrqnEWN3DdF07ijUkF1WYy8Ahfr_p4R3KnoPbiefZnIbVANCt-lt0ej32rfil2yHhQEsvFxSOjcrx6ARmPp0YAfWlN-5Sotzkxy29jaOZMEDkmRFZg3jkdC7wosPW_S6M-olC4g3HHfylpZI8O3Jdd87Qr9wD_QtUzANwnPbl2Q-9NEyxVjAZIWg_HWK9JAAaf_2IY5VwHBvyp0oeQSEHKi4hogcM59EOc4FxdR5WH45B_PenVa6W4mHFBkH8sAXxt2Zs9s2efujkfWYfyXvgL_lN7vT-TEADlAPP2L6CpWpDISOMsQWUSgGHcN_KwRh_E7qJwahR6mv4QHY6ReEoyjkmSS3swrD1l74jNs7QLAdsMywvzCMDsHabs7DYcEMGQBdP14PJ_ucLFnkivZeBDAc6sS445ocbyrpyO40XMaMorD5khRd9ej89SxR7d_v0W6Ne2Nn4XgW3pAZzu5Rdc4JvqfzLFxkp95jxy1MTAddjWISPmNOYYyXHM9SSqSpqVECOFS0f4z2zycHRqXUcOytWrvED6VGo9x7-IVCgu8vFzj0zToIWQmsDs3UoH9RnV12z0PMwGXQzca1lT_zGwJxBF3e4zJjmcJ05OMF2JgZ2_G48O3M4Dtb0jlgWbKLd0kWlIFzQ;';
9
+
10
+ const auth0UserId = '1234567890';
11
+
12
+ jest.mock('@verii/vnf-nodejs-wallet-sdk', () => {
13
+ const originalModule = jest.requireActual('@verii/vnf-nodejs-wallet-sdk');
14
+ return {
15
+ ...originalModule,
16
+ VCLProvider: {
17
+ getInstance: jest.fn().mockReturnValue({
18
+ initialize: jest.fn().mockResolvedValue(null),
19
+ }),
20
+ },
21
+ };
22
+ });
23
+ describe('Feeds Controller', () => {
24
+ let fastify;
25
+ let persistFeeds;
26
+
27
+ beforeAll(async () => {
28
+ fastify = buildFastify();
29
+ nock('https://localhost/').get('/.well-known/jwks.json').reply(200, jwks);
30
+ await fastify.ready();
31
+ await mongoDb().collection('feeds').deleteMany({});
32
+ ({ persistFeeds } = initFeedsFactory(fastify));
33
+ });
34
+
35
+ afterEach(async () => {
36
+ nock.cleanAll();
37
+ await mongoDb().collection('feeds').deleteMany({});
38
+ });
39
+
40
+ afterAll(async () => {
41
+ nock.restore();
42
+ await fastify.close();
43
+ });
44
+
45
+ describe('getFeeds', () => {
46
+ it('should return list of feeds on success', async () => {
47
+ await persistFeeds({
48
+ auth0UserId,
49
+ });
50
+
51
+ const response = await fastify.inject({
52
+ method: 'GET',
53
+ url: '/feeds',
54
+ headers: {
55
+ authorization: `Bearer ${auth0Token}`,
56
+ },
57
+ });
58
+
59
+ expect(response.statusCode).toBe(200);
60
+ expect(response.json()).toHaveLength(1);
61
+ });
62
+ });
63
+
64
+ describe('deleteFeed', () => {
65
+ it('should delete feed when exists', async () => {
66
+ const feed = await persistFeeds({
67
+ auth0UserId,
68
+ });
69
+
70
+ const response = await fastify.inject({
71
+ method: 'DELETE',
72
+ url: `/feeds/${feed._id}`,
73
+ headers: {
74
+ authorization: `Bearer ${auth0Token}`,
75
+ },
76
+ });
77
+
78
+ expect(response.statusCode).toBe(204);
79
+ });
80
+ });
81
+ });
82
+
83
+ const jwks = {
84
+ keys: [
85
+ {
86
+ alg: 'RS512',
87
+ kid: 'KEY',
88
+ x5c: ['UNUSED'],
89
+ },
90
+ {
91
+ alg: 'RS256',
92
+ kid: 'KEY',
93
+ x5c: [
94
+ `
95
+ MIIEnjCCAoYCCQDmnGII6qzGlTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZ1
96
+ bnVzZWQwHhcNMjEwOTE5MDcxODQ2WhcNMjExMDE5MDcxODQ2WjARMQ8wDQYDVQQD
97
+ DAZ1bnVzZWQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDY2acJ8JAH
98
+ XjK8j3sAXOokSWwvaEg65UJS0C7KdnfbLTeaeYFHBRY0v9Jkk/PJSXv9hMWw1aD3
99
+ n7NrhVwXeRXi/7VZuW/S4ek+hK+IMDvpKqzn+XeCpaMoRpAgloADeNY0qhYKxpr2
100
+ L0SmRQDwVy1r/g31ECewD2WpEiRSmXsQ2Q2uYT3V60BmbhUw31KGEr4SLXL9pzmb
101
+ arOH/5Rhqg+YFMywNY6i01S3UdOlUtAyWT/mVRAkVTsUEou9tBw61zputPdMrl7p
102
+ d4InmlfCmXNTPFh9EDczPQiAVPq8MDyEdRGP134+HM9+YgQUZjy+WsxmGEvplJIf
103
+ KrYtlWe11//oAXC3690LUYtvg49lNY4+H0/nngjnCDXkZo6f+PEvnBZfYl596VTV
104
+ Fx4FGNOqLwg4bAyE5j5jXtEGW1oKo1pxBg7Oe3MteQUDwMrONB3CbxdxDiN3YH94
105
+ 2nWGW9Le+CeA1QUhfjQqSoZRJURGYGoztVuIXOElnkrgwcJreX4b8y+Uo5kpp2By
106
+ 6UUaD/mMj9XQ+Ygp/J8DlJlqDXOIp6JUJ75aSK5ZIjRtWq/Go5RUjW9IW0ldEehh
107
+ /4j0ZWC0vR1/le2UmAE6tXhkH4sdx9JM84V+qRzjiGqQx3Wn00uwMiHHhte17t41
108
+ vk+b75wuHbfiq9R8irL6wqWeeuzvCC37NwIDAQABMA0GCSqGSIb3DQEBCwUAA4IC
109
+ AQCZOT6S5HLUp0gBtWK6Fxyzb9lWPE+AJipjJ80lS3OnaOIyVtyJexJ2BjTKWldJ
110
+ 48zkzLNIRsTEGEipNS6NkrkElfmoaNBpdbDch2WBkME3UYlFIR9GgbXPMlACQlwJ
111
+ f4qT3iIZ9zjpVMP8F63TzRRr7KEYW2PGHEtVQktMPprGEfU4Sz0OIa9RRV+BLsfh
112
+ x8he2pimJEzoEaWPgyJXV1S+tLUif8A/CEkZVRZ2vADA7WMGl2ZTdbmsTjXh4bf2
113
+ A4l4Zec+jwOjUPiEk5lLJwv1KeYos9wuUczAk7ku8wRzyZbrjwgRam9VQA5qmRzJ
114
+ PegEQwJaKMRu8PPK0L4KFN4v3ma7Ux6D719nko8mZ0kA2oUs6phsFmoWQfsbRbsD
115
+ CdUGnM2fPp6V285r9w9Y6+1nVdtJpbAPFJR3SxIpfYVfx3tI6C3BR9bIMr8uCf81
116
+ G+Ebvo4qTuY6Cg/mTpPLZ4cKpwSvB6cE+xeSHvKIRYm6QUYEReRxQ3b4aUKBSNwl
117
+ FEQufVGhGzeblNC3fjP+mMtqbyC8c1zkHc6tjJYO5yesKf8bjO71by2jgSced7nN
118
+ 5JvJawfEcabgHJ1aAFLj0tlPMrViFzu6y8/A5aZLc5UMISXAZXfB4wIEdyUUXJh4
119
+ rJKE/ZCb2+W8g29N5cv2P6nhahT3mYatMiQ0U/gfaIrA0A==
120
+ `.trim(),
121
+ ],
122
+ },
123
+ ],
124
+ };
@@ -310,6 +310,7 @@ describe('Test Deep Link issuing controller', () => {
310
310
  expect({
311
311
  ...omit(['_id'], savedCredential),
312
312
  id: savedCredential._id.toHexString(),
313
+ feed: false,
313
314
  updatedAt: savedCredential.updatedAt.toISOString(),
314
315
  createdAt: savedCredential.createdAt.toISOString(),
315
316
  }).toEqual(mockParsedResponse.passedCredentials[0]);
@@ -59,6 +59,7 @@ const mockParsedResponse = {
59
59
  vnfProtocolVersion: 1,
60
60
  encodedCredential:
61
61
  mockAcceptedCredentials.passedCredentials[0].encodedJwt,
62
+ feed: false,
62
63
  },
63
64
  ],
64
65
  failedCredentials: [],