@tradetrust-tt/dnsprove 2.8.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/src/index.ts ADDED
@@ -0,0 +1,219 @@
1
+ import axios from "axios";
2
+ import { OpenAttestationDNSTextRecord, OpenAttestationDNSTextRecordT } from "./records/dnsTxt";
3
+ import { OpenAttestationDnsDidRecord, OpenAttestationDnsDidRecordT } from "./records/dnsDid";
4
+ import { getLogger } from "./util/logger";
5
+ import { CodedError, DnsproveStatusCode } from "./common/error";
6
+
7
+ const { trace } = getLogger("index");
8
+
9
+ export interface IDNSRecord {
10
+ name: string;
11
+ type: number;
12
+ TTL: number;
13
+ data: string;
14
+ }
15
+
16
+ export interface IDNSQueryResponse {
17
+ AD: boolean; // Whether all response data was validated with DNSSEC,
18
+ Answer: IDNSRecord[];
19
+ }
20
+
21
+ interface GenericObject {
22
+ [key: string]: string;
23
+ }
24
+
25
+ export type CustomDnsResolver = (domain: string) => Promise<IDNSQueryResponse>;
26
+
27
+ export const defaultDnsResolvers: CustomDnsResolver[] = [
28
+ async (domain) => {
29
+ const { data } = await axios({
30
+ method: "GET",
31
+ url: `https://dns.google/resolve?name=${domain}&type=TXT`,
32
+ });
33
+
34
+ return data;
35
+ },
36
+ async (domain) => {
37
+ const { data } = await axios({
38
+ method: "GET",
39
+ url: `https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`,
40
+ headers: { accept: "application/dns-json", contentType: "application/json", connection: "keep-alive" },
41
+ });
42
+ return data;
43
+ },
44
+ ];
45
+
46
+ /**
47
+ * Returns true for strings that are openattestation records
48
+ * @param txtDataString e.g: '"openatts net=ethereum netId=3 addr=0x0c9d5E6C766030cc6f0f49951D275Ad0701F81EC"'
49
+ */
50
+ const isOpenAttestationRecord = (txtDataString: string) => {
51
+ return txtDataString.startsWith("openatts");
52
+ };
53
+
54
+ const trimValue = (str: string) => {
55
+ return str.endsWith(";") ? str.substring(0, str.length - 1).trim() : str.trim();
56
+ };
57
+
58
+ /**
59
+ * Takes a string in the format of "key=value" and adds it to a JS object as key: value
60
+ * @param obj Object that will be modified
61
+ * @param keyValuePair A key value pair to add to the given object
62
+ * @example addKeyValuePairToObject(objectToModify, "foo=bar")
63
+ */
64
+ const addKeyValuePairToObject = (obj: any, keyValuePair: string): any => {
65
+ const [key, ...values] = keyValuePair.split("=");
66
+ const value = values.join("="); // in case there were values with = in them
67
+ /* eslint-disable no-param-reassign */
68
+ // this is necessary because we modify the accumulator in .reduce
69
+ obj[key.trim()] = trimValue(value);
70
+
71
+ return obj;
72
+ };
73
+
74
+ const formatDnsDidRecord = ({ a, v, p, type }: { [key: string]: string }) => {
75
+ return {
76
+ type,
77
+ algorithm: a,
78
+ publicKey: p,
79
+ version: v,
80
+ };
81
+ };
82
+
83
+ export const queryDns = async (domain: string, customDnsResolvers: CustomDnsResolver[]): Promise<IDNSQueryResponse> => {
84
+ let data;
85
+
86
+ let i = 0;
87
+
88
+ while (!data && i < customDnsResolvers.length) {
89
+ try {
90
+ const customDnsResolver = customDnsResolvers[i];
91
+ // eslint-disable-next-line no-await-in-loop
92
+ data = await customDnsResolver(domain);
93
+ } catch (e) {
94
+ i += 1;
95
+ }
96
+ }
97
+
98
+ if (!data) {
99
+ throw new CodedError(
100
+ "Unable to query DNS",
101
+ DnsproveStatusCode.IDNS_QUERY_ERROR_GENERAL,
102
+ "IDNS_QUERY_ERROR_GENERAL"
103
+ );
104
+ }
105
+
106
+ return data;
107
+ };
108
+
109
+ /**
110
+ * Parses one openattestation DNS-TXT record and turns it into an OpenAttestationsDNSTextRecord object
111
+ * @param record e.g: '"openatts net=ethereum netId=3 addr=0x0c9d5E6C766030cc6f0f49951D275Ad0701F81EC"'
112
+ */
113
+ export const parseOpenAttestationRecord = (record: string): GenericObject => {
114
+ trace(`Parsing record: ${record}`);
115
+ const keyValuePairs = record.trim().split(" "); // tokenize into key=value elements
116
+ const recordObject = {} as GenericObject;
117
+ // @ts-ignore: we already checked for this token
118
+ recordObject.type = keyValuePairs.shift();
119
+ keyValuePairs.reduce<GenericObject>(addKeyValuePairToObject, recordObject);
120
+ return recordObject;
121
+ };
122
+
123
+ /**
124
+ * Currying function that applies a given dnssec result
125
+ */
126
+ const applyDnssecResults = <T>(dnssecStatus: boolean) => (record: T): T => {
127
+ return { ...record, dnssec: dnssecStatus };
128
+ };
129
+
130
+ /**
131
+ * Some DNS servers return TXT records with quoted strings, others don't :D
132
+ * @param record
133
+ * @returns unquoted DNS record
134
+ */
135
+ const trimDoubleQuotes = (record: string) => {
136
+ return record.startsWith('"') ? record.slice(1, -1) : record;
137
+ };
138
+
139
+ /**
140
+ * Takes a record set and breaks that info array of key value pairs
141
+ * @param recordSet e.g: [{name: "google.com", type: 16, TTL: 3599, data: '"openatts net=ethereum netId=3 addr=0x2f60375e8144e16Adf1979936301D8341D58C36C"}]
142
+ */
143
+ const parseOpenAttestationRecords = (recordSet: IDNSRecord[] = []): GenericObject[] => {
144
+ trace(`Parsing DNS results: ${JSON.stringify(recordSet)}`);
145
+ return recordSet
146
+ .map((record) => record.data)
147
+ .map(trimDoubleQuotes) // removing leading and trailing quotes if they exist
148
+ .filter(isOpenAttestationRecord)
149
+ .map(parseOpenAttestationRecord);
150
+ };
151
+
152
+ /**
153
+ * Takes a DNS-TXT Record set and returns openattestation document store records if any
154
+ * @param recordSet Refer to tests for examples
155
+ */
156
+ export const parseDocumentStoreResults = (
157
+ recordSet: IDNSRecord[] = [],
158
+ dnssec: boolean
159
+ ): OpenAttestationDNSTextRecord[] => {
160
+ return parseOpenAttestationRecords(recordSet)
161
+ .reduce((prev, curr) => {
162
+ return OpenAttestationDNSTextRecordT.guard(curr) ? [...prev, curr] : prev;
163
+ }, [] as OpenAttestationDNSTextRecord[])
164
+ .map(applyDnssecResults(dnssec));
165
+ };
166
+
167
+ export const parseDnsDidResults = (recordSet: IDNSRecord[] = [], dnssec: boolean): OpenAttestationDnsDidRecord[] => {
168
+ return parseOpenAttestationRecords(recordSet)
169
+ .map(formatDnsDidRecord)
170
+ .reduce((prev, curr) => {
171
+ return OpenAttestationDnsDidRecordT.guard(curr) ? [...prev, curr] : prev;
172
+ }, [] as OpenAttestationDnsDidRecord[])
173
+ .map(applyDnssecResults(dnssec));
174
+ };
175
+
176
+ /**
177
+ * Queries a given domain and parses the results to retrieve openattestation document store records if any
178
+ * @param domain e.g: "example.openattestation.com"
179
+ * @example
180
+ * > getDocumentStoreRecords("example.openattestation.com")
181
+ * > [ { type: 'openatts',
182
+ net: 'ethereum',
183
+ netId: '3',
184
+ addr: '0x2f60375e8144e16Adf1979936301D8341D58C36C',
185
+ dnssec: true } ]
186
+ */
187
+ export const getDocumentStoreRecords = async (
188
+ domain: string,
189
+ customDnsResolvers?: CustomDnsResolver[]
190
+ ): Promise<OpenAttestationDNSTextRecord[]> => {
191
+ trace(`Received request to resolve ${domain}`);
192
+
193
+ const dnsResolvers = customDnsResolvers || defaultDnsResolvers;
194
+
195
+ const results = await queryDns(domain, dnsResolvers);
196
+ const answers = results.Answer || [];
197
+
198
+ trace(`Lookup results: ${JSON.stringify(answers)}`);
199
+
200
+ return parseDocumentStoreResults(answers, results.AD);
201
+ };
202
+
203
+ export const getDnsDidRecords = async (
204
+ domain: string,
205
+ customDnsResolvers?: CustomDnsResolver[]
206
+ ): Promise<OpenAttestationDnsDidRecord[]> => {
207
+ trace(`Received request to resolve ${domain}`);
208
+
209
+ const dnsResolvers = customDnsResolvers || defaultDnsResolvers;
210
+
211
+ const results = await queryDns(domain, dnsResolvers);
212
+ const answers = results.Answer || [];
213
+
214
+ trace(`Lookup results: ${JSON.stringify(answers)}`);
215
+
216
+ return parseDnsDidResults(answers, results.AD);
217
+ };
218
+
219
+ export { OpenAttestationDNSTextRecord, OpenAttestationDnsDidRecord };
@@ -0,0 +1,17 @@
1
+ import { validateDid } from "./dnsDid";
2
+
3
+ describe("validateDid", () => {
4
+ it("returns true for valid did", () => {
5
+ expect(validateDid("did:github:gjgd")).toBe(true);
6
+ expect(validateDid("did:jolo:1fb352353ff51248c5104b407f9c04c3666627fcf5a167d693c9fc84b75964e2")).toBe(true);
7
+ expect(validateDid("did:schema:public-ipfs:xsd:QmUQAxKQ5sbWWrcBZzwkThktfUGZvuPQyTrqMzb3mZnLE5")).toBe(true);
8
+ expect(validateDid("did:sov:builder:VbPQNHsvoLZdaNU7fTBeFx")).toBe(true);
9
+ expect(validateDid("did:ethr:0xE6Fe788d8ca214A080b0f6aC7F48480b2AEfa9a6")).toBe(true);
10
+ });
11
+
12
+ it("returns false for invalid did", () => {
13
+ expect(validateDid("DID:github:gjgd")).toBe(false);
14
+ expect(validateDid("did:GITHUB:gjgd")).toBe(false);
15
+ expect(validateDid("did::gjgd")).toBe(false);
16
+ });
17
+ });
@@ -0,0 +1,29 @@
1
+ import { Static, Boolean, String, Literal, Record, Union, Partial } from "runtypes";
2
+
3
+ // References https://www.w3.org/TR/did-core/#did-syntax
4
+ export const validateDid = (maybeDid: string) => {
5
+ const [did, methodName, ...methodSpecificIdParts] = maybeDid.split(":");
6
+ const methodSpecificId = methodSpecificIdParts.join(":");
7
+ if (did !== "did" || !methodName || !methodSpecificId || !/[a-z]+/.test(methodName)) return false;
8
+ return true;
9
+ };
10
+
11
+ export const RecordTypesT = Literal("openatts");
12
+ export const AlgorithmT = Union(Literal("dns-did"));
13
+ export const VersionT = String;
14
+ export const PublicKeyT = String.withConstraint((maybeDid: string) => {
15
+ return validateDid(maybeDid) || `${maybeDid} is not a valid did`;
16
+ });
17
+
18
+ export const OpenAttestationDnsDidRecordT = Record({
19
+ type: RecordTypesT,
20
+ algorithm: AlgorithmT,
21
+ publicKey: PublicKeyT,
22
+ version: VersionT,
23
+ }).And(
24
+ Partial({
25
+ dnssec: Boolean,
26
+ })
27
+ );
28
+
29
+ export type OpenAttestationDnsDidRecord = Static<typeof OpenAttestationDnsDidRecordT>;
@@ -0,0 +1,51 @@
1
+ import { Static, Boolean, String, Literal, Record, Union, Partial } from "runtypes";
2
+
3
+ export const RecordTypesT = Literal("openatts");
4
+
5
+ export const BlockchainNetworkT = Literal("ethereum");
6
+
7
+ export const EthereumAddressT = String.withConstraint((maybeAddress: string) => {
8
+ return /0x[a-fA-F0-9]{40}/.test(maybeAddress) || `${maybeAddress} is not a valid ethereum address`;
9
+ });
10
+
11
+ export enum EthereumNetworks {
12
+ homestead = "1",
13
+ ropsten = "3",
14
+ rinkeby = "4",
15
+ goerli = "5",
16
+ sepolia = "11155111",
17
+ polygon = "137",
18
+ polygonMumbai = "80001",
19
+ local = "1337",
20
+ xdc = "50",
21
+ xdcapothem = "51",
22
+ }
23
+
24
+ export const EthereumNetworkIdT = Union(
25
+ Literal(EthereumNetworks.homestead),
26
+ Literal(EthereumNetworks.ropsten),
27
+ Literal(EthereumNetworks.rinkeby),
28
+ Literal(EthereumNetworks.goerli),
29
+ Literal(EthereumNetworks.sepolia),
30
+ Literal(EthereumNetworks.polygon),
31
+ Literal(EthereumNetworks.polygonMumbai),
32
+ Literal(EthereumNetworks.xdc),
33
+ Literal(EthereumNetworks.xdcapothem),
34
+ Literal(EthereumNetworks.local)
35
+ );
36
+
37
+ export const OpenAttestationDNSTextRecordT = Record({
38
+ type: RecordTypesT,
39
+ net: BlockchainNetworkT, // key names are directly lifted from the dns-txt record format
40
+ netId: EthereumNetworkIdT, // they are abbreviated because of 255 char constraint on dns-txt records
41
+ addr: EthereumAddressT,
42
+ }).And(
43
+ Partial({
44
+ dnssec: Boolean,
45
+ })
46
+ );
47
+
48
+ export type BlockchainNetwork = Static<typeof BlockchainNetworkT>;
49
+ export type EthereumAddress = Static<typeof EthereumAddressT>;
50
+ export type OpenAttestationDNSTextRecord = Static<typeof OpenAttestationDNSTextRecordT>;
51
+ export type RecordTypes = Static<typeof RecordTypesT>;
@@ -0,0 +1,12 @@
1
+ import debug from "debug";
2
+
3
+ // not using .extends because of next.js resolve modules bug where its picking up old version of debug
4
+ export const trace = (namespace: string) => debug(`dnsprove:trace:${namespace}`);
5
+ export const info = (namespace: string) => debug(`dnsprove:info:${namespace}`);
6
+ export const error = (namespace: string) => debug(`dnsprove:error:${namespace}`);
7
+
8
+ export const getLogger = (namespace: string) => ({
9
+ trace: trace(namespace),
10
+ info: info(namespace),
11
+ error: error(namespace),
12
+ });