stated-protocol-parser 1.0.7 → 5.0.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 (129) hide show
  1. package/README.md +231 -22
  2. package/dist/constants.d.ts +193 -15
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +197 -20
  5. package/dist/constants.js.map +1 -1
  6. package/dist/esm/constants.d.ts +193 -15
  7. package/dist/esm/constants.d.ts.map +1 -1
  8. package/dist/esm/{hash.browser.d.ts → hash.d.ts} +11 -5
  9. package/dist/esm/hash.d.ts.map +1 -0
  10. package/dist/esm/index.d.ts +4 -42
  11. package/dist/esm/index.d.ts.map +1 -1
  12. package/dist/esm/index.js +2102 -641
  13. package/dist/esm/index.js.map +7 -1
  14. package/dist/esm/protocol.d.ts +18 -30
  15. package/dist/esm/protocol.d.ts.map +1 -1
  16. package/dist/esm/signature.d.ts +49 -0
  17. package/dist/esm/signature.d.ts.map +1 -0
  18. package/dist/esm/types.d.ts +26 -60
  19. package/dist/esm/types.d.ts.map +1 -1
  20. package/dist/esm/utils.d.ts +10 -0
  21. package/dist/esm/utils.d.ts.map +1 -1
  22. package/dist/{hash.browser.d.ts → hash.d.ts} +11 -5
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/{hash.browser.js → hash.js} +44 -10
  25. package/dist/hash.js.map +1 -0
  26. package/dist/index.d.ts +4 -42
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -674
  29. package/dist/index.js.map +1 -1
  30. package/dist/protocol.d.ts +18 -30
  31. package/dist/protocol.d.ts.map +1 -1
  32. package/dist/protocol.js +565 -572
  33. package/dist/protocol.js.map +1 -1
  34. package/dist/signature.d.ts +49 -0
  35. package/dist/signature.d.ts.map +1 -0
  36. package/dist/signature.js +169 -0
  37. package/dist/signature.js.map +1 -0
  38. package/dist/types.d.ts +26 -60
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +27 -0
  41. package/dist/types.js.map +1 -1
  42. package/dist/utils.d.ts +10 -0
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +79 -11
  45. package/dist/utils.js.map +1 -1
  46. package/package.json +32 -27
  47. package/src/constants.ts +228 -44
  48. package/src/fixtures.test.ts +236 -0
  49. package/src/hash.test.ts +217 -215
  50. package/src/hash.ts +99 -0
  51. package/src/index.ts +5 -0
  52. package/src/organisation-verification.test.ts +50 -0
  53. package/src/person-verification.test.ts +55 -0
  54. package/src/poll.test.ts +28 -0
  55. package/src/protocol.ts +855 -650
  56. package/src/rating.test.ts +25 -0
  57. package/src/signature.test.ts +200 -0
  58. package/src/signature.ts +159 -0
  59. package/src/statement.test.ts +101 -0
  60. package/src/types.ts +155 -156
  61. package/src/utils.test.ts +140 -0
  62. package/src/utils.ts +102 -16
  63. package/dist/esm/constants.js +0 -47
  64. package/dist/esm/constants.js.map +0 -1
  65. package/dist/esm/hash.browser.d.ts.map +0 -1
  66. package/dist/esm/hash.browser.js +0 -58
  67. package/dist/esm/hash.browser.js.map +0 -1
  68. package/dist/esm/hash.node.d.ts +0 -31
  69. package/dist/esm/hash.node.d.ts.map +0 -1
  70. package/dist/esm/hash.node.js +0 -43
  71. package/dist/esm/hash.node.js.map +0 -1
  72. package/dist/esm/hash.test.d.ts +0 -2
  73. package/dist/esm/hash.test.d.ts.map +0 -1
  74. package/dist/esm/hash.test.js +0 -181
  75. package/dist/esm/hash.test.js.map +0 -1
  76. package/dist/esm/index.browser.d.ts +0 -3
  77. package/dist/esm/index.browser.d.ts.map +0 -1
  78. package/dist/esm/index.browser.js +0 -5
  79. package/dist/esm/index.browser.js.map +0 -1
  80. package/dist/esm/index.node.d.ts +0 -3
  81. package/dist/esm/index.node.d.ts.map +0 -1
  82. package/dist/esm/index.node.js +0 -5
  83. package/dist/esm/index.node.js.map +0 -1
  84. package/dist/esm/index.test.d.ts +0 -2
  85. package/dist/esm/index.test.d.ts.map +0 -1
  86. package/dist/esm/index.test.js +0 -293
  87. package/dist/esm/index.test.js.map +0 -1
  88. package/dist/esm/protocol.js +0 -639
  89. package/dist/esm/protocol.js.map +0 -1
  90. package/dist/esm/types.js +0 -2
  91. package/dist/esm/types.js.map +0 -1
  92. package/dist/esm/utils.js +0 -23
  93. package/dist/esm/utils.js.map +0 -1
  94. package/dist/esm/v3.d.ts +0 -5
  95. package/dist/esm/v3.d.ts.map +0 -1
  96. package/dist/esm/v3.js +0 -60
  97. package/dist/esm/v3.js.map +0 -1
  98. package/dist/hash.browser.d.ts.map +0 -1
  99. package/dist/hash.browser.js.map +0 -1
  100. package/dist/hash.node.d.ts +0 -31
  101. package/dist/hash.node.d.ts.map +0 -1
  102. package/dist/hash.node.js +0 -53
  103. package/dist/hash.node.js.map +0 -1
  104. package/dist/hash.test.d.ts +0 -2
  105. package/dist/hash.test.d.ts.map +0 -1
  106. package/dist/hash.test.js +0 -183
  107. package/dist/hash.test.js.map +0 -1
  108. package/dist/index.browser.d.ts +0 -3
  109. package/dist/index.browser.d.ts.map +0 -1
  110. package/dist/index.browser.js +0 -21
  111. package/dist/index.browser.js.map +0 -1
  112. package/dist/index.node.d.ts +0 -3
  113. package/dist/index.node.d.ts.map +0 -1
  114. package/dist/index.node.js +0 -21
  115. package/dist/index.node.js.map +0 -1
  116. package/dist/index.test.d.ts +0 -2
  117. package/dist/index.test.d.ts.map +0 -1
  118. package/dist/index.test.js +0 -295
  119. package/dist/index.test.js.map +0 -1
  120. package/dist/v3.d.ts +0 -5
  121. package/dist/v3.d.ts.map +0 -1
  122. package/dist/v3.js +0 -64
  123. package/dist/v3.js.map +0 -1
  124. package/src/hash.browser.ts +0 -65
  125. package/src/hash.node.ts +0 -47
  126. package/src/index.browser.ts +0 -4
  127. package/src/index.node.ts +0 -4
  128. package/src/index.test.ts +0 -378
  129. package/src/v3.ts +0 -62
package/src/protocol.ts CHANGED
@@ -1,666 +1,871 @@
1
- /* eslint-disable no-useless-concat */
2
- import { legalForms, UTCFormat, peopleCountBuckets } from './constants'
3
- import { parsePollV3 } from './v3'
4
- import { monthIndex, birthDateFormat, minPeopleCountToRange } from './utils'
1
+ import { legalForms, UTCFormat, peopleCountBuckets } from './constants';
2
+ import { monthIndex, birthDateFormat } from './utils';
3
+ import { verifySignature } from './signature';
4
+ import { sha256 } from './hash';
5
5
  import type {
6
- Statement,
7
- Quotation,
8
- Poll,
9
- OrganisationVerification,
10
- PersonVerification,
11
- Vote,
12
- DisputeAuthenticity,
13
- DisputeContent,
14
- ResponseContent,
15
- PDFSigning,
16
- Rating,
17
- RatingSubjectTypeValue,
18
- Bounty,
19
- Observation,
20
- Boycott
21
- } from './types'
22
-
23
- const fallBackVersion = 3
24
- const version = 4
25
-
26
- export * from './types'
27
- export * from './constants'
28
- export * from './utils'
29
- export * from './v3'
30
-
31
- export const buildStatement = ({ domain, author, time, tags, content, representative, supersededStatement }: Statement) => {
32
- if (content.match(/\nPublishing domain: /)) throw (new Error("Statement must not contain 'Publishing domain: ', as this marks the beginning of a new statement."))
33
- if (content.match(/\n\n/)) throw (new Error("Statement must not contain two line breaks in a row, as this is used for separating statements."))
34
- if (typeof time !== 'object' || !time.toUTCString) throw (new Error("Time must be a Date object."))
35
- if (!domain) throw (new Error("Publishing domain missing."))
36
- const statement = "Publishing domain: " + domain + "\n" +
37
- "Author: " + (author || "") + "\n" +
38
- (representative && representative?.length > 0 ? "Authorized signing representative: " + (representative || "") + "\n" : '') +
39
- "Time: " + time.toUTCString() + "\n" +
40
- (tags && tags.length > 0 ? "Tags: " + tags.join(', ') + "\n" : '') +
41
- (supersededStatement && supersededStatement?.length > 0 ? "Superseded statement: " + (supersededStatement || "") + "\n" : '') +
42
- "Format version: " + version + "\n" +
43
- "Statement content: " + content + (content.match(/\n$/) ? '' : "\n");
44
- if (statement.length > 3000) throw (new Error("Statement must not be longer than 3,000 characters."))
45
- return statement
46
- }
47
-
48
- export const parseStatement = ({ statement: s, allowNoVersion = false }: { statement: string, allowNoVersion?: boolean })
49
- : Statement & { type?: string, formatVersion: string } => {
50
- if (s.length > 3000) throw (new Error("Statement must not be longer than 3,000 characters."))
51
- if (s.match(/\n\n/)) throw new Error("Statements cannot contain two line breaks in a row, as this is used for separating statements.")
52
- const statementRegex = new RegExp(''
53
- + /^Publishing domain: (?<domain>[^\n]+?)\n/.source
54
- + /Author: (?<author>[^\n]+?)\n/.source
55
- + /(?:Authorized signing representative: (?<representative>[^\n]*?)\n)?/.source
56
- + /Time: (?<time>[^\n]+?)\n/.source
57
- + /(?:Tags: (?<tags>[^\n]*?)\n)?/.source
58
- + /(?:Superseded statement: (?<supersededStatement>[^\n]*?)\n)?/.source
59
- + /(?:Format version: (?<formatVersion>[^\n]*?)\n)?/.source
60
- + /Statement content: (?:(?<typedContent>\n\tType: (?<type>[^\n]+?)\n[\s\S]+?\n$)|(?<content>[\s\S]+?\n$))/.source
6
+ Statement,
7
+ Poll,
8
+ OrganisationVerification,
9
+ PersonVerification,
10
+ Vote,
11
+ DisputeAuthenticity,
12
+ DisputeContent,
13
+ ResponseContent,
14
+ PDFSigning,
15
+ Rating,
16
+ RatingSubjectTypeValue,
17
+ } from './types';
18
+ import { isLegalForm, isPeopleCountBucket, isRatingValue } from './types';
19
+
20
+ const version = 5;
21
+
22
+ export * from './types';
23
+ export * from './constants';
24
+ export * from './utils';
25
+
26
+ export const buildStatement = ({
27
+ domain,
28
+ author,
29
+ time,
30
+ tags,
31
+ content,
32
+ representative,
33
+ supersededStatement,
34
+ translations,
35
+ attachments,
36
+ }: Statement) => {
37
+ if (content.match(/\nPublishing domain: /))
38
+ throw new Error(
39
+ "Statement must not contain 'Publishing domain: ', as this marks the beginning of a new statement."
61
40
  );
62
- const match = s.match(statementRegex)
63
- if (!match) throw new Error("Invalid statement format:" + s)
64
-
65
- const m: Partial<Statement> & { type?: string, formatVersion: string, timeStr: string, tagsStr: string } = {
66
- domain: match[1], author: match[2], representative: match[3], timeStr: match[4], tagsStr: match[5],
67
- supersededStatement: match[6], formatVersion: match[7], content: match[8] || match[10],
68
- type: match[9] ? match[9].toLowerCase().replace(' ', '_') : undefined
69
- }
70
- if (!(m['timeStr'].match(UTCFormat))) throw new Error("Invalid statement format: time must be in UTC")
71
- if (!m['domain']) throw new Error("Invalid statement format: domain is required")
72
- if (!m['author']) throw new Error("Invalid statement format: author is required")
73
- if (!m['content']) throw new Error("Invalid statement format: statement content is required")
74
- if (!allowNoVersion && !m['formatVersion']) throw new Error("Invalid statement format: format version is required")
75
-
76
- const tags = m['tagsStr']?.split(', ')
77
- const time = new Date(m['timeStr'])
78
- return {
79
- domain: m['domain'],
80
- author: m['author'],
81
- representative: m['representative'],
82
- time,
83
- tags: (tags && tags.length > 0) ? tags : undefined,
84
- supersededStatement: m['supersededStatement'],
85
- formatVersion: m['formatVersion'] || ('' + fallBackVersion),
86
- content: m['content'],
87
- type: m['type']?.toLowerCase().replace(' ', '_'),
88
- }
89
- }
90
-
91
- export const buildQuotationContent = ({ originalAuthor, authorVerification, originalTime, source,
92
- quotation, paraphrasedStatement, picture, confidence }: Quotation) => {
93
- if (quotation && quotation.match(/\n/)) throw (new Error("Quotation must not contain line breaks."))
94
- if (!paraphrasedStatement && !quotation) throw (new Error("Quotation must contain either a quotation or a paraphrased statement."))
95
- const content = "\n" +
96
- "\t" + "Type: Quotation" + "\n" +
97
- "\t" + "Original author: " + originalAuthor + "\n" +
98
- "\t" + "Author verification: " + authorVerification + "\n" +
99
- (originalTime && originalTime?.length > 0 ? "\t" + "Original publication time: " + originalTime + "\n" : "") +
100
- (source && source?.length > 0 ? "\t" + "Source: " + (source || "") + "\n" : '') +
101
- (picture && picture.length > 0 ? "\t" + "Picture proof: " + (picture || "") + "\n" : '') +
102
- (confidence && confidence?.length > 0 ? "\t" + "Confidence: " + (confidence || "") + "\n" : '') +
103
- (quotation && quotation?.length > 0 ? "\t" + "Quotation: " + (quotation || "") + "\n" : '') +
104
- (paraphrasedStatement && paraphrasedStatement?.length > 0 ? "\t" + "Paraphrased statement: " +
105
- (paraphrasedStatement || "").replace(/\n\t([^\t])/, '\n\t\t($1)') + "\n" : '') +
106
- ""
107
- return content
108
- }
109
-
110
- export const parseQuotation = (s: string): Quotation & { type: string | undefined } => {
111
- const voteRegex = new RegExp(''
112
- + /^\n\tType: Quotation\n/.source
113
- + /\tOriginal author: (?<originalAuthor>[^\n]+?)\n/.source
114
- + /\tAuthor verification: (?<authorVerification>[^\n]+?)\n/.source
115
- + /(?:\tOriginal publication time: (?<originalTime>[^\n]+?)\n)?/.source
116
- + /(?:\tSource: (?<source>[^\n]+?)\n)?/.source
117
- + /(?:\tPicture proof: (?<picture>[^\n]+?)\n)?/.source
118
- + /(?:\tConfidence: (?<confidence>[^\n]+?)\n)?/.source
119
- + /(?:\tQuotation: (?<quotation>[^\n]+?)\n)?/.source
120
- + /(?:\tParaphrased statement: (?:(?<paraphrasedTypedStatement>\n\t\tType: (?<type>[^\n]+?)\n[\s\S]+?)|(?<paraphrasedStatement>[\s\S]+?)))/.source
121
- + /$/.source
41
+ if (content.match(/\n\n/))
42
+ throw new Error(
43
+ 'Statement content must not contain two line breaks in a row, as this is used for separating statements.'
122
44
  );
123
- let match = s.match(voteRegex)
124
- if (!match) throw new Error("Invalid quotation format: " + s)
125
- let m = {} as Quotation & { type: string | undefined }
126
- m = {
127
- originalAuthor: match[1], authorVerification: match[2], originalTime: match[3], source: match[4],
128
- picture: match[5], confidence: match[6], quotation: match[7], paraphrasedStatement: match[8] || match[10],
129
- type: match[9] ? match[9].toLowerCase().replace(' ', '_') : undefined
130
- }
131
- return {
132
- originalAuthor: m['originalAuthor'],
133
- authorVerification: m['authorVerification'],
134
- originalTime: m['originalTime'],
135
- source: m['source'],
136
- picture: m['picture'],
137
- confidence: m['confidence'],
138
- quotation: m['quotation'],
139
- paraphrasedStatement: (m['paraphrasedStatement']?.replace(/\n\t\t/g, "\n\t")),
140
- type: m['type']?.toLowerCase().replace(' ', '_'),
141
- }
142
- }
143
-
144
- export const buildPollContent = ({ country, city, legalEntity, domainScope, judges, deadline, poll,
145
- scopeDescription, scopeQueryLink, options, allowArbitraryVote, requiredProperty: propertyScope, requiredPropertyObserver: propertyScopeObserver }: Poll) => {
146
- if (!poll) throw (new Error("Poll must contain a poll question."))
147
- const scopeContent =
148
- (scopeDescription ? "\t\t" + "Description: " + scopeDescription + "\n" : "") +
149
- (country ? "\t\t" + "Country scope: " + country + "\n" : "") +
150
- (city ? "\t\t" + "City scope: " + city + "\n" : "") +
151
- (legalEntity ? "\t\t" + "Legal form scope: " + legalEntity + "\n" : "") +
152
- (domainScope && domainScope?.length > 0 ? "\t\t" + "Domain scope: " + domainScope.join(', ') + "\n" : "") +
153
- (propertyScope ? "\t\t" + "All entities with the following property: " + propertyScope + "\n" : "") +
154
- (propertyScopeObserver ? "\t\t" + "As observed by: " + propertyScopeObserver + "\n" : "") +
155
- (scopeQueryLink ? "\t\t" + "Link to query defining who can vote: " + scopeQueryLink + "\n" : "")
156
- if (scopeContent.length > 0 && !scopeDescription) throw (new Error("Poll must contain a description of who can vote."))
157
- const content = "\n" +
158
- "\t" + "Type: Poll" + "\n" +
159
- (judges ? "\t" + "The poll outcome is finalized when the following nodes agree: " + judges + "\n" : "") +
160
- (deadline ? "\t" + "Voting deadline: " + deadline.toUTCString() + "\n" : "") +
161
- "\t" + "Poll: " + poll + "\n" +
162
- (options.length > 0 && options[0] ? "\t" + "Option 1: " + options[0] + "\n" : "") +
163
- (options.length > 1 && options[1] ? "\t" + "Option 2: " + options[1] + "\n" : "") +
164
- (options.length > 2 && options[2] ? "\t" + "Option 3: " + options[2] + "\n" : "") +
165
- (options.length > 3 && options[3] ? "\t" + "Option 4: " + options[3] + "\n" : "") +
166
- (options.length > 4 && options[4] ? "\t" + "Option 5: " + options[4] + "\n" : "") +
167
- ((allowArbitraryVote === true || allowArbitraryVote === false) ? ("\t" + "Allow free text votes: " + (allowArbitraryVote ? 'Yes' : 'No') + "\n") : "") +
168
- (scopeContent ? "\t" + "Who can vote: \n" + scopeContent : "") +
169
- ""
170
- return content
171
- }
172
-
173
- export const parsePoll = (s: string, version?: string): Poll => {
174
- if (version && version === '3') return parsePollV3(s)
175
- if (version && version !== '4') throw new Error("Invalid version " + version)
176
- const pollRegex = new RegExp(''
177
- + /^\n\tType: Poll\n/.source
178
- + /(?:\tThe poll outcome is finalized when the following nodes agree: (?<judges>[^\n]+?)\n)?/.source
179
- + /(?:\tVoting deadline: (?<deadline>[^\n]+?)\n)?/.source
180
- + /\tPoll: (?<poll>[^\n]+?)\n/.source
181
- + /(?:\tOption 1: (?<option1>[^\n]+?)\n)?/.source
182
- + /(?:\tOption 2: (?<option2>[^\n]+?)\n)?/.source
183
- + /(?:\tOption 3: (?<option3>[^\n]+?)\n)?/.source
184
- + /(?:\tOption 4: (?<option4>[^\n]+?)\n)?/.source
185
- + /(?:\tOption 5: (?<option5>[^\n]+?)\n)?/.source
186
- + /(?:\tAllow free text votes: (?<allowArbitraryVote>Yes|No)\n)?/.source
187
- + /(?:\tWho can vote: (?<whoCanVote>\n[\s\S]+?\n))?/.source
188
- + /$/.source)
189
- let m: any = s.match(pollRegex)
190
- if (!m) throw new Error("Invalid poll format: " + s)
191
-
192
- m = {
193
- judges: m[1], deadline: m[2], poll: m[3],
194
- option1: m[4], option2: m[5], option3: m[6], option4: m[7], option5: m[8],
195
- allowArbitraryVote: m[9],
196
- whoCanVote: m[10]
197
- }
198
- const whoCanVoteParsed: Partial<Poll> & { domainScopeStr?: string } = {}
199
- if (m.whoCanVote) {
200
- const whoCanVoteRegex = new RegExp(''
201
- + /^\n\t\tDescription: (?<scopeDescription>[^\n]+?)\n/.source
202
- + /(?:\t\tCountry scope: (?<countryScope>[^\n]+?)\n)?/.source
203
- + /(?:\t\tCity scope: (?<cityScope>[^\n]+?)\n)?/.source
204
- + /(?:\t\tLegal form scope: (?<legalEntity>[^\n]+?)\n)?/.source
205
- + /(?:\t\tDomain scope: (?<domainScope>[^\n]+?)\n)?/.source
206
- + /(?:\t\tAll entities with the following property: (?<propertyScope>[^\n]+?)\n)?/.source
207
- + /(?:\t\tAs observed by: (?<propertyScopeObserver>[^\n]+?)\n)?/.source
208
- + /(?:\t\tLink to query defining who can vote: (?<scopeQueryLink>[^\n]+?)\n)?/.source
209
- + /$/.source)
210
- let m2: any = m.whoCanVote.match(whoCanVoteRegex)
211
- if (!m2) throw new Error("Invalid who can vote section: " + m.whoCanVote)
212
- whoCanVoteParsed['scopeDescription'] = m2[1]
213
- whoCanVoteParsed['country'] = m2[2]
214
- whoCanVoteParsed['city'] = m2[3]
215
- whoCanVoteParsed['legalEntity'] = m2[4]
216
- whoCanVoteParsed['domainScopeStr'] = m2[5]
217
- whoCanVoteParsed['requiredProperty'] = m2[6]
218
- whoCanVoteParsed['requiredPropertyObserver'] = m2[7]
219
- whoCanVoteParsed['scopeQueryLink'] = m2[8]
220
- }
221
- const options = [m.option1, m.option2, m.option3, m.option4, m.option5].filter(o => o)
222
- const domainScope = (whoCanVoteParsed.domainScopeStr as string | undefined)?.split(', ')
223
- const allowArbitraryVote = (m['allowArbitraryVote'] === 'Yes' ? true :
224
- (m['allowArbitraryVote'] === 'No' ? false : undefined))
225
- const deadlineStr = m.deadline
226
- if (deadlineStr && !deadlineStr.match(UTCFormat)) throw new Error("Invalid poll, deadline must be in UTC: " + deadlineStr)
227
- return {
228
- judges: m['judges'],
229
- deadline: deadlineStr ? new Date(deadlineStr) : undefined,
230
- poll: m['poll'],
231
- options,
232
- allowArbitraryVote,
233
- country: whoCanVoteParsed['country'],
234
- scopeDescription: whoCanVoteParsed['scopeDescription'],
235
- requiredProperty: whoCanVoteParsed['requiredProperty'],
236
- requiredPropertyObserver: whoCanVoteParsed['requiredPropertyObserver'],
237
- scopeQueryLink: whoCanVoteParsed['scopeQueryLink'],
238
- city: whoCanVoteParsed['city'],
239
- legalEntity: whoCanVoteParsed['legalEntity'],
240
- domainScope: (domainScope && domainScope.length > 0) ? domainScope : undefined,
241
- }
242
- }
243
-
244
- export const buildOrganisationVerificationContent = (
245
- { name, englishName, country, city, province, legalForm, department, domain, foreignDomain, serialNumber,
246
- confidence, reliabilityPolicy, employeeCount, pictureHash, latitude, longitude, population }: OrganisationVerification) => {
247
- if (!name || !country || !legalForm || (!domain && !foreignDomain)) throw new Error("Missing required fields")
248
- if (!Object.values(legalForms).includes(legalForm)) throw new Error("Invalid legal form " + legalForm)
249
- if (employeeCount && !Object.values(peopleCountBuckets).includes(employeeCount)) throw new Error("Invalid employee count " + employeeCount)
250
- if (population && !Object.values(peopleCountBuckets).includes(population)) throw new Error("Invalid population " + population)
251
- if (confidence && !('' + confidence)?.match(/^[0-9.]+$/)) throw new Error("Invalid confidence " + confidence)
252
-
253
- return "\n" +
254
- "\t" + "Type: Organisation verification" + "\n" +
255
- "\t" + "Description: We verified the following information about an organisation." + "\n" +
256
- "\t" + "Name: " + name + "\n" +
257
- (englishName ? "\t" + "English name: " + englishName + "\n" : "") +
258
- "\t" + "Country: " + country + "\n" +
259
- "\t" + "Legal form: " + legalForm + "\n" +
260
- (domain ? "\t" + "Owner of the domain: " + domain + "\n" : "") +
261
- (foreignDomain ? "\t" + "Foreign domain used for publishing statements: " + foreignDomain + "\n" : "") +
262
- (department ? "\t" + "Department using the domain: " + department + "\n" : "") +
263
- (province ? "\t" + "Province or state: " + province + "\n" : "") +
264
- (serialNumber ? "\t" + "Business register number: " + serialNumber + "\n" : "") +
265
- (city ? "\t" + "City: " + city + "\n" : "") +
266
- (latitude ? "\t" + "Latitude: " + latitude + "\n" : "") +
267
- (longitude ? "\t" + "Longitude: " + longitude + "\n" : "") +
268
- (population ? "\t" + "Population: " + population + "\n" : "") +
269
- (pictureHash ? "\t" + "Logo: " + pictureHash + "\n" : "") +
270
- (employeeCount ? "\t" + "Employee count: " + employeeCount + "\n" : "") +
271
- (reliabilityPolicy ? "\t" + "Reliability policy: " + reliabilityPolicy + "\n" : "") +
272
- (confidence ? "\t" + "Confidence: " + confidence + "\n" : "") +
273
- ""
274
- }
275
-
276
- export const parseOrganisationVerification = (s: string): OrganisationVerification => {
277
- const organisationVerificationRegex = new RegExp(''
278
- + /^\n\tType: Organisation verification\n/.source
279
- + /\tDescription: We verified the following information about an organisation.\n/.source
280
- + /\tName: (?<name>[^\n]+?)\n/.source
281
- + /(?:\tEnglish name: (?<englishName>[^\n]+?)\n)?/.source
282
- + /\tCountry: (?<country>[^\n]+?)\n/.source
283
- + /\tLegal (?:form|entity): (?<legalForm>[^\n]+?)\n/.source
284
- + /(?:\tOwner of the domain: (?<domain>[^\n]+?)\n)?/.source
285
- + /(?:\tForeign domain used for publishing statements: (?<foreignDomain>[^\n]+?)\n)?/.source
286
- + /(?:\tDepartment using the domain: (?<department>[^\n]+?)\n)?/.source
287
- + /(?:\tProvince or state: (?<province>[^\n]+?)\n)?/.source
288
- + /(?:\tBusiness register number: (?<serialNumber>[^\n]+?)\n)?/.source
289
- + /(?:\tCity: (?<city>[^\n]+?)\n)?/.source
290
- + /(?:\tLatitude: (?<latitude>[^\n]+?)\n)?/.source
291
- + /(?:\tLongitude: (?<longitude>[^\n]+?)\n)?/.source
292
- + /(?:\tPopulation: (?<population>[^\n]+?)\n)?/.source
293
- + /(?:\tLogo: (?<pictureHash>[^\n]+?)\n)?/.source
294
- + /(?:\tEmployee count: (?<employeeCount>[01,+-]+?)\n)?/.source
295
- + /(?:\tReliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source
296
- + /(?:\tConfidence: (?<confidence>[0-9.]+?)\n)?/.source
297
- + /$/.source
45
+ if (content.match(/\nTranslation [a-z]{2,3}:\n/))
46
+ throw new Error(
47
+ "Statement content must not contain 'Translation XX:\\n' pattern, as this is reserved for translations."
298
48
  );
299
- const m = s.match(organisationVerificationRegex)
300
- if (!m) throw new Error("Invalid organisation verification format: " + s)
301
- return {
302
- name: m[1],
303
- englishName: m[2],
304
- country: m[3],
305
- legalForm: m[4],
306
- domain: m[5],
307
- foreignDomain: m[6],
308
- department: m[7],
309
- province: m[8],
310
- serialNumber: m[9],
311
- city: m[10],
312
- latitude: m[11] ? parseFloat(m[11]) : undefined,
313
- longitude: m[12] ? parseFloat(m[12]) : undefined,
314
- population: m[13],
315
- pictureHash: m[14],
316
- employeeCount: m[15],
317
- reliabilityPolicy: m[16],
318
- confidence: m[17] ? parseFloat(m[17]) : undefined,
319
- }
320
- }
321
-
322
- export const buildPersonVerificationContent = (
323
- { name, countryOfBirth, cityOfBirth, ownDomain, foreignDomain,
324
- dateOfBirth, jobTitle, employer, verificationMethod, confidence,
325
- picture, reliabilityPolicy }: PersonVerification) => {
326
- if (!name || !countryOfBirth || !cityOfBirth || !dateOfBirth || (!ownDomain && !foreignDomain)) {
327
- console.log("Missing required fields: ", { name, countryOfBirth, cityOfBirth, dateOfBirth, ownDomain, foreignDomain })
328
- return ""
49
+ if (content.match(/\nAttachments: /))
50
+ throw new Error(
51
+ "Statement content must not contain 'Attachments: ' on a new line, as this is reserved for file attachments."
52
+ );
53
+ if (typeof time !== 'object' || !time.toUTCString) throw new Error('Time must be a Date object.');
54
+ if (!domain) throw new Error('Publishing domain missing.');
55
+ if (attachments && attachments.length > 5) throw new Error('Maximum 5 attachments allowed.');
56
+ if (attachments) {
57
+ attachments.forEach((attachment, index) => {
58
+ if (!attachment.match(/^[A-Za-z0-9_-]+\.[a-zA-Z0-9]+$/)) {
59
+ throw new Error(
60
+ `Attachment ${index + 1} must be in format 'base64hash.extension' (URL-safe base64)`
61
+ );
62
+ }
63
+ });
64
+ }
65
+
66
+ if (translations) {
67
+ for (const [lang, translation] of Object.entries(translations)) {
68
+ if (translation.match(/\nPublishing domain: /))
69
+ throw new Error(`Translation for ${lang} must not contain 'Publishing domain: '.`);
70
+ if (translation.match(/Translation [a-z]{2,3}:\n/))
71
+ throw new Error(`Translation for ${lang} must not contain 'Translation XX:\\n' pattern.`);
329
72
  }
330
- const [day, month, year] = dateOfBirth.toUTCString().split(' ').filter((i, j) => [1, 2, 3].includes(j))
331
- let content = "\n" +
332
- "\t" + "Type: Person verification" + "\n" +
333
- "\t" + "Description: We verified the following information about a person." + "\n" +
334
- "\t" + "Name: " + name + "\n" +
335
- "\t" + "Date of birth: " + [day.replace(/$0/, ''), month, year].join(' ') + "\n" +
336
- "\t" + "City of birth: " + cityOfBirth + "\n" +
337
- "\t" + "Country of birth: " + countryOfBirth + "\n" +
338
- (jobTitle ? "\t" + "Job title: " + jobTitle + "\n" : "") +
339
- (employer ? "\t" + "Employer: " + employer + "\n" : "") +
340
- (ownDomain ? "\t" + "Owner of the domain: " + ownDomain + "\n" : "") +
341
- (foreignDomain ? "\t" + "Foreign domain used for publishing statements: " + foreignDomain + "\n" : "") +
342
- (picture ? "\t" + "Picture: " + picture + "\n" : "") +
343
- (verificationMethod ? "\t" + "Verification method: " + verificationMethod + "\n" : "") +
344
- (confidence ? "\t" + "Confidence: " + confidence + "\n" : "") +
345
- (reliabilityPolicy ? "\t" + "Reliability policy: " + reliabilityPolicy + "\n" : "") +
346
- ""
347
- return content
348
- }
349
-
350
- export const parsePersonVerification = (s: string): PersonVerification => {
351
- const domainVerificationRegex = new RegExp(''
352
- + /^\n\tType: Person verification\n/.source
353
- + /\tDescription: We verified the following information about a person.\n/.source
354
- + /\tName: (?<name>[^\n]+?)\n/.source
355
- + /\tDate of birth: (?<dateOfBirth>[^\n]+?)\n/.source
356
- + /\tCity of birth: (?<cityOfBirth>[^\n]+?)\n/.source
357
- + /\tCountry of birth: (?<countryOfBirth>[^\n]+?)\n/.source
358
- + /(?:\tJob title: (?<jobTitle>[^\n]+?)\n)?/.source
359
- + /(?:\tEmployer: (?<employer>[^\n]+?)\n)?/.source
360
- + /(?:\tOwner of the domain: (?<domain>[^\n]+?)\n)?/.source
361
- + /(?:\tForeign domain used for publishing statements: (?<foreignDomain>[^\n]+?)\n)?/.source
362
- + /(?:\tPicture: (?<picture>[^\n]+?)\n)?/.source
363
- + /(?:\tVerification method: (?<verificationMethod>[^\n]+?)\n)?/.source
364
- + /(?:\tConfidence: (?<confidence>[^\n]+?)\n)?/.source
365
- + /(?:\tReliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source
366
- + /$/.source
73
+ }
74
+
75
+ const translationLines = translations
76
+ ? Object.entries(translations)
77
+ .map(([lang, translation]) => {
78
+ const translationWithNewline = translation + (translation.match(/\n$/) ? '' : '\n');
79
+ const indentedTranslation = translationWithNewline
80
+ .split('\n')
81
+ .map((line) => (line ? ' ' + line : line))
82
+ .join('\n');
83
+ return `Translation ${lang}:\n${indentedTranslation}`;
84
+ })
85
+ .join('')
86
+ : '';
87
+
88
+ const attachmentLines =
89
+ attachments && attachments.length > 0 ? 'Attachments: ' + attachments.join(', ') + '\n' : '';
90
+
91
+ const contentWithNewline = content + (content.match(/\n$/) ? '' : '\n');
92
+ // Only indent plain content (non-typed statements). Typed statements already have indentation from build functions.
93
+ const isTypedContent = contentWithNewline.trim().startsWith('Type:');
94
+ const finalContent = isTypedContent
95
+ ? contentWithNewline
96
+ : contentWithNewline
97
+ .split('\n')
98
+ .map((line) => (line ? ' ' + line : line))
99
+ .join('\n');
100
+
101
+ const statement =
102
+ 'Stated protocol version: ' +
103
+ version +
104
+ '\n' +
105
+ 'Publishing domain: ' +
106
+ domain +
107
+ '\n' +
108
+ 'Author: ' +
109
+ (author || '') +
110
+ '\n' +
111
+ (representative && representative?.length > 0
112
+ ? 'Authorized signing representative: ' + (representative || '') + '\n'
113
+ : '') +
114
+ 'Time: ' +
115
+ time.toUTCString() +
116
+ '\n' +
117
+ (tags && tags.length > 0 ? 'Tags: ' + tags.join(', ') + '\n' : '') +
118
+ (supersededStatement && supersededStatement?.length > 0
119
+ ? 'Superseded statement: ' + (supersededStatement || '') + '\n'
120
+ : '') +
121
+ 'Statement content:\n' +
122
+ finalContent +
123
+ translationLines +
124
+ attachmentLines;
125
+ if (statement.length > 3000)
126
+ throw new Error('Statement must not be longer than 3,000 characters.');
127
+ return statement;
128
+ };
129
+
130
+ export const parseStatement = ({
131
+ statement: input,
132
+ }: {
133
+ statement: string;
134
+ }): Statement & { type?: string; formatVersion: string } => {
135
+ if (input.length > 3000) throw new Error('Statement must not be longer than 3,000 characters.');
136
+ const beforeTranslations = input.split(/\nTranslation [a-z]{2,3}:\n/)[0];
137
+ if (beforeTranslations.match(/\n\n/))
138
+ throw new Error(
139
+ 'Statements cannot contain two line breaks in a row before translations, as this is used for separating statements.'
367
140
  );
368
- const m = s.match(domainVerificationRegex)
369
- if (!m) throw new Error("Invalid person verification format: " + s)
370
- if (m[2] && !m[2].match(birthDateFormat)) throw new Error("Invalid birth date format: " + m[2])
371
- let { d, month, y } = m[2].match(birthDateFormat)?.groups || {}
372
- if (!d || !month || !y) throw new Error("Invalid birth date format: " + m[2])
373
- return {
374
- name: m[1],
375
- dateOfBirth: new Date(Date.UTC(parseInt(y), monthIndex(month), parseInt(d))),
376
- cityOfBirth: m[3],
377
- countryOfBirth: m[4],
378
- jobTitle: m[5],
379
- employer: m[6],
380
- ownDomain: m[7],
381
- foreignDomain: m[8],
382
- picture: m[9],
383
- verificationMethod: m[10],
384
- confidence: m[11] ? parseFloat(m[11]) : undefined,
385
- reliabilityPolicy: m[12]
141
+
142
+ const signatureRegex = new RegExp(
143
+ '' +
144
+ /^(?<statement>[\s\S]+?)---\n/.source +
145
+ /Statement hash: (?<statementHash>[A-Za-z0-9_-]+)\n/.source +
146
+ /Public key: (?<publicKey>[A-Za-z0-9_-]+)\n/.source +
147
+ /Signature: (?<signature>[A-Za-z0-9_-]+)\n/.source +
148
+ /Algorithm: (?<algorithm>[^\n]+)\n/.source +
149
+ /$/.source
150
+ );
151
+ const signatureMatch = input.match(signatureRegex);
152
+
153
+ let statementToVerify = input;
154
+ let publicKey: string | undefined;
155
+ let signature: string | undefined;
156
+
157
+ if (signatureMatch && signatureMatch.groups) {
158
+ statementToVerify = signatureMatch.groups.statement;
159
+ const statementHash = signatureMatch.groups.statementHash;
160
+ publicKey = signatureMatch.groups.publicKey;
161
+ signature = signatureMatch.groups.signature;
162
+ const algorithm = signatureMatch.groups.algorithm;
163
+
164
+ if (algorithm !== 'Ed25519') {
165
+ throw new Error('Unsupported signature algorithm: ' + algorithm);
386
166
  }
387
- }
388
167
 
389
- export const buildVoteContent = ({ pollHash, poll, vote }: Vote) => {
390
- const content = "\n" +
391
- "\t" + "Type: Vote" + "\n" +
392
- "\t" + "Poll id: " + pollHash + "\n" +
393
- "\t" + "Poll: " + poll + "\n" +
394
- "\t" + "Option: " + vote + "\n" +
395
- ""
396
- return content
397
- }
398
-
399
- export const parseVote = (s: string): Vote => {
400
- const voteRegex = new RegExp(''
401
- + /^\n\tType: Vote\n/.source
402
- + /\tPoll id: (?<pollHash>[^\n]+?)\n/.source
403
- + /\tPoll: (?<poll>[^\n]+?)\n/.source
404
- + /\tOption: (?<vote>[^\n]+?)\n/.source
405
- + /$/.source
406
- );
407
- const m = s.match(voteRegex)
408
- if (!m) throw new Error("Invalid vote format: " + s)
409
- return {
410
- pollHash: m[1],
411
- poll: m[2],
412
- vote: m[3]
168
+ const computedHash = sha256(statementToVerify);
169
+ if (computedHash !== statementHash) {
170
+ throw new Error('Statement hash mismatch');
413
171
  }
414
- }
415
-
416
- export const buildDisputeAuthenticityContent = ({ hash, confidence, reliabilityPolicy }: DisputeAuthenticity) => {
417
- const content = "\n" +
418
- "\t" + "Type: Dispute statement authenticity" + "\n" +
419
- "\t" + "Description: We think that the referenced statement is not authentic.\n" +
420
- "\t" + "Hash of referenced statement: " + hash + "\n" +
421
- (confidence ? "\t" + "Confidence: " + confidence + "\n" : "") +
422
- (reliabilityPolicy ? "\t" + "Reliability policy: " + reliabilityPolicy + "\n" : "") +
423
- ""
424
- return content
425
- }
426
-
427
- export const parseDisputeAuthenticity = (s: string): DisputeAuthenticity => {
428
- const disputeRegex = new RegExp(''
429
- + /^\n\tType: Dispute statement authenticity\n/.source
430
- + /\tDescription: We think that the referenced statement is not authentic.\n/.source
431
- + /\tHash of referenced statement: (?<hash>[^\n]+?)\n/.source
432
- + /(?:\tConfidence: (?<confidence>[^\n]*?)\n)?/.source
433
- + /(?:\tReliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source
434
- + /$/.source
435
- );
436
- const m = s.match(disputeRegex)
437
- if (!m) throw new Error("Invalid dispute authenticity format: " + s)
438
- return {
439
- hash: m[1],
440
- confidence: m[2] ? parseFloat(m[2]) : undefined,
441
- reliabilityPolicy: m[3]
172
+
173
+ const isValid = verifySignature(statementToVerify, signature, publicKey);
174
+ if (!isValid) {
175
+ throw new Error('Invalid cryptographic signature');
442
176
  }
443
- }
444
-
445
- export const buildDisputeContentContent = ({ hash, confidence, reliabilityPolicy }: DisputeContent) => {
446
- const content = "\n" +
447
- "\t" + "Type: Dispute statement content" + "\n" +
448
- "\t" + "Description: We think that the content of the referenced statement is false.\n" +
449
- "\t" + "Hash of referenced statement: " + hash + "\n" +
450
- (confidence ? "\t" + "Confidence: " + confidence + "\n" : "") +
451
- (reliabilityPolicy ? "\t" + "Reliability policy: " + reliabilityPolicy + "\n" : "") +
452
- ""
453
- return content
454
- }
455
-
456
- export const parseDisputeContent = (s: string): DisputeContent => {
457
- const disputeRegex = new RegExp(''
458
- + /^\n\tType: Dispute statement content\n/.source
459
- + /\tDescription: We think that the content of the referenced statement is false.\n/.source
460
- + /\tHash of referenced statement: (?<hash>[^\n]+?)\n/.source
461
- + /(?:\tConfidence: (?<confidence>[^\n]*?)\n)?/.source
462
- + /(?:\tReliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source
463
- + /$/.source
177
+ }
178
+
179
+ const statementRegex = new RegExp(
180
+ '' +
181
+ /^Stated protocol version: (?<formatVersion>[^\n]+?)\n/.source +
182
+ /Publishing domain: (?<domain>[^\n]+?)\n/.source +
183
+ /Author: (?<author>[^\n]+?)\n/.source +
184
+ /(?:Authorized signing representative: (?<representative>[^\n]*?)\n)?/.source +
185
+ /Time: (?<time>[^\n]+?)\n/.source +
186
+ /(?:Tags: (?<tags>[^\n]*?)\n)?/.source +
187
+ /(?:Superseded statement: (?<supersededStatement>[^\n]*?)\n)?/.source +
188
+ /Statement content:\n/.source +
189
+ /(?:(?<typedContent> Type: (?<type>[^\n]+?)\n[\s\S]+?)(?=\nTranslation [a-z]{2,3}:\n|Attachments: |$)|(?<content>(?: [\s\S]+?)?))/
190
+ .source +
191
+ /(?=\nTranslation [a-z]{2,3}:\n|Attachments: |$)/.source +
192
+ /(?<translations>(?:\nTranslation [a-z]{2,3}:\n[\s\S]+?)*)/.source +
193
+ /(?:Attachments: (?<attachments>[^\n]+?)\n)?/.source +
194
+ /$/.source
195
+ );
196
+ const match = statementToVerify.match(statementRegex);
197
+ if (!match || !match.groups) throw new Error('Invalid statement format: ' + input);
198
+
199
+ const {
200
+ domain,
201
+ author,
202
+ representative,
203
+ time: timeStr,
204
+ tags: tagsStr,
205
+ supersededStatement,
206
+ attachments: attachmentsStr,
207
+ formatVersion,
208
+ content,
209
+ typedContent,
210
+ type,
211
+ translations: translationsStr,
212
+ } = match.groups;
213
+
214
+ const parsed = {
215
+ domain,
216
+ author,
217
+ representative,
218
+ timeStr,
219
+ tagsStr,
220
+ supersededStatement,
221
+ formatVersion,
222
+ content: content
223
+ ? content
224
+ .split('\n')
225
+ .map((line) => (line.startsWith(' ') ? line.substring(4) : line))
226
+ .join('\n')
227
+ .replace(/\n$/, '')
228
+ : typedContent,
229
+ type: type ? type.toLowerCase().replace(' ', '_') : undefined,
230
+ translationsStr,
231
+ };
232
+ if (!parsed.timeStr.match(UTCFormat))
233
+ throw new Error('Invalid statement format: time must be in UTC');
234
+ if (!parsed.domain) throw new Error('Invalid statement format: domain is required');
235
+ if (!parsed.author) throw new Error('Invalid statement format: author is required');
236
+ if (!parsed.content) throw new Error('Invalid statement format: statement content is required');
237
+ if (!parsed.formatVersion)
238
+ throw new Error('Invalid statement format: format version is required');
239
+ if (parsed.formatVersion !== '5')
240
+ throw new Error(
241
+ `Invalid statement format: only version 5 is supported, got version ${parsed.formatVersion}`
464
242
  );
465
- const m = s.match(disputeRegex)
466
- if (!m) throw new Error("Invalid dispute content format: " + s)
467
- return {
468
- hash: m[1],
469
- confidence: m[2] ? parseFloat(m[2]) : undefined,
470
- reliabilityPolicy: m[3]
243
+
244
+ const tags = parsed.tagsStr?.split(', ');
245
+ const time = new Date(parsed.timeStr);
246
+
247
+ let attachments: string[] | undefined = undefined;
248
+ if (attachmentsStr && attachmentsStr.length > 0) {
249
+ attachments = attachmentsStr.split(', ').map((a) => a.trim());
250
+ if (attachments.length > 5) {
251
+ throw new Error('Maximum 5 attachments allowed');
471
252
  }
472
- }
253
+ }
254
+
255
+ let translations: Record<string, string> | undefined = undefined;
256
+ if (parsed.translationsStr && parsed.translationsStr.length > 0) {
257
+ translations = {};
258
+ const translationParts = parsed.translationsStr
259
+ .split(/\nTranslation ([a-z]{2,3}):\n/)
260
+ .filter((part) => part.length > 0);
261
+ for (let i = 0; i < translationParts.length; i += 2) {
262
+ if (i + 1 < translationParts.length) {
263
+ const lang = translationParts[i];
264
+ const rawTranslation = translationParts[i + 1];
265
+ // Strip indentation from translation content
266
+ const translation = rawTranslation
267
+ .split('\n')
268
+ .map((line) => (line.startsWith(' ') ? line.substring(4) : line))
269
+ .join('\n')
270
+ .replace(/\n$/, '');
271
+ translations[lang] = translation;
272
+ }
273
+ }
274
+ }
275
+
276
+ return {
277
+ domain: parsed.domain,
278
+ author: parsed.author,
279
+ representative: parsed.representative,
280
+ time,
281
+ tags: tags && tags.length > 0 ? tags : undefined,
282
+ supersededStatement: parsed.supersededStatement,
283
+ formatVersion: parsed.formatVersion,
284
+ content: parsed.content,
285
+ type: parsed.type?.toLowerCase().replace(' ', '_'),
286
+ translations: translations && Object.keys(translations).length > 0 ? translations : undefined,
287
+ attachments: attachments && attachments.length > 0 ? attachments : undefined,
288
+ };
289
+ };
290
+
291
+ export const buildPollContent = ({
292
+ deadline,
293
+ poll,
294
+ scopeDescription,
295
+ options,
296
+ allowArbitraryVote,
297
+ }: Poll) => {
298
+ if (!poll) throw new Error('Poll must contain a poll question.');
299
+ if (poll.includes('\n')) throw new Error('Poll question must be single line.');
300
+ if (scopeDescription && scopeDescription.includes('\n'))
301
+ throw new Error('Scope description must be single line.');
302
+ options.forEach((option, index) => {
303
+ if (option && option.includes('\n'))
304
+ throw new Error(`Option ${index + 1} must be single line.`);
305
+ });
306
+ const content =
307
+ ' Type: Poll\n' +
308
+ (deadline ? ' Voting deadline: ' + deadline.toUTCString() + '\n' : '') +
309
+ ' Poll: ' +
310
+ poll +
311
+ '\n' +
312
+ (options.length > 0 && options[0] ? ' Option 1: ' + options[0] + '\n' : '') +
313
+ (options.length > 1 && options[1] ? ' Option 2: ' + options[1] + '\n' : '') +
314
+ (options.length > 2 && options[2] ? ' Option 3: ' + options[2] + '\n' : '') +
315
+ (options.length > 3 && options[3] ? ' Option 4: ' + options[3] + '\n' : '') +
316
+ (options.length > 4 && options[4] ? ' Option 5: ' + options[4] + '\n' : '') +
317
+ (allowArbitraryVote === true || allowArbitraryVote === false
318
+ ? ' Allow free text votes: ' + (allowArbitraryVote ? 'Yes' : 'No') + '\n'
319
+ : '') +
320
+ (scopeDescription ? ' Who can vote: ' + scopeDescription + '\n' : '');
321
+ return content;
322
+ };
323
+
324
+ export const parsePoll = (content: string, version?: string): Poll => {
325
+ if (version !== '5') throw new Error('Invalid version ' + version);
326
+ const pollRegex = new RegExp(
327
+ '' +
328
+ /^ Type: Poll\n/.source +
329
+ /(?: Voting deadline: (?<deadline>[^\n]+?)\n)?/.source +
330
+ / Poll: (?<poll>[^\n]+?)\n/.source +
331
+ /(?: Option 1: (?<option1>[^\n]+?)\n)?/.source +
332
+ /(?: Option 2: (?<option2>[^\n]+?)\n)?/.source +
333
+ /(?: Option 3: (?<option3>[^\n]+?)\n)?/.source +
334
+ /(?: Option 4: (?<option4>[^\n]+?)\n)?/.source +
335
+ /(?: Option 5: (?<option5>[^\n]+?)\n)?/.source +
336
+ /(?: Allow free text votes: (?<allowArbitraryVote>Yes|No)\n)?/.source +
337
+ /(?: Who can vote: (?<scopeDescription>[^\n]+?)\n)?/.source +
338
+ /$/.source
339
+ );
340
+ const match = content.match(pollRegex);
341
+ if (!match || !match.groups) throw new Error('Invalid poll format: ' + content);
342
+
343
+ const {
344
+ deadline,
345
+ poll,
346
+ option1,
347
+ option2,
348
+ option3,
349
+ option4,
350
+ option5,
351
+ allowArbitraryVote: allowArbitraryVoteStr,
352
+ scopeDescription,
353
+ } = match.groups;
354
+
355
+ const options = [option1, option2, option3, option4, option5].filter((o) => o);
356
+ const allowArbitraryVote =
357
+ allowArbitraryVoteStr === 'Yes' ? true : allowArbitraryVoteStr === 'No' ? false : undefined;
358
+ const deadlineStr = deadline;
359
+ if (deadlineStr && !deadlineStr.match(UTCFormat))
360
+ throw new Error('Invalid poll, deadline must be in UTC: ' + deadlineStr);
361
+ return {
362
+ deadline: deadlineStr ? new Date(deadlineStr) : undefined,
363
+ poll,
364
+ options,
365
+ allowArbitraryVote,
366
+ scopeDescription,
367
+ };
368
+ };
369
+
370
+ export const buildOrganisationVerificationContent = ({
371
+ name,
372
+ englishName,
373
+ country,
374
+ city,
375
+ province,
376
+ legalForm,
377
+ department,
378
+ domain,
379
+ foreignDomain,
380
+ serialNumber,
381
+ confidence,
382
+ reliabilityPolicy,
383
+ employeeCount,
384
+ pictureHash,
385
+ latitude,
386
+ longitude,
387
+ population,
388
+ publicKey,
389
+ }: OrganisationVerification) => {
390
+ if (!name || !country || !legalForm || (!domain && !foreignDomain))
391
+ throw new Error('Missing required fields');
392
+ if (!Object.values(legalForms).includes(legalForm))
393
+ throw new Error('Invalid legal form ' + legalForm);
394
+ if (employeeCount && !Object.values(peopleCountBuckets).includes(employeeCount))
395
+ throw new Error('Invalid employee count ' + employeeCount);
396
+ if (population && !Object.values(peopleCountBuckets).includes(population))
397
+ throw new Error('Invalid population ' + population);
398
+ if (confidence && !('' + confidence)?.match(/^[0-9.]+$/))
399
+ throw new Error('Invalid confidence ' + confidence);
400
+ if (pictureHash && !pictureHash.match(/^[A-Za-z0-9_-]+\.[a-zA-Z0-9]+$/)) {
401
+ throw new Error("Logo must be in format 'base64hash.extension' (URL-safe base64)");
402
+ }
403
+ if (publicKey && !publicKey.match(/^[A-Za-z0-9_-]+$/)) {
404
+ throw new Error('Public key must be in URL-safe base64 format (A-Z, a-z, 0-9, _, -)');
405
+ }
406
+
407
+ return (
408
+ ' Type: Organisation verification\n' +
409
+ ' Description: We verified the following information about an organisation.\n' +
410
+ ' Name: ' +
411
+ name +
412
+ '\n' +
413
+ (englishName ? ' English name: ' + englishName + '\n' : '') +
414
+ ' Country: ' +
415
+ country +
416
+ '\n' +
417
+ ' Legal form: ' +
418
+ legalForm +
419
+ '\n' +
420
+ (domain ? ' Owner of the domain: ' + domain + '\n' : '') +
421
+ (foreignDomain
422
+ ? ' Foreign domain used for publishing statements: ' + foreignDomain + '\n'
423
+ : '') +
424
+ (department ? ' Department using the domain: ' + department + '\n' : '') +
425
+ (province ? ' Province or state: ' + province + '\n' : '') +
426
+ (serialNumber ? ' Business register number: ' + serialNumber + '\n' : '') +
427
+ (city ? ' City: ' + city + '\n' : '') +
428
+ (latitude ? ' Latitude: ' + latitude + '\n' : '') +
429
+ (longitude ? ' Longitude: ' + longitude + '\n' : '') +
430
+ (population ? ' Population: ' + population + '\n' : '') +
431
+ (pictureHash ? ' Logo: ' + pictureHash + '\n' : '') +
432
+ (employeeCount ? ' Employee count: ' + employeeCount + '\n' : '') +
433
+ (publicKey ? ' Public key: ' + publicKey + '\n' : '') +
434
+ (reliabilityPolicy ? ' Reliability policy: ' + reliabilityPolicy + '\n' : '') +
435
+ (confidence ? ' Confidence: ' + confidence + '\n' : '')
436
+ );
437
+ };
438
+
439
+ export const parseOrganisationVerification = (content: string): OrganisationVerification => {
440
+ const organisationVerificationRegex = new RegExp(
441
+ '' +
442
+ /^ Type: Organisation verification\n/.source +
443
+ / Description: We verified the following information about an organisation.\n/.source +
444
+ / Name: (?<name>[^\n]+?)\n/.source +
445
+ /(?: English name: (?<englishName>[^\n]+?)\n)?/.source +
446
+ / Country: (?<country>[^\n]+?)\n/.source +
447
+ / Legal (?:form|entity): (?<legalForm>[^\n]+?)\n/.source +
448
+ /(?: Owner of the domain: (?<domain>[^\n]+?)\n)?/.source +
449
+ /(?: Foreign domain used for publishing statements: (?<foreignDomain>[^\n]+?)\n)?/.source +
450
+ /(?: Department using the domain: (?<department>[^\n]+?)\n)?/.source +
451
+ /(?: Province or state: (?<province>[^\n]+?)\n)?/.source +
452
+ /(?: Business register number: (?<serialNumber>[^\n]+?)\n)?/.source +
453
+ /(?: City: (?<city>[^\n]+?)\n)?/.source +
454
+ /(?: Latitude: (?<latitude>[^\n]+?)\n)?/.source +
455
+ /(?: Longitude: (?<longitude>[^\n]+?)\n)?/.source +
456
+ /(?: Population: (?<population>[^\n]+?)\n)?/.source +
457
+ /(?: Logo: (?<pictureHash>[A-Za-z0-9_-]+\.[a-zA-Z0-9]+)\n)?/.source +
458
+ /(?: Employee count: (?<employeeCount>[01,+-]+?)\n)?/.source +
459
+ /(?: Public key: (?<publicKey>[A-Za-z0-9_-]+)\n)?/.source +
460
+ /(?: Reliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source +
461
+ /(?: Confidence: (?<confidence>[0-9.]+?))?/.source +
462
+ /\n?$/.source
463
+ );
464
+ const match = content.match(organisationVerificationRegex);
465
+ if (!match || !match.groups)
466
+ throw new Error('Invalid organisation verification format: ' + content);
467
+
468
+ const {
469
+ name,
470
+ englishName,
471
+ country,
472
+ legalForm,
473
+ domain,
474
+ foreignDomain,
475
+ department,
476
+ province,
477
+ serialNumber,
478
+ city,
479
+ latitude,
480
+ longitude,
481
+ population,
482
+ pictureHash,
483
+ employeeCount,
484
+ publicKey,
485
+ reliabilityPolicy,
486
+ confidence,
487
+ } = match.groups;
488
+
489
+ if (!isLegalForm(legalForm)) {
490
+ throw new Error('Invalid legal form after validation: ' + legalForm);
491
+ }
492
+
493
+ return {
494
+ name,
495
+ englishName,
496
+ country,
497
+ legalForm,
498
+ domain,
499
+ foreignDomain,
500
+ department,
501
+ province,
502
+ serialNumber,
503
+ city,
504
+ latitude: latitude ? parseFloat(latitude) : undefined,
505
+ longitude: longitude ? parseFloat(longitude) : undefined,
506
+ population: population && isPeopleCountBucket(population) ? population : undefined,
507
+ pictureHash,
508
+ employeeCount: employeeCount && isPeopleCountBucket(employeeCount) ? employeeCount : undefined,
509
+ publicKey,
510
+ reliabilityPolicy,
511
+ confidence: confidence ? parseFloat(confidence) : undefined,
512
+ };
513
+ };
514
+
515
+ export const buildPersonVerificationContent = ({
516
+ name,
517
+ countryOfBirth,
518
+ cityOfBirth,
519
+ ownDomain,
520
+ foreignDomain,
521
+ dateOfBirth,
522
+ jobTitle,
523
+ employer,
524
+ verificationMethod,
525
+ confidence,
526
+ picture,
527
+ reliabilityPolicy,
528
+ publicKey,
529
+ }: PersonVerification) => {
530
+ if (!name || !countryOfBirth || !cityOfBirth || !dateOfBirth || (!ownDomain && !foreignDomain)) {
531
+ throw new Error('Missing required fields for person verification');
532
+ }
533
+ if (publicKey && !publicKey.match(/^[A-Za-z0-9_-]+$/)) {
534
+ throw new Error('Public key must be in URL-safe base64 format (A-Z, a-z, 0-9, _, -)');
535
+ }
536
+ const [day, month, year] = dateOfBirth
537
+ .toUTCString()
538
+ .split(' ')
539
+ .filter((_i, j) => [1, 2, 3].includes(j));
540
+ const content =
541
+ ' Type: Person verification\n' +
542
+ ' Description: We verified the following information about a person.\n' +
543
+ ' Name: ' +
544
+ name +
545
+ '\n' +
546
+ ' Date of birth: ' +
547
+ [day.replace(/^0/, ''), month, year].join(' ') +
548
+ '\n' +
549
+ ' City of birth: ' +
550
+ cityOfBirth +
551
+ '\n' +
552
+ ' Country of birth: ' +
553
+ countryOfBirth +
554
+ '\n' +
555
+ (jobTitle ? ' Job title: ' + jobTitle + '\n' : '') +
556
+ (employer ? ' Employer: ' + employer + '\n' : '') +
557
+ (ownDomain ? ' Owner of the domain: ' + ownDomain + '\n' : '') +
558
+ (foreignDomain
559
+ ? ' Foreign domain used for publishing statements: ' + foreignDomain + '\n'
560
+ : '') +
561
+ (picture ? ' Picture: ' + picture + '\n' : '') +
562
+ (verificationMethod ? ' Verification method: ' + verificationMethod + '\n' : '') +
563
+ (publicKey ? ' Public key: ' + publicKey + '\n' : '') +
564
+ (confidence ? ' Confidence: ' + confidence + '\n' : '') +
565
+ (reliabilityPolicy ? ' Reliability policy: ' + reliabilityPolicy + '\n' : '');
566
+ return content;
567
+ };
568
+
569
+ export const parsePersonVerification = (content: string): PersonVerification => {
570
+ const domainVerificationRegex = new RegExp(
571
+ '' +
572
+ /^ Type: Person verification\n/.source +
573
+ / Description: We verified the following information about a person.\n/.source +
574
+ / Name: (?<name>[^\n]+?)\n/.source +
575
+ / Date of birth: (?<dateOfBirth>[^\n]+?)\n/.source +
576
+ / City of birth: (?<cityOfBirth>[^\n]+?)\n/.source +
577
+ / Country of birth: (?<countryOfBirth>[^\n]+?)\n/.source +
578
+ /(?: Job title: (?<jobTitle>[^\n]+?)\n)?/.source +
579
+ /(?: Employer: (?<employer>[^\n]+?)\n)?/.source +
580
+ /(?: Owner of the domain: (?<domain>[^\n]+?)\n)?/.source +
581
+ /(?: Foreign domain used for publishing statements: (?<foreignDomain>[^\n]+?)\n)?/.source +
582
+ /(?: Picture: (?<picture>[^\n]+?)\n)?/.source +
583
+ /(?: Verification method: (?<verificationMethod>[^\n]+?)\n)?/.source +
584
+ /(?: Public key: (?<publicKey>[A-Za-z0-9_-]+)\n)?/.source +
585
+ /(?: Confidence: (?<confidence>[^\n]+?)\n)?/.source +
586
+ /(?: Reliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source +
587
+ /$/.source
588
+ );
589
+ const match = content.match(domainVerificationRegex);
590
+ if (!match || !match.groups) throw new Error('Invalid person verification format: ' + content);
591
+
592
+ const {
593
+ name,
594
+ dateOfBirth: dateOfBirthStr,
595
+ cityOfBirth,
596
+ countryOfBirth,
597
+ jobTitle,
598
+ employer,
599
+ domain,
600
+ foreignDomain,
601
+ picture,
602
+ verificationMethod,
603
+ publicKey,
604
+ confidence,
605
+ reliabilityPolicy,
606
+ } = match.groups;
607
+
608
+ if (dateOfBirthStr && !dateOfBirthStr.match(birthDateFormat))
609
+ throw new Error('Invalid birth date format: ' + dateOfBirthStr);
610
+ const { d, month, y } = dateOfBirthStr.match(birthDateFormat)?.groups || {};
611
+ if (!d || !month || !y) throw new Error('Invalid birth date format: ' + dateOfBirthStr);
612
+
613
+ return {
614
+ name,
615
+ dateOfBirth: new Date(Date.UTC(parseInt(y), monthIndex(month), parseInt(d))),
616
+ cityOfBirth,
617
+ countryOfBirth,
618
+ jobTitle,
619
+ employer,
620
+ ownDomain: domain,
621
+ foreignDomain,
622
+ picture,
623
+ verificationMethod,
624
+ publicKey,
625
+ confidence: confidence ? parseFloat(confidence) : undefined,
626
+ reliabilityPolicy,
627
+ };
628
+ };
629
+
630
+ export const buildVoteContent = ({ pollHash, poll, vote }: Vote) => {
631
+ const content =
632
+ ' Type: Vote\n' +
633
+ ' Poll id: ' +
634
+ pollHash +
635
+ '\n' +
636
+ ' Poll:\n ' +
637
+ poll +
638
+ '\n' +
639
+ ' Option:\n ' +
640
+ vote +
641
+ '\n';
642
+ return content;
643
+ };
644
+
645
+ export const parseVote = (content: string): Vote => {
646
+ const voteRegex = new RegExp(
647
+ '' +
648
+ /^ Type: Vote\n/.source +
649
+ / Poll id: (?<pollHash>[^\n]+?)\n/.source +
650
+ / Poll:\n (?<poll>[^\n]+?)\n/.source +
651
+ / Option:\n (?<vote>[^\n]+?)\n/.source +
652
+ /$/.source
653
+ );
654
+ const match = content.match(voteRegex);
655
+ if (!match || !match.groups) throw new Error('Invalid vote format: ' + content);
656
+
657
+ const { pollHash, poll, vote } = match.groups;
658
+ return { pollHash, poll, vote };
659
+ };
660
+
661
+ export const buildDisputeAuthenticityContent = ({
662
+ hash,
663
+ confidence,
664
+ reliabilityPolicy,
665
+ }: DisputeAuthenticity) => {
666
+ const content =
667
+ ' Type: Dispute statement authenticity\n' +
668
+ ' Description: We think that the referenced statement is not authentic.\n' +
669
+ ' Hash of referenced statement: ' +
670
+ hash +
671
+ '\n' +
672
+ (confidence ? ' Confidence: ' + confidence + '\n' : '') +
673
+ (reliabilityPolicy ? ' Reliability policy: ' + reliabilityPolicy + '\n' : '');
674
+ return content;
675
+ };
676
+
677
+ export const parseDisputeAuthenticity = (content: string): DisputeAuthenticity => {
678
+ const disputeRegex = new RegExp(
679
+ '' +
680
+ /^ Type: Dispute statement authenticity\n/.source +
681
+ / Description: We think that the referenced statement is not authentic.\n/.source +
682
+ / Hash of referenced statement: (?<hash>[^\n]+?)\n/.source +
683
+ /(?: Confidence: (?<confidence>[^\n]*?)\n)?/.source +
684
+ /(?: Reliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source +
685
+ /$/.source
686
+ );
687
+ const match = content.match(disputeRegex);
688
+ if (!match || !match.groups) throw new Error('Invalid dispute authenticity format: ' + content);
689
+
690
+ const { hash, confidence, reliabilityPolicy } = match.groups;
691
+ return {
692
+ hash,
693
+ confidence: confidence ? parseFloat(confidence) : undefined,
694
+ reliabilityPolicy,
695
+ };
696
+ };
697
+
698
+ export const buildDisputeContentContent = ({
699
+ hash,
700
+ confidence,
701
+ reliabilityPolicy,
702
+ }: DisputeContent) => {
703
+ const content =
704
+ ' Type: Dispute statement content\n' +
705
+ ' Description: We think that the content of the referenced statement is false.\n' +
706
+ ' Hash of referenced statement: ' +
707
+ hash +
708
+ '\n' +
709
+ (confidence ? ' Confidence: ' + confidence + '\n' : '') +
710
+ (reliabilityPolicy ? ' Reliability policy: ' + reliabilityPolicy + '\n' : '');
711
+ return content;
712
+ };
713
+
714
+ export const parseDisputeContent = (content: string): DisputeContent => {
715
+ const disputeRegex = new RegExp(
716
+ '' +
717
+ /^ Type: Dispute statement content\n/.source +
718
+ / Description: We think that the content of the referenced statement is false.\n/.source +
719
+ / Hash of referenced statement: (?<hash>[^\n]+?)\n/.source +
720
+ /(?: Confidence: (?<confidence>[^\n]*?)\n)?/.source +
721
+ /(?: Reliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source +
722
+ /$/.source
723
+ );
724
+ const match = content.match(disputeRegex);
725
+ if (!match || !match.groups) throw new Error('Invalid dispute content format: ' + content);
726
+
727
+ const { hash, confidence, reliabilityPolicy } = match.groups;
728
+ return {
729
+ hash,
730
+ confidence: confidence ? parseFloat(confidence) : undefined,
731
+ reliabilityPolicy,
732
+ };
733
+ };
473
734
 
474
735
  export const buildResponseContent = ({ hash, response }: ResponseContent) => {
475
- const content = "\n" +
476
- "\t" + "Type: Response" + "\n" +
477
- "\t" + "Hash of referenced statement: " + hash + "\n" +
478
- "\t" + "Response: " + response + "\n" +
479
- ""
480
- return content
481
- }
482
-
483
- export const parseResponseContent = (s: string): ResponseContent => {
484
- const disputeRegex = new RegExp(''
485
- + /^\n\tType: Response\n/.source
486
- + /\tHash of referenced statement: (?<hash>[^\n]+?)\n/.source
487
- + /\tResponse: (?<response>[^\n]*?)\n/.source
488
- + /$/.source
489
- );
490
- const m = s.match(disputeRegex)
491
- if (!m) throw new Error("Invalid response content format: " + s)
492
- return {
493
- hash: m[1],
494
- response: m[2]
495
- }
496
- }
736
+ const content =
737
+ ' Type: Response\n' +
738
+ ' Hash of referenced statement: ' +
739
+ hash +
740
+ '\n' +
741
+ ' Response:\n ' +
742
+ response +
743
+ '\n';
744
+ return content;
745
+ };
746
+
747
+ export const parseResponseContent = (content: string): ResponseContent => {
748
+ const responseRegex = new RegExp(
749
+ '' +
750
+ /^ Type: Response\n/.source +
751
+ / Hash of referenced statement: (?<hash>[^\n]+?)\n/.source +
752
+ / Response:\n (?<response>[^\n]*?)\n/.source +
753
+ /$/.source
754
+ );
755
+ const match = content.match(responseRegex);
756
+ if (!match || !match.groups) throw new Error('Invalid response content format: ' + content);
757
+
758
+ const { hash, response } = match.groups;
759
+ return { hash, response };
760
+ };
497
761
 
498
762
  export const buildPDFSigningContent = ({ hash }: PDFSigning) => {
499
- const content = "\n" +
500
- "\t" + "Type: Sign PDF" + "\n" +
501
- "\t" + "Description: We hereby digitally sign the referenced PDF file.\n" +
502
- "\t" + "PDF file hash: " + hash + "\n" +
503
- ""
504
- return content
505
- }
506
-
507
- export const parsePDFSigning = (s: string): PDFSigning => {
508
- const signingRegex = new RegExp(''
509
- + /^\n\tType: Sign PDF\n/.source
510
- + /\tDescription: We hereby digitally sign the referenced PDF file.\n/.source
511
- + /\tPDF file hash: (?<hash>[^\n]+?)\n/.source
512
- + /$/.source
513
- );
514
- const m = s.match(signingRegex)
515
- if (!m) throw new Error("Invalid PDF signing format: " + s)
516
- return {
517
- hash: m[1]
518
- }
519
- }
520
-
521
- export const buildRating = ({ subjectName, subjectType, subjectReference, documentFileHash, rating, quality, comment }: Rating) => {
522
- if (![1, 2, 3, 4, 5].includes(rating)) throw new Error("Invalid rating: " + rating)
523
- const content = "\n" +
524
- "\t" + "Type: Rating" + "\n" +
525
- (subjectType ? "\t" + "Subject type: " + subjectType + "\n" : "") +
526
- "\t" + "Subject name: " + subjectName + "\n" +
527
- (subjectReference ? "\t" + "URL that identifies the subject: " + subjectReference + "\n" : "") +
528
- (documentFileHash ? "\t" + "Document file hash: " + documentFileHash + "\n" : "") +
529
- (quality ? "\t" + "Rated quality: " + quality + "\n" : "") +
530
- "\t" + "Our rating: " + rating + "/5 Stars\n" +
531
- (comment ? "\t" + "Comment: " + comment + "\n" : "") +
532
- ""
533
- return content
534
- }
535
-
536
- export const parseRating = (s: string): Rating => {
537
- const ratingRegex = new RegExp(''
538
- + /^\n\tType: Rating\n/.source
539
- + /(?:\tSubject type: (?<subjectType>[^\n]*?)\n)?/.source
540
- + /\tSubject name: (?<subjectName>[^\n]*?)\n/.source
541
- + /(?:\tURL that identifies the subject: (?<subjectReference>[^\n]*?)\n)?/.source
542
- + /(?:\tDocument file hash: (?<documentFileHash>[^\n]*?)\n)?/.source
543
- + /(?:\tRated quality: (?<quality>[^\n]*?)\n)?/.source
544
- + /\tOur rating: (?<rating>[1-5])\/5 Stars\n/.source
545
- + /(?:\tComment: (?<comment>[\s\S]+?)\n)?/.source
546
- + /$/.source
547
- );
548
- const m = s.match(ratingRegex)
549
- if (!m) throw new Error("Invalid rating format: " + s)
550
- const rating = parseInt(m[6])
551
- if (![1, 2, 3, 4, 5].includes(rating)) throw new Error("Invalid rating: " + m[6])
552
- if (m[1] && !['Organisation', 'Policy proposal', 'Regulation',
553
- 'Treaty draft', 'Product', 'Research publication'].includes(m[1])) throw new Error("Invalid subject type: " + m[1])
554
- if (!m[2]) throw new Error("Missing subject name")
555
- return {
556
- subjectType: m[1] as RatingSubjectTypeValue,
557
- subjectName: m[2],
558
- subjectReference: m[3],
559
- documentFileHash: m[4],
560
- quality: m[5],
561
- rating,
562
- comment: m[7]
563
- }
564
- }
565
-
566
- export const buildBounty = ({ motivation, bounty, reward, judge, judgePay }: Bounty) => {
567
- const content = "\n" +
568
- "\t" + "Type: Bounty" + "\n" +
569
- (motivation ? "\t" + "In order to: " + motivation + "\n" : "") +
570
- "\t" + "We will reward any entity that: " + bounty + "\n" +
571
- "\t" + "The reward is: " + reward + "\n" +
572
- "\t" + "In case of dispute, bounty claims are judged by: " + judge + "\n" +
573
- (judgePay ? "\t" + "The judge will be paid per investigated case with a maxium of: " + judgePay + "\n" : "") +
574
- ""
575
- return content
576
- }
577
-
578
- export const parseBounty = (s: string): Bounty => {
579
- const bountyRegex = new RegExp(''
580
- + /^\n\tType: Bounty\n/.source
581
- + /(?:\tIn order to: (?<motivation>[^\n]*?)\n)?/.source
582
- + /\tWe will reward any entity that: (?<bounty>[^\n]*?)\n/.source
583
- + /\tThe reward is: (?<reward>[^\n]*?)\n/.source
584
- + /\tIn case of dispute, bounty claims are judged by: (?<judge>[^\n]*?)\n/.source
585
- + /(?:\tThe judge will be paid per investigated case with a maxium of: (?<judgePay>[^\n]*?)\n)?/.source
586
- + /$/.source
587
- );
588
- const m = s.match(bountyRegex)
589
- if (!m) throw new Error("Invalid bounty format: " + s)
590
- return {
591
- motivation: m[1],
592
- bounty: m[2],
593
- reward: m[3],
594
- judge: m[4],
595
- judgePay: m[5]
596
- }
597
- }
598
-
599
- export const buildObservation = ({ approach, confidence, reliabilityPolicy, subject, subjectReference, observationReference, property, value }: Observation) => {
600
- const content = "\n" +
601
- "\t" + "Type: Observation" + "\n" +
602
- (approach ? "\t" + "Approach: " + approach + "\n" : "") +
603
- (confidence ? "\t" + "Confidence: " + confidence + "\n" : "") +
604
- (reliabilityPolicy ? "\t" + "Reliability policy: " + reliabilityPolicy + "\n" : "") +
605
- "\t" + "Subject: " + subject + "\n" +
606
- (subjectReference ? "\t" + "Subject identity reference: " + subjectReference + "\n" : "") +
607
- (observationReference ? "\t" + "Observation reference: " + observationReference + "\n" : "") +
608
- "\t" + "Observed property: " + property + "\n" +
609
- (value ? "\t" + "Observed value: " + value + "\n" : "") +
610
- ""
611
- return content
612
- }
613
-
614
- export const parseObservation = (s: string): Observation => {
615
- const observationRegex = new RegExp(''
616
- + /^\n\tType: Observation\n/.source
617
- + /(?:\tApproach: (?<approach>[^\n]*?)\n)?/.source
618
- + /(?:\tConfidence: (?<confidence>[^\n]*?)\n)?/.source
619
- + /(?:\tReliability policy: (?<reliabilityPolicy>[^\n]+?)\n)?/.source
620
- + /\tSubject: (?<subject>[^\n]*?)\n/.source
621
- + /(?:\tSubject identity reference: (?<subjectReference>[^\n]*?)\n)?/.source
622
- + /(?:\tObservation reference: (?<observationReference>[^\n]*?)\n)?/.source
623
- + /\tObserved property: (?<property>[^\n]*?)\n/.source
624
- + /(?:\tObserved value: (?<value>[\s\S]+?)\n)?/.source
625
- + /$/.source
626
- );
627
- const m = s.match(observationRegex)
628
- if (!m) throw new Error("Invalid observation format: " + s)
629
- return {
630
- approach: m[1],
631
- confidence: m[2] ? parseFloat(m[2]) : undefined,
632
- reliabilityPolicy: m[3],
633
- subject: m[4],
634
- subjectReference: m[5],
635
- observationReference: m[6],
636
- property: m[7],
637
- value: m[8]
638
- }
639
- }
640
-
641
- export const buildBoycott = ({ description, subject, subjectReference }: Boycott) => {
642
- const content = "\n" +
643
- "\t" + "Type: Boycott" + "\n" +
644
- (description ? "\t" + "Description: " + description + "\n" : "") +
645
- "\t" + "Subject: " + subject + "\n" +
646
- (subjectReference ? "\t" + "Subject identity reference: " + subjectReference + "\n" : "") +
647
- ""
648
- return content
649
- }
650
-
651
- export const parseBoycott = (s: string): Boycott => {
652
- const observationRegex = new RegExp(''
653
- + /^\n\tType: Boycott\n/.source
654
- + /(?:\tDescription: (?<description>[^\n]*?)\n)?/.source
655
- + /\tSubject: (?<subject>[^\n]*?)\n/.source
656
- + /(?:\tSubject identity reference: (?<subjectReference>[^\n]*?)\n)?/.source
657
- + /$/.source
658
- );
659
- const m = s.match(observationRegex)
660
- if (!m) throw new Error("Invalid observation format: " + s)
661
- return {
662
- description: m[1],
663
- subject: m[2],
664
- subjectReference: m[3],
665
- }
666
- }
763
+ if (!hash.match(/^[A-Za-z0-9_-]+$/)) {
764
+ throw new Error('PDF file hash must be in URL-safe base64 format (A-Z, a-z, 0-9, _, -)');
765
+ }
766
+ const content =
767
+ ' Type: Sign PDF\n' +
768
+ ' Description: We hereby digitally sign the referenced PDF file.\n' +
769
+ ' PDF file hash: ' +
770
+ hash +
771
+ '\n';
772
+ return content;
773
+ };
774
+
775
+ export const parsePDFSigning = (content: string): PDFSigning => {
776
+ const signingRegex = new RegExp(
777
+ '' +
778
+ /^ Type: Sign PDF\n/.source +
779
+ / Description: We hereby digitally sign the referenced PDF file.\n/.source +
780
+ / PDF file hash: (?<hash>[A-Za-z0-9_-]+)\n/.source +
781
+ /$/.source
782
+ );
783
+ const match = content.match(signingRegex);
784
+ if (!match || !match.groups) throw new Error('Invalid PDF signing format: ' + content);
785
+
786
+ const { hash } = match.groups;
787
+ return { hash };
788
+ };
789
+
790
+ export const buildRating = ({
791
+ subjectName,
792
+ subjectType,
793
+ subjectReference,
794
+ documentFileHash,
795
+ rating,
796
+ quality,
797
+ comment,
798
+ }: Rating) => {
799
+ if (![1, 2, 3, 4, 5].includes(rating)) throw new Error('Invalid rating: ' + rating);
800
+ const content =
801
+ ' Type: Rating\n' +
802
+ (subjectType ? ' Subject type: ' + subjectType + '\n' : '') +
803
+ ' Subject name: ' +
804
+ subjectName +
805
+ '\n' +
806
+ (subjectReference ? ' URL that identifies the subject: ' + subjectReference + '\n' : '') +
807
+ (documentFileHash ? ' Document file hash: ' + documentFileHash + '\n' : '') +
808
+ (quality ? ' Rated quality: ' + quality + '\n' : '') +
809
+ ' Our rating: ' +
810
+ rating +
811
+ '/5 Stars\n' +
812
+ (comment ? ' Comment:\n ' + comment + '\n' : '');
813
+ return content;
814
+ };
815
+
816
+ export const parseRating = (content: string): Rating => {
817
+ const ratingRegex = new RegExp(
818
+ '' +
819
+ /^ Type: Rating\n/.source +
820
+ /(?: Subject type: (?<subjectType>[^\n]*?)\n)?/.source +
821
+ / Subject name: (?<subjectName>[^\n]*?)\n/.source +
822
+ /(?: URL that identifies the subject: (?<subjectReference>[^\n]*?)\n)?/.source +
823
+ /(?: Document file hash: (?<documentFileHash>[^\n]*?)\n)?/.source +
824
+ /(?: Rated quality: (?<quality>[^\n]*?)\n)?/.source +
825
+ / Our rating: (?<rating>[1-5])\/5 Stars\n/.source +
826
+ /(?: Comment:\n (?<comment>[\s\S]+?)\n)?/.source +
827
+ /$/.source
828
+ );
829
+ const match = content.match(ratingRegex);
830
+ if (!match || !match.groups) throw new Error('Invalid rating format: ' + content);
831
+
832
+ const {
833
+ subjectType,
834
+ subjectName,
835
+ subjectReference,
836
+ documentFileHash,
837
+ quality,
838
+ rating: ratingStr,
839
+ comment,
840
+ } = match.groups;
841
+
842
+ const rating = parseInt(ratingStr);
843
+ if (![1, 2, 3, 4, 5].includes(rating)) throw new Error('Invalid rating: ' + ratingStr);
844
+ if (
845
+ subjectType &&
846
+ ![
847
+ 'Organisation',
848
+ 'Policy proposal',
849
+ 'Regulation',
850
+ 'Treaty draft',
851
+ 'Product',
852
+ 'Research publication',
853
+ ].includes(subjectType)
854
+ )
855
+ throw new Error('Invalid subject type: ' + subjectType);
856
+ if (!subjectName) throw new Error('Missing subject name');
857
+
858
+ if (!isRatingValue(rating)) {
859
+ throw new Error('Invalid rating after validation: ' + rating);
860
+ }
861
+
862
+ return {
863
+ subjectType: subjectType as RatingSubjectTypeValue,
864
+ subjectName,
865
+ subjectReference,
866
+ documentFileHash,
867
+ quality,
868
+ rating,
869
+ comment,
870
+ };
871
+ };