@idos-network/credentials 1.1.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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/assets/country-codes.xml +271 -0
- package/assets/ed25519-signature-2020-v1.json +93 -0
- package/assets/idos-credential-subject-face-id-v1.json +8 -0
- package/assets/idos-credential-subject-v1.json +41 -0
- package/assets/idos-credential-subject-v2.json +37 -0
- package/assets/idos-credentials-v1.json +9 -0
- package/assets/v1.json +273 -0
- package/dist/builder/index.d.mts +20 -0
- package/dist/builder/index.d.mts.map +1 -0
- package/dist/builder/index.mjs +619 -0
- package/dist/builder/index.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/types/index.d.mts +135 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +63 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +33 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +124 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/builder/builder.test.ts +407 -0
- package/src/builder/index.ts +122 -0
- package/src/builder/loader.ts +76 -0
- package/src/builder/verifier.ts +55 -0
- package/src/index.ts +2 -0
- package/src/types/credentials.ts +282 -0
- package/src/types/index.ts +4 -0
- package/src/types/issuers.ts +21 -0
- package/src/utils/index.ts +277 -0
- package/src/utils/utils.test.ts +303 -0
- package/types/index.d.ts +4 -0
- package/types/internals.d.ts +124 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
export type idOSCredential = {
|
|
4
|
+
id: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
public_notes: string;
|
|
7
|
+
content: string;
|
|
8
|
+
encryptor_public_key: string;
|
|
9
|
+
issuer_auth_public_key: string;
|
|
10
|
+
original_id?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type InsertableIDOSCredential = Omit<idOSCredential, "id" | "original_id"> & {
|
|
14
|
+
id?: idOSCredential["id"];
|
|
15
|
+
content_hash?: string;
|
|
16
|
+
public_notes_signature: string;
|
|
17
|
+
broader_signature: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// https://github.com/colinhacks/zod/issues/3751
|
|
21
|
+
export const CredentialFieldsSchema: z.ZodObject<{
|
|
22
|
+
id: z.ZodString;
|
|
23
|
+
level: z.ZodString;
|
|
24
|
+
issued: z.ZodOptional<z.ZodDate>;
|
|
25
|
+
approvedAt: z.ZodOptional<z.ZodDate>;
|
|
26
|
+
expirationDate: z.ZodOptional<z.ZodDate>;
|
|
27
|
+
}> = z.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
|
|
30
|
+
/* Level of KYC verification performed (e.g., basic, intermediate, advanced). */
|
|
31
|
+
level: z.string(),
|
|
32
|
+
|
|
33
|
+
/* @default Date.now() */
|
|
34
|
+
issued: z.date().optional(),
|
|
35
|
+
|
|
36
|
+
/* Date the credential was approved. */
|
|
37
|
+
approvedAt: z.date().optional(),
|
|
38
|
+
|
|
39
|
+
/* Date the credential was revoked. */
|
|
40
|
+
expirationDate: z.date().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type CredentialFields = z.infer<typeof CredentialFieldsSchema>;
|
|
44
|
+
|
|
45
|
+
// https://github.com/colinhacks/zod/issues/3751
|
|
46
|
+
export const CredentialResidentialAddressSchema: z.ZodObject<{
|
|
47
|
+
street: z.ZodString;
|
|
48
|
+
houseNumber: z.ZodOptional<z.ZodString>;
|
|
49
|
+
additionalAddressInfo: z.ZodOptional<z.ZodString>;
|
|
50
|
+
region: z.ZodOptional<z.ZodString>;
|
|
51
|
+
city: z.ZodString;
|
|
52
|
+
postalCode: z.ZodOptional<z.ZodString>;
|
|
53
|
+
country: z.ZodString;
|
|
54
|
+
proofCategory: z.ZodString;
|
|
55
|
+
proofDateOfIssue: z.ZodOptional<z.ZodDate>;
|
|
56
|
+
proofFile: z.ZodType<Buffer<ArrayBufferLike>>;
|
|
57
|
+
}> = z.object({
|
|
58
|
+
/* Street address. */
|
|
59
|
+
street: z.string(),
|
|
60
|
+
|
|
61
|
+
/* House number. */
|
|
62
|
+
houseNumber: z.string().optional(),
|
|
63
|
+
|
|
64
|
+
/* Additional address information (e.g., apartment number). */
|
|
65
|
+
additionalAddressInfo: z.string().optional(),
|
|
66
|
+
|
|
67
|
+
/* Region (e.g., state, province). */
|
|
68
|
+
region: z.string().optional(),
|
|
69
|
+
|
|
70
|
+
/* Locality (e.g., city, town). */
|
|
71
|
+
city: z.string(),
|
|
72
|
+
|
|
73
|
+
/* Postal code. */
|
|
74
|
+
postalCode: z.string().optional(),
|
|
75
|
+
|
|
76
|
+
/* Country (ISO 3166-1 alpha-2). */
|
|
77
|
+
country: z.string().min(2).max(2),
|
|
78
|
+
|
|
79
|
+
/* Residential Address Proof Category Type of document provided to verify the address(e.g., utility bill, bank statement). */
|
|
80
|
+
proofCategory: z.string(),
|
|
81
|
+
|
|
82
|
+
/* Residential Address Proof Date Of Issue Date the address proof document was issued. */
|
|
83
|
+
proofDateOfIssue: z.date().optional(),
|
|
84
|
+
|
|
85
|
+
/* Residential Address Proof File or URL of the document provided as address proof. */
|
|
86
|
+
proofFile: z.instanceof(Buffer),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type CredentialResidentialAddress = z.infer<typeof CredentialResidentialAddressSchema>;
|
|
90
|
+
|
|
91
|
+
export const IDDocumentTypeSchema: z.ZodEnum<{
|
|
92
|
+
PASSPORT: "PASSPORT";
|
|
93
|
+
DRIVERS: "DRIVERS";
|
|
94
|
+
ID_CARD: "ID_CARD";
|
|
95
|
+
VOTING_CARD: "VOTING_CARD";
|
|
96
|
+
PAN_CARD: "PAN_CARD";
|
|
97
|
+
INTERNAL_PASSPORT: "INTERNAL_PASSPORT";
|
|
98
|
+
RESIDENCE_PERMIT: "RESIDENCE_PERMIT";
|
|
99
|
+
}> = z.enum([
|
|
100
|
+
"PASSPORT",
|
|
101
|
+
"DRIVERS",
|
|
102
|
+
"ID_CARD",
|
|
103
|
+
"VOTING_CARD",
|
|
104
|
+
"PAN_CARD",
|
|
105
|
+
"INTERNAL_PASSPORT",
|
|
106
|
+
"RESIDENCE_PERMIT",
|
|
107
|
+
]);
|
|
108
|
+
export type IDDocumentType = z.infer<typeof IDDocumentTypeSchema>;
|
|
109
|
+
|
|
110
|
+
export const GenderSchema: z.ZodEnum<{
|
|
111
|
+
M: "M";
|
|
112
|
+
F: "F";
|
|
113
|
+
OTHER: "OTHER";
|
|
114
|
+
}> = z.enum(["M", "F", "OTHER"]);
|
|
115
|
+
export type Gender = z.infer<typeof GenderSchema>;
|
|
116
|
+
|
|
117
|
+
// https://github.com/colinhacks/zod/issues/3751
|
|
118
|
+
export const CredentialSubjectSchema: z.ZodObject<{
|
|
119
|
+
id: z.ZodString;
|
|
120
|
+
// Indonesian and Indian did not always have first name
|
|
121
|
+
firstName: z.ZodOptional<z.ZodString>;
|
|
122
|
+
middleName: z.ZodOptional<z.ZodString>;
|
|
123
|
+
ssn: z.ZodOptional<z.ZodString>;
|
|
124
|
+
gender: z.ZodOptional<typeof GenderSchema>;
|
|
125
|
+
nationality: z.ZodOptional<z.ZodString>;
|
|
126
|
+
// Indonesian and Indian did not always have family name
|
|
127
|
+
familyName: z.ZodOptional<z.ZodString>;
|
|
128
|
+
maidenName: z.ZodOptional<z.ZodString>;
|
|
129
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
130
|
+
phoneNumber: z.ZodOptional<z.ZodString>;
|
|
131
|
+
dateOfBirth: z.ZodDate;
|
|
132
|
+
placeOfBirth: z.ZodOptional<z.ZodString>;
|
|
133
|
+
idDocumentCountry: z.ZodString;
|
|
134
|
+
idDocumentNumber: z.ZodString;
|
|
135
|
+
idDocumentType: typeof IDDocumentTypeSchema;
|
|
136
|
+
idDocumentDateOfIssue: z.ZodOptional<z.ZodDate>;
|
|
137
|
+
idDocumentDateOfExpiry: z.ZodOptional<z.ZodDate>;
|
|
138
|
+
idDocumentFrontFile: z.ZodType<Buffer<ArrayBufferLike>>;
|
|
139
|
+
idDocumentBackFile: z.ZodOptional<z.ZodType<Buffer<ArrayBufferLike>>>;
|
|
140
|
+
selfieFile: z.ZodType<Buffer<ArrayBufferLike>>;
|
|
141
|
+
residentialAddress: z.ZodOptional<typeof CredentialResidentialAddressSchema>;
|
|
142
|
+
}> = z
|
|
143
|
+
.object({
|
|
144
|
+
/* ID(unique credential) Unique identifier for the credential itself. */
|
|
145
|
+
id: z.string(),
|
|
146
|
+
|
|
147
|
+
/* First name. */
|
|
148
|
+
firstName: z.string().optional(),
|
|
149
|
+
|
|
150
|
+
/* Middle name. */
|
|
151
|
+
middleName: z.string().optional(),
|
|
152
|
+
|
|
153
|
+
/* Social-security-number (without dashes) */
|
|
154
|
+
ssn: z.string().min(9).max(9).optional(),
|
|
155
|
+
|
|
156
|
+
/* Nationality (ISO 3166-1 alpha-2). */
|
|
157
|
+
nationality: z.string().min(2).max(2).optional(),
|
|
158
|
+
|
|
159
|
+
/* Gender (M or F, empty if not provided). */
|
|
160
|
+
gender: GenderSchema.optional(),
|
|
161
|
+
|
|
162
|
+
/* Family name. */
|
|
163
|
+
familyName: z.string().optional(),
|
|
164
|
+
|
|
165
|
+
/* Maiden name. */
|
|
166
|
+
maidenName: z.string().optional(),
|
|
167
|
+
|
|
168
|
+
/* Date of birth. */
|
|
169
|
+
dateOfBirth: z.date(),
|
|
170
|
+
|
|
171
|
+
/* Place of birth. */
|
|
172
|
+
placeOfBirth: z.string().optional(),
|
|
173
|
+
|
|
174
|
+
/* Email. */
|
|
175
|
+
email: z.email().optional(),
|
|
176
|
+
|
|
177
|
+
/* Phone number. */
|
|
178
|
+
phoneNumber: z.string().optional(),
|
|
179
|
+
|
|
180
|
+
/* Country that issued the identity document (ISO 3166-1 alpha-2). */
|
|
181
|
+
idDocumentCountry: z.string().min(2).max(2),
|
|
182
|
+
|
|
183
|
+
/* ID Document Number Unique number on the identity document. */
|
|
184
|
+
idDocumentNumber: z.string(),
|
|
185
|
+
|
|
186
|
+
/* ID Document Type Type of identity document(e.g., Passport, Driver's License, National ID). */
|
|
187
|
+
idDocumentType: IDDocumentTypeSchema,
|
|
188
|
+
|
|
189
|
+
/* ID Document Date of Issue Date the identity document was issued. */
|
|
190
|
+
idDocumentDateOfIssue: z.date().optional(),
|
|
191
|
+
|
|
192
|
+
/* ID Document Date of Expiry Expiration date of the identity document - if applicable. */
|
|
193
|
+
idDocumentDateOfExpiry: z.date().optional(),
|
|
194
|
+
|
|
195
|
+
/* ID Document Front File Buffer with file representing the front of the identity document. */
|
|
196
|
+
idDocumentFrontFile: z.instanceof(Buffer),
|
|
197
|
+
|
|
198
|
+
/* ID Document Back File Buffer with file representing the back of the identity document - if applicable. */
|
|
199
|
+
idDocumentBackFile: z.instanceof(Buffer).optional(),
|
|
200
|
+
|
|
201
|
+
/* (ID Document) Selfie File Buffer with selfie with the identity document for verification purposes. */
|
|
202
|
+
selfieFile: z.instanceof(Buffer),
|
|
203
|
+
|
|
204
|
+
/* Residential Address Full residential address of the individual - if applicable. */
|
|
205
|
+
residentialAddress: CredentialResidentialAddressSchema.optional(),
|
|
206
|
+
})
|
|
207
|
+
.refine(
|
|
208
|
+
// At least one of firstName or familyName must be present,
|
|
209
|
+
(data) => data.firstName || data.familyName,
|
|
210
|
+
{ message: "At least one of firstName or familyName must be provided" },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
export type CredentialSubject = z.infer<typeof CredentialSubjectSchema>;
|
|
214
|
+
|
|
215
|
+
export interface VerifiableCredentialSubject extends Omit<
|
|
216
|
+
CredentialSubject,
|
|
217
|
+
| "residentialAddress"
|
|
218
|
+
| "idDocumentBackFile"
|
|
219
|
+
| "idDocumentFrontFile"
|
|
220
|
+
| "selfieFile"
|
|
221
|
+
| "idDocumentDateOfIssue"
|
|
222
|
+
| "idDocumentDateOfExpiry"
|
|
223
|
+
| "dateOfBirth"
|
|
224
|
+
> {
|
|
225
|
+
"@context": string;
|
|
226
|
+
// Files are strings in verifiable credentials, but as an input they should be buffers
|
|
227
|
+
idDocumentFrontFile?: string;
|
|
228
|
+
idDocumentBackFile?: string;
|
|
229
|
+
selfieFile?: string;
|
|
230
|
+
// Dates are strings in verifiable credentials
|
|
231
|
+
dateOfBirth?: string;
|
|
232
|
+
idDocumentDateOfIssue?: string;
|
|
233
|
+
idDocumentDateOfExpiry?: string;
|
|
234
|
+
// This is the residential address flattened fields
|
|
235
|
+
residentialAddressStreet?: string;
|
|
236
|
+
residentialAddressHouseNumber?: string;
|
|
237
|
+
residentialAddressAdditionalAddressInfo?: string;
|
|
238
|
+
residentialAddressRegion?: string;
|
|
239
|
+
residentialAddressCity?: string;
|
|
240
|
+
residentialAddressPostalCode?: string;
|
|
241
|
+
residentialAddressCountry?: string;
|
|
242
|
+
residentialAddressProofCategory?: string;
|
|
243
|
+
// Dates are strings in verifiable credentials
|
|
244
|
+
residentialAddressProofDateOfIssue?: string;
|
|
245
|
+
// Files are strings in verifiable credentials, but as an input they should be buffers
|
|
246
|
+
residentialAddressProofFile?: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// TODO: This is a stub of the types for @digitalbazaar/vc
|
|
250
|
+
// when they introduce TypeScript support we should remove this
|
|
251
|
+
// The copy is here because `types.d.ts` file is not bundled.
|
|
252
|
+
export interface VerifiedCredentialsProof {
|
|
253
|
+
type: string;
|
|
254
|
+
created: string;
|
|
255
|
+
verificationMethod: string;
|
|
256
|
+
proofValue: string;
|
|
257
|
+
proofPurpose: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const CredentialSubjectFaceIdSchema: z.ZodObject<{
|
|
261
|
+
faceSignUserId: z.ZodString;
|
|
262
|
+
}> = z.object({
|
|
263
|
+
faceSignUserId: z.string(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export type CredentialSubjectFaceId = z.infer<typeof CredentialSubjectFaceIdSchema>;
|
|
267
|
+
|
|
268
|
+
export interface VerifiedCredentials<K> {
|
|
269
|
+
"@context": string[];
|
|
270
|
+
type: string[];
|
|
271
|
+
issuer: string;
|
|
272
|
+
id: string;
|
|
273
|
+
level: string;
|
|
274
|
+
issued: string;
|
|
275
|
+
approvedAt: string;
|
|
276
|
+
expirationDate: string;
|
|
277
|
+
credentialSubject: K;
|
|
278
|
+
issuanceDate: string;
|
|
279
|
+
proof: VerifiedCredentialsProof;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export type VerifiableCredential<K> = VerifiedCredentials<K>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Ed25519VerificationKey2020,
|
|
3
|
+
Ed25519VerificationKey2020Options,
|
|
4
|
+
} from "@digitalbazaar/ed25519-verification-key-2020";
|
|
5
|
+
|
|
6
|
+
export interface CustomIssuerType {
|
|
7
|
+
issuer: string;
|
|
8
|
+
|
|
9
|
+
/* Multibase encoded public key */
|
|
10
|
+
publicKeyMultibase: string;
|
|
11
|
+
|
|
12
|
+
/* Multibase encoded private key */
|
|
13
|
+
privateKeyMultibase?: string;
|
|
14
|
+
|
|
15
|
+
/* id and controller will be generated by the library */
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AvailableIssuerType =
|
|
19
|
+
| Omit<Ed25519VerificationKey2020Options, "type">
|
|
20
|
+
| Ed25519VerificationKey2020
|
|
21
|
+
| CustomIssuerType;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { Ed25519VerificationKey2020 } from "@digitalbazaar/ed25519-verification-key-2020";
|
|
2
|
+
import { base64Decode, base64Encode, hexEncode, utf8Encode } from "@idos-network/utils/codecs";
|
|
3
|
+
import * as base85 from "base85";
|
|
4
|
+
import { every, get } from "es-toolkit/compat";
|
|
5
|
+
import invariant from "tiny-invariant";
|
|
6
|
+
import nacl from "tweetnacl";
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AvailableIssuerType,
|
|
10
|
+
CredentialFields,
|
|
11
|
+
CredentialResidentialAddress,
|
|
12
|
+
CredentialSubject,
|
|
13
|
+
CredentialSubjectFaceId,
|
|
14
|
+
CustomIssuerType,
|
|
15
|
+
InsertableIDOSCredential,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
export function fileToBase85(file: Buffer): string {
|
|
19
|
+
return base85.encode(file, "ascii85");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function base85ToFile(data: string): Buffer | false {
|
|
23
|
+
return base85.decode(data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function capitalizeFirstLetter(str: string): string {
|
|
27
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {
|
|
31
|
+
return (
|
|
32
|
+
typeof issuer === "object" &&
|
|
33
|
+
issuer !== null &&
|
|
34
|
+
"type" in issuer &&
|
|
35
|
+
"id" in issuer &&
|
|
36
|
+
"controller" in issuer
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {
|
|
41
|
+
return (
|
|
42
|
+
typeof issuer === "object" &&
|
|
43
|
+
issuer !== null &&
|
|
44
|
+
"issuer" in issuer &&
|
|
45
|
+
"publicKeyMultibase" in issuer
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function issuerToKey(
|
|
50
|
+
issuer: AvailableIssuerType,
|
|
51
|
+
): Promise<Ed25519VerificationKey2020> {
|
|
52
|
+
if (isIssuerKey(issuer)) {
|
|
53
|
+
return issuer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isCustomIssuerType(issuer)) {
|
|
57
|
+
return await Ed25519VerificationKey2020.from({
|
|
58
|
+
id: `${issuer.issuer}/keys/1`,
|
|
59
|
+
controller: `${issuer.issuer}/issuers/1`,
|
|
60
|
+
publicKeyMultibase: issuer.publicKeyMultibase,
|
|
61
|
+
privateKeyMultibase: issuer.privateKeyMultibase,
|
|
62
|
+
type: "Ed25519VerificationKey2020",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return await Ed25519VerificationKey2020.from({ ...issuer, type: "Ed25519VerificationKey2020" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function convertValues<
|
|
70
|
+
K extends
|
|
71
|
+
| CredentialFields
|
|
72
|
+
| CredentialSubject
|
|
73
|
+
| CredentialResidentialAddress
|
|
74
|
+
| CredentialSubjectFaceId,
|
|
75
|
+
>(fields: K, prefix?: string): Record<string, unknown> {
|
|
76
|
+
const acc: Record<string, unknown> = {};
|
|
77
|
+
|
|
78
|
+
for (const key in fields) {
|
|
79
|
+
if (Object.hasOwn(fields, key)) {
|
|
80
|
+
const value = fields[key];
|
|
81
|
+
const name = prefix ? `${prefix}${capitalizeFirstLetter(key)}` : key;
|
|
82
|
+
if (value instanceof Date) {
|
|
83
|
+
acc[name] = value.toISOString();
|
|
84
|
+
} else if (value instanceof Buffer) {
|
|
85
|
+
// Convert file to base85
|
|
86
|
+
acc[name] = fileToBase85(value);
|
|
87
|
+
} else {
|
|
88
|
+
acc[name] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return acc;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type BaseLevel = "basic" | "plus";
|
|
97
|
+
type Addon = "liveness" | "email" | "phoneNumber";
|
|
98
|
+
|
|
99
|
+
export function deriveLevel(credential: CredentialSubject): string {
|
|
100
|
+
let level: BaseLevel = "basic";
|
|
101
|
+
|
|
102
|
+
// Address is a sign for plus+
|
|
103
|
+
const address = credential.residentialAddress;
|
|
104
|
+
if (address?.proofFile && address?.city && address?.proofCategory) {
|
|
105
|
+
level = "plus";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const addons: Addon[] = [];
|
|
109
|
+
if (credential.selfieFile) {
|
|
110
|
+
addons.push("liveness");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (credential.email) {
|
|
114
|
+
addons.push("email");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (credential.phoneNumber) {
|
|
118
|
+
addons.push("phoneNumber");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [level, ...addons].join("+");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseLevel(level: string): {
|
|
125
|
+
base: BaseLevel;
|
|
126
|
+
addons: Addon[];
|
|
127
|
+
} {
|
|
128
|
+
const [base, ...addons] = level.split("+") as [BaseLevel, ...Addon[]];
|
|
129
|
+
return { base, addons };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function matchLevelOrHigher(
|
|
133
|
+
level: BaseLevel,
|
|
134
|
+
requiredAddons: Addon[],
|
|
135
|
+
currentLevel: string,
|
|
136
|
+
): boolean {
|
|
137
|
+
const { base: currentBaseLevel, addons: currentAddons } = parseLevel(currentLevel);
|
|
138
|
+
|
|
139
|
+
// TODO: Consider pop+ or uniqueness+ scenarios
|
|
140
|
+
if (level === "plus" && currentBaseLevel !== "plus") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return requiredAddons.every((addon) => currentAddons.includes(addon));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function levelScore(level: string): number {
|
|
148
|
+
const { base, addons } = parseLevel(level);
|
|
149
|
+
let score = 0;
|
|
150
|
+
|
|
151
|
+
if (base === "plus") {
|
|
152
|
+
score += 100;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
score += addons.length * 10;
|
|
156
|
+
|
|
157
|
+
return score;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function pickHighestMatchingLevel(
|
|
161
|
+
levels: string[],
|
|
162
|
+
requiredLevel: BaseLevel,
|
|
163
|
+
requiredAddons: Addon[],
|
|
164
|
+
): string | null {
|
|
165
|
+
return (
|
|
166
|
+
levels
|
|
167
|
+
.filter((currentLevel) => matchLevelOrHigher(requiredLevel, requiredAddons, currentLevel))
|
|
168
|
+
.sort((a, b) => {
|
|
169
|
+
const aAddons = levelScore(a);
|
|
170
|
+
const bAddons = levelScore(b);
|
|
171
|
+
return bAddons - aAddons; // descending
|
|
172
|
+
})[0] ?? null
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function highestMatchingCredential<K extends { public_notes: string }>(
|
|
177
|
+
credentials: K[],
|
|
178
|
+
requiredLevel: BaseLevel,
|
|
179
|
+
{
|
|
180
|
+
addons: requiredAddons = [],
|
|
181
|
+
publicNotesConstraint = {},
|
|
182
|
+
}: {
|
|
183
|
+
addons?: Addon[];
|
|
184
|
+
publicNotesConstraint?: Record<string, number | string>;
|
|
185
|
+
},
|
|
186
|
+
): K | undefined {
|
|
187
|
+
const matchingCredentials = credentials
|
|
188
|
+
.map((credential) => {
|
|
189
|
+
const publicNotes = JSON.parse(credential.public_notes || "{}");
|
|
190
|
+
return {
|
|
191
|
+
credential,
|
|
192
|
+
publicNotes,
|
|
193
|
+
};
|
|
194
|
+
})
|
|
195
|
+
.filter(({ publicNotes }) => {
|
|
196
|
+
const level = publicNotes.level;
|
|
197
|
+
|
|
198
|
+
if (!level) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!matchLevelOrHigher(requiredLevel, requiredAddons, level)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const key in publicNotesConstraint) {
|
|
207
|
+
if (publicNotes[key] !== publicNotesConstraint[key]) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return true;
|
|
213
|
+
})
|
|
214
|
+
.sort((a, b) => {
|
|
215
|
+
const aLevel = a.publicNotes.level;
|
|
216
|
+
const bLevel = b.publicNotes.level;
|
|
217
|
+
return levelScore(bLevel) - levelScore(aLevel); // descending
|
|
218
|
+
})
|
|
219
|
+
.map(({ credential }) => credential);
|
|
220
|
+
|
|
221
|
+
return matchingCredentials[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function recordFilter(
|
|
225
|
+
rec: Record<string, unknown>,
|
|
226
|
+
pick: Record<string, unknown[]>,
|
|
227
|
+
omit: Record<string, unknown[]>,
|
|
228
|
+
): boolean {
|
|
229
|
+
const matchCriteria = (content: Record<string, unknown>, criteria: Record<string, unknown[]>) =>
|
|
230
|
+
every(Object.entries(criteria), ([path, targetSet]) => targetSet.includes(get(content, path)));
|
|
231
|
+
|
|
232
|
+
if (Object.keys(pick).length > 0 && !matchCriteria(rec, pick)) {
|
|
233
|
+
// Fast fail on pick criteria
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (Object.keys(omit).length > 0 && matchCriteria(rec, omit)) {
|
|
238
|
+
// Fast fail on omit criteria
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function buildInsertableIDOSCredential(
|
|
246
|
+
userId: string,
|
|
247
|
+
publicNotes: string,
|
|
248
|
+
content: string,
|
|
249
|
+
encryptorPublicKey: string,
|
|
250
|
+
): InsertableIDOSCredential {
|
|
251
|
+
invariant(encryptorPublicKey, "Missing `encryptorPublicKey`");
|
|
252
|
+
|
|
253
|
+
const ephemeralAuthenticationKeyPair = nacl.sign.keyPair();
|
|
254
|
+
|
|
255
|
+
const publicNotesSignature = nacl.sign.detached(
|
|
256
|
+
utf8Encode(publicNotes),
|
|
257
|
+
ephemeralAuthenticationKeyPair.secretKey,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
user_id: userId,
|
|
262
|
+
content,
|
|
263
|
+
|
|
264
|
+
public_notes: publicNotes,
|
|
265
|
+
public_notes_signature: base64Encode(publicNotesSignature),
|
|
266
|
+
|
|
267
|
+
broader_signature: base64Encode(
|
|
268
|
+
nacl.sign.detached(
|
|
269
|
+
Uint8Array.from([...publicNotesSignature, ...base64Decode(content)]),
|
|
270
|
+
ephemeralAuthenticationKeyPair.secretKey,
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
|
|
274
|
+
issuer_auth_public_key: hexEncode(ephemeralAuthenticationKeyPair.publicKey, true),
|
|
275
|
+
encryptor_public_key: encryptorPublicKey,
|
|
276
|
+
};
|
|
277
|
+
}
|