@idos-network/credentials 1.5.0 → 2.2.2
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/README.md +289 -85
- package/assets/idos-credential-subject-edd-v1.json +10 -0
- package/assets/idos-credential-subject-v1.json +1 -1
- package/assets/idos-credential-subject-v2.json +1 -1
- package/assets/idos-credential-subject-v3.json +83 -0
- package/assets/idos-credentials-v1.json +2 -2
- package/assets/idos-credentials-v2.json +10 -0
- package/dist/index-0NqXMjNk.d.mts +395 -0
- package/dist/index-0NqXMjNk.d.mts.map +1 -0
- package/dist/index-WW372Tf5.d.mts +57 -0
- package/dist/index-WW372Tf5.d.mts.map +1 -0
- package/dist/index.mjs +1 -1
- package/dist/loader-Cg6hs1-P.mjs +2 -0
- package/dist/loader-Cg6hs1-P.mjs.map +1 -0
- package/dist/parser/index.d.mts +19 -0
- package/dist/parser/index.d.mts.map +1 -0
- package/dist/parser/index.mjs +2 -0
- package/dist/parser/index.mjs.map +1 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/schemas-BVGuUlPe.mjs +2 -0
- package/dist/schemas-BVGuUlPe.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -134
- package/dist/types/index.mjs +1 -63
- package/dist/utils/index.d.mts +37 -15
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +1 -116
- package/dist/utils/index.mjs.map +1 -1
- package/dist/verifier/index.d.mts +8 -0
- package/dist/verifier/index.d.mts.map +1 -0
- package/dist/verifier/index.mjs +2 -0
- package/dist/verifier/index.mjs.map +1 -0
- package/package.json +23 -10
- package/src/parser/index.test.ts +266 -0
- package/src/parser/index.ts +69 -0
- package/src/schemas/EddV1/index.test.ts +40 -0
- package/src/schemas/EddV1/index.ts +53 -0
- package/src/schemas/Envelope.ts +37 -0
- package/src/schemas/FaceIdV1/index.test.ts +26 -0
- package/src/schemas/FaceIdV1/index.ts +33 -0
- package/src/schemas/KycV1/IdDocument.ts +41 -0
- package/src/schemas/KycV1/ResidentialAddress.ts +56 -0
- package/src/schemas/KycV1/Subject.ts +86 -0
- package/src/schemas/KycV1/index.test.ts +156 -0
- package/src/schemas/KycV1/index.ts +40 -0
- package/src/schemas/KycV2/Subject.ts +67 -0
- package/src/schemas/KycV2/index.test.ts +162 -0
- package/src/schemas/KycV2/index.ts +45 -0
- package/src/schemas/KycV3/Biometric.ts +14 -0
- package/src/schemas/KycV3/Contact.ts +13 -0
- package/src/schemas/KycV3/EDD.ts +32 -0
- package/src/schemas/KycV3/IdDocument.ts +92 -0
- package/src/schemas/KycV3/Onboarding.ts +36 -0
- package/src/schemas/KycV3/Person.ts +105 -0
- package/src/schemas/KycV3/ResidentialAddress.ts +73 -0
- package/src/schemas/KycV3/Screening.ts +27 -0
- package/src/schemas/KycV3/SourceOfWealth.ts +44 -0
- package/src/schemas/KycV3/Subject.ts +54 -0
- package/src/schemas/KycV3/index.test.ts +682 -0
- package/src/schemas/KycV3/index.ts +108 -0
- package/src/schemas/VerifiableCredential.ts +232 -0
- package/src/schemas/enums.ts +126 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/integration.test.ts +251 -0
- package/src/schemas/types.ts +11 -0
- package/src/schemas/utils.ts +274 -0
- package/src/types/credentials.ts +18 -249
- package/src/utils/{utils.test.ts → index.test.ts} +59 -67
- package/src/utils/index.ts +129 -134
- package/src/utils/issuer.ts +42 -0
- package/src/{builder → utils}/loader.ts +22 -17
- package/src/verifier/index.test.ts +149 -0
- package/src/{builder/verifier.ts → verifier/index.ts} +4 -8
- package/types/internals.d.ts +11 -6
- package/dist/builder/index.d.mts +0 -19
- package/dist/builder/index.d.mts.map +0 -1
- package/dist/builder/index.mjs +0 -619
- package/dist/builder/index.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/src/builder/builder.test.ts +0 -407
- package/src/builder/index.ts +0 -122
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IsString } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import type { PublicNotes } from "../types";
|
|
4
|
+
|
|
5
|
+
import { CONTEXT_IDOS_CREDENTIAL_V1, CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID } from "../../utils/loader";
|
|
6
|
+
import { EnvelopeV1 } from "../Envelope";
|
|
7
|
+
import { VerifiableCredentialBase } from "../VerifiableCredential";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* One field and no sections, so the subject lives here rather than in a file of its own.
|
|
11
|
+
* No `id`: unlike the other versions, this subject has never declared one.
|
|
12
|
+
*/
|
|
13
|
+
export class Subject {
|
|
14
|
+
@IsString()
|
|
15
|
+
faceSignUserId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class FaceIdV1 extends VerifiableCredentialBase<EnvelopeV1, Subject> {
|
|
19
|
+
static readonly envelopeContext: string = CONTEXT_IDOS_CREDENTIAL_V1;
|
|
20
|
+
static readonly subjectContext: string = CONTEXT_IDOS_CREDENTIAL_V1_FACE_ID;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super(EnvelopeV1, Subject);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override level(): string {
|
|
27
|
+
return "human";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override publicNotes(): PublicNotes {
|
|
31
|
+
return { type: "pop", level: this.level() };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IsOptional, IsString, IsISO31661Alpha2 } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import { Base85FileField, IsoDateField } from "../utils";
|
|
4
|
+
|
|
5
|
+
/** The identity document, as v1 and v2 describe it: every field optional and untyped. */
|
|
6
|
+
export class IdDocument {
|
|
7
|
+
/* The country of issuance of the identity document (ISO 3166-1 alpha-2). */
|
|
8
|
+
@IsOptional()
|
|
9
|
+
@IsISO31661Alpha2()
|
|
10
|
+
country?: string;
|
|
11
|
+
|
|
12
|
+
/* The unique number on the identity document. */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
number?: string;
|
|
16
|
+
|
|
17
|
+
/* The type of identity document (e.g. PASSPORT, ID_CARD, DRIVERS). */
|
|
18
|
+
@IsOptional()
|
|
19
|
+
@IsString()
|
|
20
|
+
type?: string;
|
|
21
|
+
|
|
22
|
+
/* The date the identity document was issued. */
|
|
23
|
+
@IsOptional()
|
|
24
|
+
@IsoDateField()
|
|
25
|
+
dateOfIssue?: Date;
|
|
26
|
+
|
|
27
|
+
/* The date the identity document expires. */
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsoDateField()
|
|
30
|
+
dateOfExpiry?: Date;
|
|
31
|
+
|
|
32
|
+
/* The file containing the front of the identity document. */
|
|
33
|
+
@IsOptional()
|
|
34
|
+
@Base85FileField()
|
|
35
|
+
frontFile?: Buffer;
|
|
36
|
+
|
|
37
|
+
/* The file containing the back of the identity document. */
|
|
38
|
+
@IsOptional()
|
|
39
|
+
@Base85FileField()
|
|
40
|
+
backFile?: Buffer;
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { IsOptional, IsString, IsISO31661Alpha2 } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import { Base85FileField, IsoDateField } from "../utils";
|
|
4
|
+
|
|
5
|
+
/** The residential address, as v1 and v2 describe it: every field optional, no `verified` flag. */
|
|
6
|
+
export class ResidentialAddress {
|
|
7
|
+
/* Street address. */
|
|
8
|
+
@IsOptional()
|
|
9
|
+
@IsString()
|
|
10
|
+
street?: string;
|
|
11
|
+
|
|
12
|
+
/* House number. */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
houseNumber?: string;
|
|
16
|
+
|
|
17
|
+
/* Additional address information (e.g. apartment number). */
|
|
18
|
+
@IsOptional()
|
|
19
|
+
@IsString()
|
|
20
|
+
additionalAddressInfo?: string;
|
|
21
|
+
|
|
22
|
+
/* Region (e.g. state, province). */
|
|
23
|
+
@IsOptional()
|
|
24
|
+
@IsString()
|
|
25
|
+
region?: string;
|
|
26
|
+
|
|
27
|
+
/* Locality (e.g. city, town). */
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsString()
|
|
30
|
+
city?: string;
|
|
31
|
+
|
|
32
|
+
/* Postal code. */
|
|
33
|
+
@IsOptional()
|
|
34
|
+
@IsString()
|
|
35
|
+
postalCode?: string;
|
|
36
|
+
|
|
37
|
+
/* Country (ISO 3166-1 alpha-2). */
|
|
38
|
+
@IsOptional()
|
|
39
|
+
@IsISO31661Alpha2()
|
|
40
|
+
country?: string;
|
|
41
|
+
|
|
42
|
+
/* Type of document provided to verify the address (e.g. utility bill, bank statement). */
|
|
43
|
+
@IsOptional()
|
|
44
|
+
@IsString()
|
|
45
|
+
proofCategory?: string;
|
|
46
|
+
|
|
47
|
+
/* Date the address proof document was issued. */
|
|
48
|
+
@IsOptional()
|
|
49
|
+
@IsoDateField()
|
|
50
|
+
proofDateOfIssue?: Date;
|
|
51
|
+
|
|
52
|
+
/* The document provided as address proof. */
|
|
53
|
+
@IsOptional()
|
|
54
|
+
@Base85FileField()
|
|
55
|
+
proofFile?: Buffer;
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { IsEmail, IsOptional, IsString, IsISO31661Alpha2 } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import { Base85FileField, IsoDateField, Section } from "../utils";
|
|
4
|
+
import { IdDocument } from "./IdDocument";
|
|
5
|
+
import { ResidentialAddress } from "./ResidentialAddress";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The credential subject. The fields v1 groups under `root` are declared here directly: they
|
|
9
|
+
* travel unprefixed, which is exactly how a class's own fields are flattened.
|
|
10
|
+
*/
|
|
11
|
+
export class Subject {
|
|
12
|
+
@IsString()
|
|
13
|
+
id: string;
|
|
14
|
+
|
|
15
|
+
@IsOptional()
|
|
16
|
+
@IsString()
|
|
17
|
+
applicantId?: string;
|
|
18
|
+
|
|
19
|
+
@IsOptional()
|
|
20
|
+
@IsString()
|
|
21
|
+
inquiryId?: string;
|
|
22
|
+
|
|
23
|
+
@IsOptional()
|
|
24
|
+
@IsString()
|
|
25
|
+
firstName?: string;
|
|
26
|
+
|
|
27
|
+
@IsOptional()
|
|
28
|
+
@IsString()
|
|
29
|
+
middleName?: string;
|
|
30
|
+
|
|
31
|
+
@IsOptional()
|
|
32
|
+
@IsISO31661Alpha2()
|
|
33
|
+
nationality?: string;
|
|
34
|
+
|
|
35
|
+
@IsOptional()
|
|
36
|
+
@IsString()
|
|
37
|
+
familyName?: string;
|
|
38
|
+
|
|
39
|
+
@IsOptional()
|
|
40
|
+
@IsString()
|
|
41
|
+
maidenName?: string;
|
|
42
|
+
|
|
43
|
+
@IsOptional()
|
|
44
|
+
@IsString()
|
|
45
|
+
gender?: string;
|
|
46
|
+
|
|
47
|
+
@IsOptional()
|
|
48
|
+
@IsString()
|
|
49
|
+
governmentId?: string;
|
|
50
|
+
|
|
51
|
+
@IsOptional()
|
|
52
|
+
@IsString()
|
|
53
|
+
governmentIdType?: string;
|
|
54
|
+
|
|
55
|
+
@IsOptional()
|
|
56
|
+
@IsEmail()
|
|
57
|
+
email?: string;
|
|
58
|
+
|
|
59
|
+
@IsOptional()
|
|
60
|
+
@IsString()
|
|
61
|
+
ssn?: string;
|
|
62
|
+
|
|
63
|
+
@IsOptional()
|
|
64
|
+
@IsString()
|
|
65
|
+
phoneNumber?: string;
|
|
66
|
+
|
|
67
|
+
@IsOptional()
|
|
68
|
+
@IsoDateField()
|
|
69
|
+
dateOfBirth?: Date;
|
|
70
|
+
|
|
71
|
+
@IsOptional()
|
|
72
|
+
@IsString()
|
|
73
|
+
placeOfBirth?: string;
|
|
74
|
+
|
|
75
|
+
@IsOptional()
|
|
76
|
+
@Base85FileField()
|
|
77
|
+
selfieFile?: Buffer;
|
|
78
|
+
|
|
79
|
+
@Section(() => IdDocument)
|
|
80
|
+
@IsOptional()
|
|
81
|
+
idDocument?: IdDocument;
|
|
82
|
+
|
|
83
|
+
@Section(() => ResidentialAddress)
|
|
84
|
+
@IsOptional()
|
|
85
|
+
residentialAddress?: ResidentialAddress;
|
|
86
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { base85ToFile, fileToBase85 } from "@idos-network/utils/codecs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { VerifiableCredential } from "../../types";
|
|
5
|
+
|
|
6
|
+
import { KycV1 } from "./index";
|
|
7
|
+
|
|
8
|
+
function serializedCredential(
|
|
9
|
+
credentialSubject: Record<string, unknown>,
|
|
10
|
+
): VerifiableCredential<unknown> {
|
|
11
|
+
return {
|
|
12
|
+
"@context": [],
|
|
13
|
+
type: ["VerifiableCredential"],
|
|
14
|
+
issuer: "did:example:issuer",
|
|
15
|
+
id: "https://issuer.example/credentials/123",
|
|
16
|
+
level: "basic",
|
|
17
|
+
approvedAt: new Date("2022-01-01").toISOString(),
|
|
18
|
+
issuanceDate: new Date("2022-01-01").toISOString(),
|
|
19
|
+
credentialSubject,
|
|
20
|
+
proof: { type: "Ed25519Signature2020", proofValue: "proof" },
|
|
21
|
+
} as unknown as VerifiableCredential<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("KycV1", () => {
|
|
25
|
+
it("is valid with only the ids set", () => {
|
|
26
|
+
const credential = new KycV1()
|
|
27
|
+
.setEnvelope({ id: "https://issuer.example/credentials/123" })
|
|
28
|
+
.setSubject({ id: "uuid:abc" });
|
|
29
|
+
|
|
30
|
+
expect(() => credential.checkValidity()).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("derives level from the subject, reaching plus on a complete address proof", () => {
|
|
34
|
+
const credential = new KycV1().setSubject({
|
|
35
|
+
id: "uuid:abc",
|
|
36
|
+
email: "ada@example.com",
|
|
37
|
+
selfieFile: Buffer.from("Selfie"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(credential.level()).toBe("basic+liveness+email");
|
|
41
|
+
|
|
42
|
+
credential.addSection("residentialAddress", {
|
|
43
|
+
city: "London",
|
|
44
|
+
proofFile: Buffer.from("Proof"),
|
|
45
|
+
});
|
|
46
|
+
expect(credential.level()).toBe("basic+liveness+email");
|
|
47
|
+
|
|
48
|
+
credential.addSection("residentialAddress", {
|
|
49
|
+
city: "London",
|
|
50
|
+
country: "GB",
|
|
51
|
+
proofCategory: "UTILITY_BILL",
|
|
52
|
+
proofFile: Buffer.from("Proof"),
|
|
53
|
+
});
|
|
54
|
+
expect(credential.level()).toBe("plus+liveness+email");
|
|
55
|
+
expect(credential.publicNotes()).toEqual({ type: "kyc", level: "plus+liveness+email" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("flattens the subject, keeping its own fields unprefixed", () => {
|
|
59
|
+
const credential = new KycV1()
|
|
60
|
+
.setEnvelope({
|
|
61
|
+
id: "https://issuer.example/credentials/123",
|
|
62
|
+
approvedAt: new Date("2022-01-01"),
|
|
63
|
+
})
|
|
64
|
+
.setSubject({
|
|
65
|
+
id: "uuid:abc",
|
|
66
|
+
firstName: "Ada",
|
|
67
|
+
familyName: "Lovelace",
|
|
68
|
+
nationality: "GB",
|
|
69
|
+
dateOfBirth: new Date("1815-12-10"),
|
|
70
|
+
selfieFile: Buffer.from("Selfie"),
|
|
71
|
+
})
|
|
72
|
+
.addSection("idDocument", {
|
|
73
|
+
country: "GB",
|
|
74
|
+
number: "123456789",
|
|
75
|
+
type: "PASSPORT",
|
|
76
|
+
dateOfIssue: new Date("2020-01-01"),
|
|
77
|
+
frontFile: Buffer.from("Front"),
|
|
78
|
+
})
|
|
79
|
+
.addSection("residentialAddress", {
|
|
80
|
+
street: "Main St",
|
|
81
|
+
city: "London",
|
|
82
|
+
country: "GB",
|
|
83
|
+
proofCategory: "UTILITY_BILL",
|
|
84
|
+
proofFile: Buffer.from("Proof"),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const serialized = credential.serializeSubject();
|
|
88
|
+
|
|
89
|
+
expect(serialized.id).toBe("uuid:abc");
|
|
90
|
+
expect(serialized.firstName).toBe("Ada");
|
|
91
|
+
expect(serialized.dateOfBirth).toBe(new Date("1815-12-10").toISOString());
|
|
92
|
+
expect(base85ToFile(serialized.selfieFile as string)?.toString()).toBe("Selfie");
|
|
93
|
+
expect(serialized.idDocumentCountry).toBe("GB");
|
|
94
|
+
expect(serialized.idDocumentDateOfIssue).toBe(new Date("2020-01-01").toISOString());
|
|
95
|
+
expect(base85ToFile(serialized.idDocumentFrontFile as string)?.toString()).toBe("Front");
|
|
96
|
+
expect(serialized.residentialAddressCity).toBe("London");
|
|
97
|
+
expect(base85ToFile(serialized.residentialAddressProofFile as string)?.toString()).toBe(
|
|
98
|
+
"Proof",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect("middleName" in serialized).toBe(false);
|
|
102
|
+
expect("idDocumentBackFile" in serialized).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("round-trips through deserialize", async () => {
|
|
106
|
+
const roundTrip = new KycV1();
|
|
107
|
+
|
|
108
|
+
await roundTrip.deserialize(
|
|
109
|
+
serializedCredential({
|
|
110
|
+
id: "uuid:abc",
|
|
111
|
+
firstName: "Ada",
|
|
112
|
+
dateOfBirth: new Date("1815-12-10").toISOString(),
|
|
113
|
+
selfieFile: fileToBase85(Buffer.from("Selfie")),
|
|
114
|
+
idDocumentType: "PASSPORT",
|
|
115
|
+
residentialAddressCity: "London",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(roundTrip.subject.id).toBe("uuid:abc");
|
|
120
|
+
expect(roundTrip.subject.dateOfBirth).toEqual(new Date("1815-12-10"));
|
|
121
|
+
expect(roundTrip.subject.selfieFile?.toString()).toBe("Selfie");
|
|
122
|
+
expect(roundTrip.subject.idDocument?.type).toBe("PASSPORT");
|
|
123
|
+
expect(roundTrip.subject.residentialAddress?.city).toBe("London");
|
|
124
|
+
expect(roundTrip.envelope.approvedAt).toEqual(new Date("2022-01-01"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects fields the subject does not declare", async () => {
|
|
128
|
+
expect(() => new KycV1().setSubject({ id: "x", bogus: 1 } as never)).toThrow(
|
|
129
|
+
"Unexpected subject fields: bogus",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(() => new KycV1().addSection("idDocument", { bogus: 1 } as never)).toThrow(
|
|
133
|
+
"Unexpected idDocument fields: bogus",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
new KycV1().deserialize(serializedCredential({ id: "uuid:abc", bogus: 1 })),
|
|
138
|
+
).rejects.toThrow("Unexpected subject fields: bogus");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("keeps the applicant and government-id fields v2 dropped", () => {
|
|
142
|
+
const serialized = new KycV1()
|
|
143
|
+
.setEnvelope({ id: "https://issuer.example/credentials/123" })
|
|
144
|
+
.setSubject({
|
|
145
|
+
id: "uuid:abc",
|
|
146
|
+
applicantId: "applicant-1",
|
|
147
|
+
inquiryId: "inquiry-1",
|
|
148
|
+
governmentId: "gov-1",
|
|
149
|
+
governmentIdType: "NATIONAL_ID",
|
|
150
|
+
})
|
|
151
|
+
.serializeSubject();
|
|
152
|
+
|
|
153
|
+
expect(serialized.applicantId).toBe("applicant-1");
|
|
154
|
+
expect(serialized.governmentIdType).toBe("NATIONAL_ID");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PublicNotes } from "../types";
|
|
2
|
+
|
|
3
|
+
import { CONTEXT_IDOS_CREDENTIAL_V1, CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT } from "../../utils/loader";
|
|
4
|
+
import { EnvelopeV1 } from "../Envelope";
|
|
5
|
+
import { VerifiableCredentialBase } from "../VerifiableCredential";
|
|
6
|
+
import { Subject } from "./Subject";
|
|
7
|
+
|
|
8
|
+
export class KycV1 extends VerifiableCredentialBase<EnvelopeV1, Subject> {
|
|
9
|
+
static readonly envelopeContext: string = CONTEXT_IDOS_CREDENTIAL_V1;
|
|
10
|
+
static readonly subjectContext: string = CONTEXT_IDOS_CREDENTIAL_V1_SUBJECT;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super(EnvelopeV1, Subject);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override level(): string {
|
|
17
|
+
const { residentialAddress, selfieFile, email, phoneNumber } = this.subject;
|
|
18
|
+
|
|
19
|
+
// A complete address proof is the sign of plus.
|
|
20
|
+
const base =
|
|
21
|
+
residentialAddress?.proofFile &&
|
|
22
|
+
residentialAddress.city &&
|
|
23
|
+
residentialAddress.proofCategory &&
|
|
24
|
+
residentialAddress.country
|
|
25
|
+
? "plus"
|
|
26
|
+
: "basic";
|
|
27
|
+
|
|
28
|
+
const addons = [
|
|
29
|
+
selfieFile && "liveness",
|
|
30
|
+
email && "email",
|
|
31
|
+
phoneNumber && "phoneNumber",
|
|
32
|
+
].filter(Boolean);
|
|
33
|
+
|
|
34
|
+
return [base, ...addons].join("+");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override publicNotes(): PublicNotes {
|
|
38
|
+
return { type: "kyc", level: this.level() };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { IsEmail, IsOptional, IsString, IsISO31661Alpha2 } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import { IdDocument } from "../KycV1/IdDocument";
|
|
4
|
+
import { ResidentialAddress } from "../KycV1/ResidentialAddress";
|
|
5
|
+
import { Base85FileField, IsoDateField, Section } from "../utils";
|
|
6
|
+
|
|
7
|
+
/** v1's subject, minus the applicant/inquiry and government-id fields v2 dropped. */
|
|
8
|
+
export class Subject {
|
|
9
|
+
@IsString()
|
|
10
|
+
id: string;
|
|
11
|
+
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsString()
|
|
14
|
+
firstName?: string;
|
|
15
|
+
|
|
16
|
+
@IsOptional()
|
|
17
|
+
@IsString()
|
|
18
|
+
middleName?: string;
|
|
19
|
+
|
|
20
|
+
@IsOptional()
|
|
21
|
+
@IsISO31661Alpha2()
|
|
22
|
+
nationality?: string;
|
|
23
|
+
|
|
24
|
+
@IsOptional()
|
|
25
|
+
@IsString()
|
|
26
|
+
familyName?: string;
|
|
27
|
+
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsString()
|
|
30
|
+
maidenName?: string;
|
|
31
|
+
|
|
32
|
+
@IsOptional()
|
|
33
|
+
@IsString()
|
|
34
|
+
gender?: string;
|
|
35
|
+
|
|
36
|
+
@IsOptional()
|
|
37
|
+
@IsEmail()
|
|
38
|
+
email?: string;
|
|
39
|
+
|
|
40
|
+
@IsOptional()
|
|
41
|
+
@IsString()
|
|
42
|
+
phoneNumber?: string;
|
|
43
|
+
|
|
44
|
+
@IsOptional()
|
|
45
|
+
@IsString()
|
|
46
|
+
ssn?: string;
|
|
47
|
+
|
|
48
|
+
@IsOptional()
|
|
49
|
+
@IsoDateField()
|
|
50
|
+
dateOfBirth?: Date;
|
|
51
|
+
|
|
52
|
+
@IsOptional()
|
|
53
|
+
@IsString()
|
|
54
|
+
placeOfBirth?: string;
|
|
55
|
+
|
|
56
|
+
@IsOptional()
|
|
57
|
+
@Base85FileField()
|
|
58
|
+
selfieFile?: Buffer;
|
|
59
|
+
|
|
60
|
+
@Section(() => IdDocument)
|
|
61
|
+
@IsOptional()
|
|
62
|
+
idDocument?: IdDocument;
|
|
63
|
+
|
|
64
|
+
@Section(() => ResidentialAddress)
|
|
65
|
+
@IsOptional()
|
|
66
|
+
residentialAddress?: ResidentialAddress;
|
|
67
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { base85ToFile, fileToBase85 } from "@idos-network/utils/codecs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { VerifiableCredential } from "../../types";
|
|
5
|
+
|
|
6
|
+
import { KycV2 } from "./index";
|
|
7
|
+
|
|
8
|
+
function serializedCredential(
|
|
9
|
+
credentialSubject: Record<string, unknown>,
|
|
10
|
+
): VerifiableCredential<unknown> {
|
|
11
|
+
return {
|
|
12
|
+
"@context": [],
|
|
13
|
+
type: ["VerifiableCredential"],
|
|
14
|
+
issuer: "did:example:issuer",
|
|
15
|
+
id: "https://issuer.example/credentials/123",
|
|
16
|
+
level: "basic",
|
|
17
|
+
approvedAt: new Date("2022-01-01").toISOString(),
|
|
18
|
+
issuanceDate: new Date("2022-01-01").toISOString(),
|
|
19
|
+
credentialSubject,
|
|
20
|
+
proof: { type: "Ed25519Signature2020", proofValue: "proof" },
|
|
21
|
+
} as unknown as VerifiableCredential<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("KycV2", () => {
|
|
25
|
+
it("is valid with only the ids set", () => {
|
|
26
|
+
const credential = new KycV2()
|
|
27
|
+
.setEnvelope({ id: "https://issuer.example/credentials/123" })
|
|
28
|
+
.setSubject({ id: "uuid:abc" });
|
|
29
|
+
|
|
30
|
+
expect(() => credential.checkValidity()).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("derives level from the subject, reaching plus on a complete address proof", () => {
|
|
34
|
+
const credential = new KycV2().setSubject({
|
|
35
|
+
id: "uuid:abc",
|
|
36
|
+
email: "ada@example.com",
|
|
37
|
+
selfieFile: Buffer.from("Selfie"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(credential.level()).toBe("basic+liveness+email");
|
|
41
|
+
|
|
42
|
+
credential.addSection("residentialAddress", {
|
|
43
|
+
city: "London",
|
|
44
|
+
proofFile: Buffer.from("Proof"),
|
|
45
|
+
});
|
|
46
|
+
expect(credential.level()).toBe("basic+liveness+email");
|
|
47
|
+
|
|
48
|
+
credential.addSection("residentialAddress", {
|
|
49
|
+
city: "London",
|
|
50
|
+
country: "GB",
|
|
51
|
+
proofCategory: "UTILITY_BILL",
|
|
52
|
+
proofFile: Buffer.from("Proof"),
|
|
53
|
+
});
|
|
54
|
+
expect(credential.level()).toBe("plus+liveness+email");
|
|
55
|
+
expect(credential.publicNotes()).toEqual({ type: "kyc", level: "plus+liveness+email" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("flattens the subject, keeping its own fields unprefixed", () => {
|
|
59
|
+
const credential = new KycV2()
|
|
60
|
+
.setEnvelope({
|
|
61
|
+
id: "https://issuer.example/credentials/123",
|
|
62
|
+
approvedAt: new Date("2022-01-01"),
|
|
63
|
+
})
|
|
64
|
+
.setSubject({
|
|
65
|
+
id: "uuid:abc",
|
|
66
|
+
firstName: "Ada",
|
|
67
|
+
familyName: "Lovelace",
|
|
68
|
+
nationality: "GB",
|
|
69
|
+
dateOfBirth: new Date("1815-12-10"),
|
|
70
|
+
selfieFile: Buffer.from("Selfie"),
|
|
71
|
+
})
|
|
72
|
+
.addSection("idDocument", {
|
|
73
|
+
country: "GB",
|
|
74
|
+
number: "123456789",
|
|
75
|
+
type: "PASSPORT",
|
|
76
|
+
dateOfIssue: new Date("2020-01-01"),
|
|
77
|
+
frontFile: Buffer.from("Front"),
|
|
78
|
+
})
|
|
79
|
+
.addSection("residentialAddress", {
|
|
80
|
+
street: "Main St",
|
|
81
|
+
city: "London",
|
|
82
|
+
country: "GB",
|
|
83
|
+
proofCategory: "UTILITY_BILL",
|
|
84
|
+
proofFile: Buffer.from("Proof"),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const serialized = credential.serializeSubject();
|
|
88
|
+
|
|
89
|
+
expect(serialized.id).toBe("uuid:abc");
|
|
90
|
+
expect(serialized.firstName).toBe("Ada");
|
|
91
|
+
expect(serialized.dateOfBirth).toBe(new Date("1815-12-10").toISOString());
|
|
92
|
+
expect(base85ToFile(serialized.selfieFile as string)?.toString()).toBe("Selfie");
|
|
93
|
+
expect(serialized.idDocumentCountry).toBe("GB");
|
|
94
|
+
expect(serialized.idDocumentDateOfIssue).toBe(new Date("2020-01-01").toISOString());
|
|
95
|
+
expect(base85ToFile(serialized.idDocumentFrontFile as string)?.toString()).toBe("Front");
|
|
96
|
+
expect(serialized.residentialAddressCity).toBe("London");
|
|
97
|
+
expect(base85ToFile(serialized.residentialAddressProofFile as string)?.toString()).toBe(
|
|
98
|
+
"Proof",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect("middleName" in serialized).toBe(false);
|
|
102
|
+
expect("idDocumentBackFile" in serialized).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("round-trips through deserialize", async () => {
|
|
106
|
+
const roundTrip = new KycV2();
|
|
107
|
+
|
|
108
|
+
await roundTrip.deserialize(
|
|
109
|
+
serializedCredential({
|
|
110
|
+
id: "uuid:abc",
|
|
111
|
+
firstName: "Ada",
|
|
112
|
+
dateOfBirth: new Date("1815-12-10").toISOString(),
|
|
113
|
+
selfieFile: fileToBase85(Buffer.from("Selfie")),
|
|
114
|
+
idDocumentType: "PASSPORT",
|
|
115
|
+
residentialAddressCity: "London",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(roundTrip.subject.id).toBe("uuid:abc");
|
|
120
|
+
expect(roundTrip.subject.dateOfBirth).toEqual(new Date("1815-12-10"));
|
|
121
|
+
expect(roundTrip.subject.selfieFile?.toString()).toBe("Selfie");
|
|
122
|
+
expect(roundTrip.subject.idDocument?.type).toBe("PASSPORT");
|
|
123
|
+
expect(roundTrip.subject.residentialAddress?.city).toBe("London");
|
|
124
|
+
expect(roundTrip.envelope.approvedAt).toEqual(new Date("2022-01-01"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects fields the subject does not declare", async () => {
|
|
128
|
+
expect(() => new KycV2().setSubject({ id: "x", bogus: 1 } as never)).toThrow(
|
|
129
|
+
"Unexpected subject fields: bogus",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(() => new KycV2().addSection("idDocument", { bogus: 1 } as never)).toThrow(
|
|
133
|
+
"Unexpected idDocument fields: bogus",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
new KycV2().deserialize(serializedCredential({ id: "uuid:abc", bogus: 1 })),
|
|
138
|
+
).rejects.toThrow("Unexpected subject fields: bogus");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("carries the v2 envelope fields v1 lacks", () => {
|
|
142
|
+
const envelope = new KycV2()
|
|
143
|
+
.setEnvelope({
|
|
144
|
+
id: "https://issuer.example/credentials/123",
|
|
145
|
+
kycLevel: 2,
|
|
146
|
+
issued: new Date("2022-01-01"),
|
|
147
|
+
expirationDate: new Date("2030-01-01"),
|
|
148
|
+
})
|
|
149
|
+
.setSubject({ id: "uuid:abc" })
|
|
150
|
+
.serializeEnvelope();
|
|
151
|
+
|
|
152
|
+
expect(envelope.kycLevel).toBe(2);
|
|
153
|
+
expect(envelope.issued).toBe(new Date("2022-01-01").toISOString());
|
|
154
|
+
expect(envelope.expirationDate).toBe(new Date("2030-01-01").toISOString());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("rejects the fields only v1 declares", () => {
|
|
158
|
+
expect(() => new KycV2().setSubject({ id: "x", applicantId: "a" } as never)).toThrow(
|
|
159
|
+
"Unexpected subject fields: applicantId",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PublicNotes } from "../types";
|
|
2
|
+
|
|
3
|
+
import { CONTEXT_IDOS_CREDENTIAL_V2, CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT } from "../../utils/loader";
|
|
4
|
+
import { EnvelopeV2 } from "../Envelope";
|
|
5
|
+
import { VerifiableCredentialBase } from "../VerifiableCredential";
|
|
6
|
+
import { Subject } from "./Subject";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* v2 derives its level the same way as v1; what changed is the subject context and the
|
|
10
|
+
* envelope, which gained `kycLevel` and the issuance dates — declared in v2's own,
|
|
11
|
+
* separately protected envelope context.
|
|
12
|
+
*/
|
|
13
|
+
export class KycV2 extends VerifiableCredentialBase<EnvelopeV2, Subject> {
|
|
14
|
+
static readonly envelopeContext: string = CONTEXT_IDOS_CREDENTIAL_V2;
|
|
15
|
+
static readonly subjectContext: string = CONTEXT_IDOS_CREDENTIAL_V2_SUBJECT;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super(EnvelopeV2, Subject);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override level(): string {
|
|
22
|
+
const { residentialAddress, selfieFile, email, phoneNumber } = this.subject;
|
|
23
|
+
|
|
24
|
+
// A complete address proof is the sign of plus.
|
|
25
|
+
const base =
|
|
26
|
+
residentialAddress?.proofFile &&
|
|
27
|
+
residentialAddress.city &&
|
|
28
|
+
residentialAddress.proofCategory &&
|
|
29
|
+
residentialAddress.country
|
|
30
|
+
? "plus"
|
|
31
|
+
: "basic";
|
|
32
|
+
|
|
33
|
+
const addons = [
|
|
34
|
+
selfieFile && "liveness",
|
|
35
|
+
email && "email",
|
|
36
|
+
phoneNumber && "phoneNumber",
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
|
|
39
|
+
return [base, ...addons].join("+");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override publicNotes(): PublicNotes {
|
|
43
|
+
return { type: "kyc", level: this.level() };
|
|
44
|
+
}
|
|
45
|
+
}
|