@idos-network/credentials 1.5.0 → 2.2.0

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.
Files changed (82) hide show
  1. package/README.md +289 -85
  2. package/assets/idos-credential-subject-edd-v1.json +10 -0
  3. package/assets/idos-credential-subject-v1.json +1 -1
  4. package/assets/idos-credential-subject-v2.json +1 -1
  5. package/assets/idos-credential-subject-v3.json +83 -0
  6. package/assets/idos-credentials-v1.json +2 -2
  7. package/assets/idos-credentials-v2.json +10 -0
  8. package/dist/index-0NqXMjNk.d.mts +395 -0
  9. package/dist/index-0NqXMjNk.d.mts.map +1 -0
  10. package/dist/index-WW372Tf5.d.mts +57 -0
  11. package/dist/index-WW372Tf5.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -1
  13. package/dist/loader-Cg6hs1-P.mjs +2 -0
  14. package/dist/loader-Cg6hs1-P.mjs.map +1 -0
  15. package/dist/parser/index.d.mts +19 -0
  16. package/dist/parser/index.d.mts.map +1 -0
  17. package/dist/parser/index.mjs +2 -0
  18. package/dist/parser/index.mjs.map +1 -0
  19. package/dist/schemas/index.d.mts +2 -0
  20. package/dist/schemas/index.mjs +1 -0
  21. package/dist/schemas-BVGuUlPe.mjs +2 -0
  22. package/dist/schemas-BVGuUlPe.mjs.map +1 -0
  23. package/dist/types/index.d.mts +2 -134
  24. package/dist/types/index.mjs +1 -63
  25. package/dist/utils/index.d.mts +37 -15
  26. package/dist/utils/index.d.mts.map +1 -1
  27. package/dist/utils/index.mjs +1 -116
  28. package/dist/utils/index.mjs.map +1 -1
  29. package/dist/verifier/index.d.mts +8 -0
  30. package/dist/verifier/index.d.mts.map +1 -0
  31. package/dist/verifier/index.mjs +2 -0
  32. package/dist/verifier/index.mjs.map +1 -0
  33. package/package.json +21 -9
  34. package/src/parser/index.test.ts +266 -0
  35. package/src/parser/index.ts +69 -0
  36. package/src/schemas/EddV1/index.test.ts +40 -0
  37. package/src/schemas/EddV1/index.ts +53 -0
  38. package/src/schemas/Envelope.ts +37 -0
  39. package/src/schemas/FaceIdV1/index.test.ts +26 -0
  40. package/src/schemas/FaceIdV1/index.ts +33 -0
  41. package/src/schemas/KycV1/IdDocument.ts +41 -0
  42. package/src/schemas/KycV1/ResidentialAddress.ts +56 -0
  43. package/src/schemas/KycV1/Subject.ts +86 -0
  44. package/src/schemas/KycV1/index.test.ts +156 -0
  45. package/src/schemas/KycV1/index.ts +40 -0
  46. package/src/schemas/KycV2/Subject.ts +67 -0
  47. package/src/schemas/KycV2/index.test.ts +162 -0
  48. package/src/schemas/KycV2/index.ts +45 -0
  49. package/src/schemas/KycV3/Biometric.ts +14 -0
  50. package/src/schemas/KycV3/Contact.ts +13 -0
  51. package/src/schemas/KycV3/EDD.ts +32 -0
  52. package/src/schemas/KycV3/IdDocument.ts +92 -0
  53. package/src/schemas/KycV3/Onboarding.ts +36 -0
  54. package/src/schemas/KycV3/Person.ts +105 -0
  55. package/src/schemas/KycV3/ResidentialAddress.ts +73 -0
  56. package/src/schemas/KycV3/Screening.ts +27 -0
  57. package/src/schemas/KycV3/SourceOfWealth.ts +44 -0
  58. package/src/schemas/KycV3/Subject.ts +54 -0
  59. package/src/schemas/KycV3/index.test.ts +682 -0
  60. package/src/schemas/KycV3/index.ts +108 -0
  61. package/src/schemas/VerifiableCredential.ts +232 -0
  62. package/src/schemas/enums.ts +126 -0
  63. package/src/schemas/index.ts +14 -0
  64. package/src/schemas/integration.test.ts +251 -0
  65. package/src/schemas/types.ts +11 -0
  66. package/src/schemas/utils.ts +274 -0
  67. package/src/types/credentials.ts +18 -249
  68. package/src/utils/{utils.test.ts → index.test.ts} +59 -67
  69. package/src/utils/index.ts +129 -134
  70. package/src/utils/issuer.ts +42 -0
  71. package/src/{builder → utils}/loader.ts +22 -17
  72. package/src/verifier/index.test.ts +149 -0
  73. package/src/{builder/verifier.ts → verifier/index.ts} +4 -8
  74. package/types/internals.d.ts +11 -6
  75. package/dist/builder/index.d.mts +0 -19
  76. package/dist/builder/index.d.mts.map +0 -1
  77. package/dist/builder/index.mjs +0 -619
  78. package/dist/builder/index.mjs.map +0 -1
  79. package/dist/types/index.d.mts.map +0 -1
  80. package/dist/types/index.mjs.map +0 -1
  81. package/src/builder/builder.test.ts +0 -407
  82. package/src/builder/index.ts +0 -122
@@ -0,0 +1,682 @@
1
+ import { fileToBase85 } from "@idos-network/utils/codecs";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import type { VerifiableCredential } from "../../types";
5
+
6
+ import { KycV3 } from ".";
7
+
8
+ /** A credential with the required sections filled in, ready to be extended per test. */
9
+ function basicCredential(): KycV3 {
10
+ const credential = new KycV3();
11
+
12
+ credential.setEnvelope({
13
+ id: "https://issuer.example/credentials/123",
14
+ issued: new Date("2022-01-01"),
15
+ approvedAt: new Date("2022-01-02"),
16
+ expirationDate: new Date("2030-01-01"),
17
+ });
18
+
19
+ credential.setSubject({ id: "uuid:abc" });
20
+ credential.addSection("person", {
21
+ firstName: "John",
22
+ familyName: "Doe",
23
+ nationality: "US",
24
+ dateOfBirth: new Date("1990-01-01"),
25
+ });
26
+ credential.addSection("idDocument", {
27
+ type: "PASSPORT",
28
+ number: "123456789",
29
+ country: "US",
30
+ frontFile: Buffer.from("Front"),
31
+ frontFileType: "image/jpeg",
32
+ frontFileName: "front.jpg",
33
+ });
34
+
35
+ return credential;
36
+ }
37
+
38
+ /** Wraps a flat subject into a credential of the shape `deserialize` accepts. */
39
+ function serializedCredential(
40
+ credentialSubject: Record<string, unknown>,
41
+ ): VerifiableCredential<unknown> {
42
+ return {
43
+ "@context": [],
44
+ type: ["VerifiableCredential"],
45
+ issuer: "did:example:issuer",
46
+ id: "https://issuer.example/credentials/123",
47
+ level: "basic+liveness",
48
+ kycLevel: 1,
49
+ issued: new Date("2022-01-01").toISOString(),
50
+ approvedAt: new Date("2022-01-02").toISOString(),
51
+ expirationDate: new Date("2030-01-01").toISOString(),
52
+ issuanceDate: new Date("2022-01-01").toISOString(),
53
+ credentialSubject,
54
+ proof: {
55
+ type: "Ed25519Signature2020",
56
+ created: new Date("2022-01-01").toISOString(),
57
+ verificationMethod: "did:example:issuer#key-1",
58
+ proofValue: "proof",
59
+ proofPurpose: "assertionMethod",
60
+ },
61
+ } as unknown as VerifiableCredential<unknown>;
62
+ }
63
+
64
+ describe("KycV3 validity", () => {
65
+ it("is valid with required fields", () => {
66
+ const credential = basicCredential();
67
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
68
+
69
+ expect(() => credential.checkValidity()).not.toThrow();
70
+ expect(credential.level()).toBe("basic+liveness");
71
+ });
72
+
73
+ it("can issue a level = 0 credentials", async () => {
74
+ const credential = new KycV3();
75
+ credential.setEnvelope({ id: "https://issuer.example/credentials/123" });
76
+ credential.setSubject({ id: "uuid:abc" });
77
+ credential.addSection("person", {
78
+ firstName: "John",
79
+ dateOfBirth: new Date("1990-01-01"),
80
+ nationality: "US",
81
+ });
82
+ credential.addSection("residentialAddress", {
83
+ source: "MANUAL",
84
+ street: "Main St",
85
+ city: "New York",
86
+ country: "US",
87
+ });
88
+ credential.addSection("contact", { phoneNumber: "+1234567890" });
89
+
90
+ expect(() => credential.checkValidity()).not.toThrow();
91
+ expect(credential.kycLevel()).toBe(0);
92
+ expect(credential.level()).toBe("unverified+phoneNumber");
93
+ });
94
+
95
+ it("rejects a subject missing a required section", () => {
96
+ const credential = new KycV3();
97
+ credential.setEnvelope({ id: "https://issuer.example/credentials/123" });
98
+ credential.setSubject({ id: "uuid:abc" });
99
+
100
+ expect(() => credential.checkValidity()).toThrow(/person/);
101
+ });
102
+
103
+ it("requires one of firstName or familyName", () => {
104
+ expect(() =>
105
+ new KycV3().addSection("person", { nationality: "US", dateOfBirth: new Date("1990-01-01") }),
106
+ ).toThrow(/firstName/);
107
+
108
+ expect(() =>
109
+ new KycV3().addSection("person", {
110
+ familyName: "Doe",
111
+ nationality: "US",
112
+ dateOfBirth: new Date("1990-01-01"),
113
+ }),
114
+ ).not.toThrow();
115
+ });
116
+
117
+ it("requires a nationality unless stateless or holding a second one", () => {
118
+ expect(() =>
119
+ new KycV3().addSection("person", { firstName: "John", dateOfBirth: new Date("1990-01-01") }),
120
+ ).toThrow(/nationality/);
121
+
122
+ expect(() =>
123
+ new KycV3().addSection("person", {
124
+ firstName: "John",
125
+ stateless: true,
126
+ dateOfBirth: new Date("1990-01-01"),
127
+ }),
128
+ ).not.toThrow();
129
+
130
+ expect(() =>
131
+ new KycV3().addSection("person", {
132
+ firstName: "John",
133
+ secondNationality: "GB",
134
+ dateOfBirth: new Date("1990-01-01"),
135
+ }),
136
+ ).not.toThrow();
137
+ });
138
+
139
+ it("requires proof fields on a verified address, but still checks their types when unverified", () => {
140
+ expect(() =>
141
+ new KycV3().addSection("residentialAddress", {
142
+ source: "PROOF_OF_ADDRESS",
143
+ street: "Main St",
144
+ city: "New York",
145
+ country: "US",
146
+ }),
147
+ ).toThrow(/proofCategory|proofFile/);
148
+
149
+ expect(() =>
150
+ new KycV3().addSection("residentialAddress", {
151
+ source: "MANUAL",
152
+ street: "Oak Ave",
153
+ city: "Boston",
154
+ country: "US",
155
+ }),
156
+ ).not.toThrow();
157
+
158
+ expect(() =>
159
+ new KycV3().addSection("residentialAddress", {
160
+ source: "MANUAL",
161
+ street: "Oak Ave",
162
+ city: "Boston",
163
+ country: "US",
164
+ proofFile: "not-a-buffer" as never,
165
+ proofFileType: "image/jpeg",
166
+ proofFileName: "proof.jpg",
167
+ }),
168
+ ).toThrow(/proofFile/);
169
+ });
170
+
171
+ it("accepts an external-database address without proof, but does not count it as plus", () => {
172
+ const credential = basicCredential();
173
+ credential.addSection("residentialAddress", {
174
+ source: "EXTERNAL_DB",
175
+ street: "Main St",
176
+ city: "New York",
177
+ country: "US",
178
+ });
179
+
180
+ expect(() => credential.checkValidity()).not.toThrow();
181
+ expect(credential.level()).toBe("basic");
182
+
183
+ expect(() =>
184
+ new KycV3().addSection("residentialAddress", {
185
+ source: "bogus" as never,
186
+ street: "Main St",
187
+ city: "New York",
188
+ country: "US",
189
+ }),
190
+ ).toThrow(/source/);
191
+ });
192
+
193
+ it("requires a currency alongside each source-of-wealth amount", () => {
194
+ expect(() =>
195
+ new KycV3().addSection("sourceOfWealth", {
196
+ type: "SALARY",
197
+ yearlyGrossIncome: "LESS_THAN_20000",
198
+ }),
199
+ ).toThrow(/yearlyGrossIncomeCurrency/);
200
+
201
+ expect(() =>
202
+ new KycV3().addSection("sourceOfWealth", {
203
+ type: "SALARY",
204
+ approximateNetWorth: "UP_TO_25000",
205
+ }),
206
+ ).toThrow(/approximateNetWorthCurrency/);
207
+
208
+ expect(() =>
209
+ new KycV3().addSection("sourceOfWealth", {
210
+ type: "SALARY",
211
+ yearlyGrossIncome: "LESS_THAN_20000",
212
+ yearlyGrossIncomeCurrency: "EUR",
213
+ }),
214
+ ).not.toThrow();
215
+ });
216
+
217
+ it("rejects values outside an enum, and out-of-range scores", () => {
218
+ expect(() => new KycV3().addSection("edd", { occupation: "ASTRONAUT" as never })).toThrow(
219
+ /occupation/,
220
+ );
221
+
222
+ expect(() =>
223
+ new KycV3().addSection("screening", {
224
+ sanctionsCheckResult: "CLEAR",
225
+ pepCheckResult: "CLEAR",
226
+ pepConfidenceScore: 101,
227
+ }),
228
+ ).toThrow(/pepConfidenceScore/);
229
+ });
230
+
231
+ it("rejects a field that's not part of the schema", () => {
232
+ expect(() =>
233
+ new KycV3().addSection("person", {
234
+ firstName: "John",
235
+ nationality: "US",
236
+ dateOfBirth: new Date("1990-01-01"),
237
+ // @ts-expect-error - this field is not part of the schema
238
+ unexpectedJsonLdField: "breaks-json-ld",
239
+ }),
240
+ ).toThrow(/unexpectedJsonLdField/);
241
+ });
242
+
243
+ it("rejects an unparseable date", () => {
244
+ expect(() =>
245
+ new KycV3().addSection("person", {
246
+ firstName: "John",
247
+ nationality: "US",
248
+ dateOfBirth: "not-a-date" as never,
249
+ }),
250
+ ).toThrow(/dateOfBirth/);
251
+ });
252
+
253
+ it("checks sections for extra fields even when validation is skipped", () => {
254
+ const cases = [
255
+ { section: "contact", knownFields: { email: 123 } },
256
+ { section: "biometric", knownFields: { selfieFile: "not-a-buffer" } },
257
+ { section: "residentialAddress", knownFields: { street: 123 } },
258
+ { section: "screening", knownFields: { sanctionsCheckResult: "not-a-real-result" } },
259
+ { section: "onboarding", knownFields: { employmentStatus: "not-a-real-status" } },
260
+ { section: "edd", knownFields: { occupation: "not-a-real-occupation" } },
261
+ { section: "sourceOfWealth", knownFields: { type: "not-a-real-type" } },
262
+ ] as const;
263
+
264
+ for (const { section, knownFields } of cases) {
265
+ expect(() => new KycV3().addSection(section, knownFields as never, false)).not.toThrow();
266
+
267
+ expect(() =>
268
+ new KycV3().addSection(
269
+ section,
270
+ { ...knownFields, unexpectedJsonLdField: "breaks-json-ld" } as never,
271
+ false,
272
+ ),
273
+ ).toThrow(`Unexpected ${section} fields: unexpectedJsonLdField`);
274
+ }
275
+ });
276
+
277
+ it("accepts a date in either form, and leaves omitted fields alone", () => {
278
+ const credential = basicCredential();
279
+ credential.setEnvelope({
280
+ id: "https://issuer.example/credentials/123",
281
+ issued: new Date("2022-01-01").toISOString() as never,
282
+ });
283
+
284
+ expect(credential.envelope.issued).toEqual(new Date("2022-01-01"));
285
+ // A field left out of the second call keeps the value the first one set.
286
+ expect(credential.envelope.approvedAt).toEqual(new Date("2022-01-02"));
287
+ expect(credential.envelope.level).toBeUndefined();
288
+ });
289
+
290
+ it("rejects unknown envelope fields", () => {
291
+ expect(() => new KycV3().setEnvelope({ id: "x", bogus: 1 } as never)).toThrow(
292
+ "Unexpected envelope fields: bogus",
293
+ );
294
+ });
295
+ });
296
+
297
+ describe("KycV3 derived fields", () => {
298
+ it("derives level and kycLevel on serialize when omitted from the envelope", () => {
299
+ const credential = basicCredential();
300
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
301
+ credential.addSection("contact", { email: "john@example.com" });
302
+
303
+ expect(credential.envelope.level).toBeUndefined();
304
+ expect(credential.envelope.kycLevel).toBeUndefined();
305
+
306
+ credential.serializeSubject();
307
+
308
+ expect(credential.envelope.level).toBe("basic+liveness+email");
309
+ expect(credential.envelope.kycLevel).toBe(1);
310
+ });
311
+
312
+ it("derives expirationDate from the document when the caller omits it", () => {
313
+ const credential = new KycV3();
314
+ credential.setEnvelope({ id: "https://issuer.example/credentials/123" });
315
+ credential.setSubject({ id: "uuid:abc" });
316
+ credential.addSection("person", {
317
+ firstName: "John",
318
+ nationality: "US",
319
+ dateOfBirth: new Date("1990-01-01"),
320
+ });
321
+ credential.addSection("idDocument", {
322
+ type: "PASSPORT",
323
+ number: "123456789",
324
+ country: "US",
325
+ dateOfExpiry: new Date("2030-06-01"),
326
+ frontFile: Buffer.from("Front"),
327
+ frontFileType: "image/jpeg",
328
+ frontFileName: "front.jpg",
329
+ });
330
+
331
+ credential.checkValidity();
332
+
333
+ expect(credential.envelope.expirationDate).toEqual(new Date("2030-06-01"));
334
+ });
335
+
336
+ it("reaches plus and kycLevel 3 with a verified address, SOW and onboarding", () => {
337
+ const credential = basicCredential();
338
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
339
+ credential.addSection("residentialAddress", {
340
+ source: "PROOF_OF_ADDRESS",
341
+ street: "Main St",
342
+ city: "New York",
343
+ country: "US",
344
+ proofCategory: "UTILITY_BILL",
345
+ proofDateOfIssue: new Date("2021-01-01"),
346
+ proofFile: Buffer.from("Proof"),
347
+ proofFileType: "image/jpeg",
348
+ proofFileName: "proof.jpg",
349
+ });
350
+ credential.addSection("sourceOfWealth", { type: "SALARY" });
351
+ credential.addSection("onboarding", {
352
+ intendedUse: "INVESTING",
353
+ employmentStatus: "EMPLOYED",
354
+ expectedMonthlyTransactionCount: "LESS_THAN_5",
355
+ expectedMonthlyTransactionVolume: "MORE_THAN_500_LESS_THAN_2000",
356
+ expectedMonthlyTransactionVolumeCurrency: "EUR",
357
+ });
358
+
359
+ expect(credential.level()).toBe("plus+liveness+sow+onboarding");
360
+ expect(credential.kycLevel()).toBe(3);
361
+
362
+ expect(credential.publicNotes()).toEqual({
363
+ type: "kyc",
364
+ level: "plus+liveness+sow+onboarding",
365
+ kycLevel: 3,
366
+ });
367
+ });
368
+ });
369
+
370
+ describe("KycV3 serialization", () => {
371
+ it("flattens every section into the credential subject", () => {
372
+ const credential = new KycV3();
373
+
374
+ credential.setEnvelope({
375
+ id: "https://issuer.example/credentials/123",
376
+ issued: new Date("2022-01-01"),
377
+ approvedAt: new Date("2022-01-02"),
378
+ expirationDate: new Date("2030-01-01"),
379
+ });
380
+
381
+ credential.setSubject({ id: "uuid:abc" });
382
+ credential.addSection("person", {
383
+ firstName: "John",
384
+ familyName: "Doe",
385
+ gender: "M",
386
+ nationality: "US",
387
+ dateOfBirth: new Date("1990-01-01"),
388
+ placeOfBirth: "New York, NY",
389
+ });
390
+ credential.addSection("idDocument", {
391
+ type: "PASSPORT",
392
+ number: "123456789",
393
+ country: "US",
394
+ dateOfIssue: new Date("2020-01-01"),
395
+ dateOfExpiry: new Date("2030-01-01"),
396
+ issuingAuthority: "US Department of State",
397
+ frontFile: Buffer.from("Front"),
398
+ frontFileType: "image/jpeg",
399
+ frontFileName: "front.jpg",
400
+ backFile: Buffer.from("Back"),
401
+ backFileType: "image/jpeg",
402
+ backFileName: "back.jpg",
403
+ // cspell:disable-next-line
404
+ mrzLine1: "P<USADOE<<JOHN<<<<<<<<<<<<<<<<<<<<<<<<",
405
+ });
406
+ credential.addSection("contact", { email: "john@example.com", phoneNumber: "+1234567890" });
407
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
408
+ credential.addSection("residentialAddress", {
409
+ source: "PROOF_OF_ADDRESS",
410
+ street: "Main St",
411
+ city: "New York",
412
+ country: "US",
413
+ proofCategory: "UTILITY_BILL",
414
+ proofDateOfIssue: new Date("2021-01-01"),
415
+ proofFile: Buffer.from("Proof"),
416
+ proofFileType: "image/jpeg",
417
+ proofFileName: "proof.jpg",
418
+ ipCountry: "US",
419
+ });
420
+ credential.addSection("screening", {
421
+ sanctionsCheckResult: "CLEAR",
422
+ sanctionsConfidenceScore: 95,
423
+ pepCheckResult: "NOT_CHECKED",
424
+ pepConfidenceScore: 10,
425
+ });
426
+ credential.addSection("edd", {
427
+ occupation: "REAL_ESTATE",
428
+ sourceOfFundsCategory: "SALARY",
429
+ sourceOfFundsProofFile: Buffer.from("Funds"),
430
+ sourceOfFundsProofFileType: "application/pdf",
431
+ sourceOfFundsProofFileName: "funds.pdf",
432
+ });
433
+ credential.addSection("sourceOfWealth", {
434
+ type: "SALARY",
435
+ yearlyGrossIncome: "LESS_THAN_20000",
436
+ yearlyGrossIncomeCurrency: "EUR",
437
+ approximateNetWorth: "UP_TO_25000",
438
+ approximateNetWorthCurrency: "EUR",
439
+ sourceOfWealthProofFile: Buffer.from("Proof"),
440
+ });
441
+ credential.addSection("onboarding", {
442
+ intendedUse: "INVESTING",
443
+ employmentStatus: "EMPLOYED",
444
+ expectedMonthlyTransactionCount: "LESS_THAN_5",
445
+ expectedMonthlyTransactionVolume: "MORE_THAN_500_LESS_THAN_2000",
446
+ expectedMonthlyTransactionVolumeCurrency: "EUR",
447
+ });
448
+
449
+ const level = "plus+liveness+email+phoneNumber+edd+sow+screening+onboarding";
450
+
451
+ expect(credential.level()).toBe(level);
452
+ expect(credential.kycLevel()).toBe(3);
453
+ expect(() => credential.checkValidity()).not.toThrow();
454
+
455
+ expect(credential.publicNotes()).toEqual({
456
+ type: "kyc",
457
+ level,
458
+ kycLevel: 3,
459
+ });
460
+
461
+ expect(credential.serializeEnvelope()).toEqual({
462
+ id: "https://issuer.example/credentials/123",
463
+ level: level,
464
+ kycLevel: 3,
465
+ issued: new Date("2022-01-01").toISOString(),
466
+ approvedAt: new Date("2022-01-02").toISOString(),
467
+ expirationDate: new Date("2030-01-01").toISOString(),
468
+ });
469
+
470
+ expect(credential.serializeSubject()).toEqual({
471
+ "@context": [
472
+ "https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v3.json",
473
+ ],
474
+ id: "uuid:abc",
475
+ personFirstName: "John",
476
+ personFamilyName: "Doe",
477
+ personGender: "M",
478
+ personNationality: "US",
479
+ personDateOfBirth: new Date("1990-01-01").toISOString(),
480
+ personPlaceOfBirth: "New York, NY",
481
+ idDocumentType: "PASSPORT",
482
+ idDocumentNumber: "123456789",
483
+ idDocumentCountry: "US",
484
+ idDocumentDateOfIssue: new Date("2020-01-01").toISOString(),
485
+ idDocumentDateOfExpiry: new Date("2030-01-01").toISOString(),
486
+ idDocumentIssuingAuthority: "US Department of State",
487
+ idDocumentFrontFile: fileToBase85(Buffer.from("Front")),
488
+ idDocumentFrontFileType: "image/jpeg",
489
+ idDocumentFrontFileName: "front.jpg",
490
+ idDocumentBackFile: fileToBase85(Buffer.from("Back")),
491
+ idDocumentBackFileType: "image/jpeg",
492
+ idDocumentBackFileName: "back.jpg",
493
+ // cspell:disable-next-line
494
+ idDocumentMrzLine1: "P<USADOE<<JOHN<<<<<<<<<<<<<<<<<<<<<<<<",
495
+ contactEmail: "john@example.com",
496
+ contactPhoneNumber: "+1234567890",
497
+ biometricSelfieFile: fileToBase85(Buffer.from("Selfie")),
498
+ biometricSelfieMatch: 99,
499
+ residentialAddressSource: "PROOF_OF_ADDRESS",
500
+ residentialAddressStreet: "Main St",
501
+ residentialAddressCity: "New York",
502
+ residentialAddressCountry: "US",
503
+ residentialAddressProofCategory: "UTILITY_BILL",
504
+ residentialAddressProofDateOfIssue: new Date("2021-01-01").toISOString(),
505
+ residentialAddressProofFile: fileToBase85(Buffer.from("Proof")),
506
+ residentialAddressProofFileType: "image/jpeg",
507
+ residentialAddressProofFileName: "proof.jpg",
508
+ residentialAddressIpCountry: "US",
509
+ screeningSanctionsCheckResult: "CLEAR",
510
+ screeningSanctionsConfidenceScore: 95,
511
+ screeningPepCheckResult: "NOT_CHECKED",
512
+ screeningPepConfidenceScore: 10,
513
+ eddOccupation: "REAL_ESTATE",
514
+ eddSourceOfFundsCategory: "SALARY",
515
+ eddSourceOfFundsProofFile: fileToBase85(Buffer.from("Funds")),
516
+ eddSourceOfFundsProofFileType: "application/pdf",
517
+ eddSourceOfFundsProofFileName: "funds.pdf",
518
+ sourceOfWealthType: "SALARY",
519
+ sourceOfWealthYearlyGrossIncome: "LESS_THAN_20000",
520
+ sourceOfWealthYearlyGrossIncomeCurrency: "EUR",
521
+ sourceOfWealthApproximateNetWorth: "UP_TO_25000",
522
+ sourceOfWealthApproximateNetWorthCurrency: "EUR",
523
+ sourceOfWealthSourceOfWealthProofFile: fileToBase85(Buffer.from("Proof")),
524
+ onboardingIntendedUse: "INVESTING",
525
+ onboardingEmploymentStatus: "EMPLOYED",
526
+ onboardingExpectedMonthlyTransactionCount: "LESS_THAN_5",
527
+ onboardingExpectedMonthlyTransactionVolume: "MORE_THAN_500_LESS_THAN_2000",
528
+ onboardingExpectedMonthlyTransactionVolumeCurrency: "EUR",
529
+ });
530
+ });
531
+
532
+ it("omits absent optional fields rather than emitting undefined", () => {
533
+ const serialized = basicCredential().serializeSubject();
534
+
535
+ expect("personMiddleName" in serialized).toBe(false);
536
+ expect("idDocumentBackFile" in serialized).toBe(false);
537
+ expect("residentialAddressStreet" in serialized).toBe(false);
538
+ });
539
+ });
540
+
541
+ describe("KycV3 deserialization", () => {
542
+ it("deserializes flat envelope and subject fields", async () => {
543
+ const credential = new KycV3();
544
+
545
+ await credential.deserialize(
546
+ serializedCredential({
547
+ "@context": [
548
+ "https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v3.json",
549
+ ],
550
+ id: "uuid:abc",
551
+ personFirstName: "John",
552
+ personFamilyName: "Doe",
553
+ personNationality: "US",
554
+ personDateOfBirth: new Date("1990-01-01").toISOString(),
555
+ idDocumentType: "PASSPORT",
556
+ idDocumentNumber: "123456789",
557
+ idDocumentCountry: "US",
558
+ idDocumentFrontFile: fileToBase85(Buffer.from("Front")),
559
+ eddSourceOfFundsProofFile: fileToBase85(Buffer.from("Funds")),
560
+ onboardingIntendedUse: "INVESTING",
561
+ onboardingEmploymentStatus: "EMPLOYED",
562
+ onboardingExpectedMonthlyTransactionCount: "LESS_THAN_5",
563
+ onboardingExpectedMonthlyTransactionVolume: "MORE_THAN_500_LESS_THAN_2000",
564
+ onboardingExpectedMonthlyTransactionVolumeCurrency: "EUR",
565
+ }),
566
+ );
567
+
568
+ expect(credential.envelope).toEqual({
569
+ id: "https://issuer.example/credentials/123",
570
+ level: "basic+liveness",
571
+ kycLevel: 1,
572
+ issued: new Date("2022-01-01"),
573
+ approvedAt: new Date("2022-01-02"),
574
+ expirationDate: new Date("2030-01-01"),
575
+ });
576
+
577
+ expect(credential.subject.person?.dateOfBirth).toEqual(new Date("1990-01-01"));
578
+ expect(credential.subject.idDocument?.frontFile.toString()).toBe("Front");
579
+ expect(credential.subject.edd?.sourceOfFundsProofFile?.toString()).toBe("Funds");
580
+ expect(credential.subject.onboarding?.employmentStatus).toBe("EMPLOYED");
581
+ expect(credential.subject.onboarding?.expectedMonthlyTransactionCount).toBe("LESS_THAN_5");
582
+
583
+ // Sections absent from the wire stay absent rather than becoming empty objects.
584
+ expect(credential.subject.biometric).toBeUndefined();
585
+ expect(credential.subject.screening).toBeUndefined();
586
+ });
587
+
588
+ it("round-trips an unverified address", async () => {
589
+ const credential = basicCredential();
590
+ credential.addSection("biometric", { selfieFile: Buffer.from("Selfie"), selfieMatch: 99 });
591
+ credential.addSection("residentialAddress", {
592
+ source: "MANUAL",
593
+ street: "Oak Ave",
594
+ houseNumber: "42",
595
+ city: "Boston",
596
+ postalCode: "02108",
597
+ country: "US",
598
+ });
599
+
600
+ expect(credential.level()).toBe("basic+liveness");
601
+ expect(credential.kycLevel()).toBe(1);
602
+
603
+ const serialized = credential.serializeSubject();
604
+ expect(serialized.residentialAddressSource).toBe("MANUAL");
605
+ expect("residentialAddressProofCategory" in serialized).toBe(false);
606
+ expect("residentialAddressProofFile" in serialized).toBe(false);
607
+
608
+ const roundTrip = new KycV3();
609
+ await roundTrip.deserialize(serializedCredential(serialized));
610
+
611
+ expect(roundTrip.subject.residentialAddress).toEqual({
612
+ source: "MANUAL",
613
+ street: "Oak Ave",
614
+ houseNumber: "42",
615
+ city: "Boston",
616
+ postalCode: "02108",
617
+ country: "US",
618
+ });
619
+ });
620
+
621
+ it("rejects a wire key the subject does not declare", async () => {
622
+ await expect(
623
+ new KycV3().deserialize(
624
+ serializedCredential({
625
+ "@context": ["https://example.com/whatever"],
626
+ id: "uuid:abc",
627
+ personFirstName: "John",
628
+ personNationality: "US",
629
+ personDateOfBirth: new Date("1990-01-01").toISOString(),
630
+ idDocumentType: "PASSPORT",
631
+ idDocumentNumber: "123456789",
632
+ idDocumentCountry: "US",
633
+ idDocumentFrontFile: fileToBase85(Buffer.from("Front")),
634
+ unexpectedJsonLdField: "breaks-json-ld",
635
+ }),
636
+ ),
637
+ ).rejects.toThrow("Unexpected subject fields: unexpectedJsonLdField");
638
+ });
639
+
640
+ it("drops the credential's own keys rather than reading them as fields", async () => {
641
+ const credential = new KycV3();
642
+
643
+ await credential.deserialize(
644
+ serializedCredential({
645
+ "@context": ["https://example.com/whatever"],
646
+ id: "uuid:abc",
647
+ personFirstName: "John",
648
+ personNationality: "US",
649
+ personDateOfBirth: new Date("1990-01-01").toISOString(),
650
+ idDocumentType: "PASSPORT",
651
+ idDocumentNumber: "123456789",
652
+ idDocumentCountry: "US",
653
+ idDocumentFrontFile: fileToBase85(Buffer.from("Front")),
654
+ }),
655
+ );
656
+
657
+ // The subject's `@context` is accepted, but is not one of its fields.
658
+ expect(credential.subject).not.toHaveProperty("@context");
659
+ expect(credential.envelope).not.toHaveProperty("proof");
660
+ expect(credential.envelope).not.toHaveProperty("issuanceDate");
661
+ expect(credential.envelope).not.toHaveProperty("@context");
662
+ });
663
+
664
+ it("reports a file that is not valid ascii85 instead of silently dropping it", async () => {
665
+ const credential = new KycV3();
666
+
667
+ await credential.deserialize(
668
+ serializedCredential({
669
+ id: "uuid:abc",
670
+ personFirstName: "John",
671
+ personNationality: "US",
672
+ personDateOfBirth: new Date("1990-01-01").toISOString(),
673
+ idDocumentType: "PASSPORT",
674
+ idDocumentNumber: "123456789",
675
+ idDocumentCountry: "US",
676
+ idDocumentFrontFile: "not-ascii85",
677
+ }),
678
+ );
679
+
680
+ expect(() => credential.checkValidity()).toThrow(/frontFile/);
681
+ });
682
+ });