stated-protocol 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 (62) hide show
  1. package/README.md +409 -0
  2. package/dist/constants.d.ts +225 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +227 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/esm/constants.d.ts +225 -0
  7. package/dist/esm/constants.d.ts.map +1 -0
  8. package/dist/esm/hash.d.ts +37 -0
  9. package/dist/esm/hash.d.ts.map +1 -0
  10. package/dist/esm/index.d.ts +6 -0
  11. package/dist/esm/index.d.ts.map +1 -0
  12. package/dist/esm/index.js +2104 -0
  13. package/dist/esm/index.js.map +7 -0
  14. package/dist/esm/protocol.d.ts +30 -0
  15. package/dist/esm/protocol.d.ts.map +1 -0
  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 +115 -0
  19. package/dist/esm/types.d.ts.map +1 -0
  20. package/dist/esm/utils.d.ts +14 -0
  21. package/dist/esm/utils.d.ts.map +1 -0
  22. package/dist/hash.d.ts +37 -0
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/hash.js +99 -0
  25. package/dist/hash.js.map +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +22 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/protocol.d.ts +30 -0
  31. package/dist/protocol.d.ts.map +1 -0
  32. package/dist/protocol.js +677 -0
  33. package/dist/protocol.js.map +1 -0
  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 +115 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +30 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/utils.d.ts +14 -0
  43. package/dist/utils.d.ts.map +1 -0
  44. package/dist/utils.js +96 -0
  45. package/dist/utils.js.map +1 -0
  46. package/package.json +66 -0
  47. package/src/constants.ts +245 -0
  48. package/src/fixtures.test.ts +236 -0
  49. package/src/hash.test.ts +219 -0
  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 +871 -0
  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 +185 -0
  61. package/src/utils.test.ts +140 -0
  62. package/src/utils.ts +104 -0
@@ -0,0 +1,245 @@
1
+ export const legalForms = {
2
+ local_government: 'local government',
3
+ state_government: 'state government',
4
+ foreign_affairs_ministry: 'foreign affairs ministry',
5
+ corporation: 'corporation',
6
+ };
7
+
8
+ export const statementTypes = {
9
+ statement: 'statement',
10
+ organisationVerification: 'organisation_verification',
11
+ personVerification: 'person_verification',
12
+ poll: 'poll',
13
+ vote: 'vote',
14
+ response: 'response',
15
+ disputeContent: 'dispute_statement_content',
16
+ disputeAuthenticity: 'dispute_statement_authenticity',
17
+ rating: 'rating',
18
+ signPdf: 'sign_pdf',
19
+ unsupported: 'unsupported',
20
+ };
21
+
22
+ export const peopleCountBuckets = {
23
+ '0': '0-10',
24
+ '10': '10-100',
25
+ '100': '100-1000',
26
+ '1000': '1000-10,000',
27
+ '10000': '10,000-100,000',
28
+ '100000': '100,000+',
29
+ '1000000': '1,000,000+',
30
+ '10000000': '10,000,000+',
31
+ };
32
+
33
+ export const supportedLanguages = {
34
+ aa: 'aa',
35
+ ab: 'ab',
36
+ af: 'af',
37
+ ak: 'ak',
38
+ am: 'am',
39
+ ar: 'ar',
40
+ an: 'an',
41
+ as: 'as',
42
+ av: 'av',
43
+ ay: 'ay',
44
+ az: 'az',
45
+ ba: 'ba',
46
+ bm: 'bm',
47
+ be: 'be',
48
+ bn: 'bn',
49
+ bi: 'bi',
50
+ bo: 'bo',
51
+ bs: 'bs',
52
+ br: 'br',
53
+ bg: 'bg',
54
+ ca: 'ca',
55
+ cs: 'cs',
56
+ ch: 'ch',
57
+ ce: 'ce',
58
+ cv: 'cv',
59
+ kw: 'kw',
60
+ co: 'co',
61
+ cr: 'cr',
62
+ cy: 'cy',
63
+ da: 'da',
64
+ de: 'de',
65
+ dv: 'dv',
66
+ dz: 'dz',
67
+ el: 'el',
68
+ en: 'en',
69
+ eo: 'eo',
70
+ et: 'et',
71
+ eu: 'eu',
72
+ ee: 'ee',
73
+ fo: 'fo',
74
+ fa: 'fa',
75
+ fj: 'fj',
76
+ fi: 'fi',
77
+ fr: 'fr',
78
+ fy: 'fy',
79
+ ff: 'ff',
80
+ gd: 'gd',
81
+ ga: 'ga',
82
+ gl: 'gl',
83
+ gv: 'gv',
84
+ gn: 'gn',
85
+ gu: 'gu',
86
+ ht: 'ht',
87
+ ha: 'ha',
88
+ sh: 'sh',
89
+ he: 'he',
90
+ hz: 'hz',
91
+ hi: 'hi',
92
+ ho: 'ho',
93
+ hr: 'hr',
94
+ hu: 'hu',
95
+ hy: 'hy',
96
+ ig: 'ig',
97
+ io: 'io',
98
+ ii: 'ii',
99
+ iu: 'iu',
100
+ ie: 'ie',
101
+ ia: 'ia',
102
+ id: 'id',
103
+ ik: 'ik',
104
+ is: 'is',
105
+ it: 'it',
106
+ jv: 'jv',
107
+ ja: 'ja',
108
+ kl: 'kl',
109
+ kn: 'kn',
110
+ ks: 'ks',
111
+ ka: 'ka',
112
+ kr: 'kr',
113
+ kk: 'kk',
114
+ km: 'km',
115
+ ki: 'ki',
116
+ rw: 'rw',
117
+ ky: 'ky',
118
+ kv: 'kv',
119
+ kg: 'kg',
120
+ ko: 'ko',
121
+ kj: 'kj',
122
+ ku: 'ku',
123
+ lo: 'lo',
124
+ la: 'la',
125
+ lv: 'lv',
126
+ li: 'li',
127
+ ln: 'ln',
128
+ lt: 'lt',
129
+ lb: 'lb',
130
+ lu: 'lu',
131
+ lg: 'lg',
132
+ mh: 'mh',
133
+ ml: 'ml',
134
+ mr: 'mr',
135
+ mk: 'mk',
136
+ mg: 'mg',
137
+ mt: 'mt',
138
+ mn: 'mn',
139
+ mi: 'mi',
140
+ ms: 'ms',
141
+ my: 'my',
142
+ na: 'na',
143
+ nv: 'nv',
144
+ nr: 'nr',
145
+ nd: 'nd',
146
+ ng: 'ng',
147
+ ne: 'ne',
148
+ nl: 'nl',
149
+ nn: 'nn',
150
+ nb: 'nb',
151
+ no: 'no',
152
+ ny: 'ny',
153
+ oc: 'oc',
154
+ oj: 'oj',
155
+ or: 'or',
156
+ om: 'om',
157
+ os: 'os',
158
+ pa: 'pa',
159
+ pi: 'pi',
160
+ pl: 'pl',
161
+ pt: 'pt',
162
+ ps: 'ps',
163
+ qu: 'qu',
164
+ rm: 'rm',
165
+ ro: 'ro',
166
+ rn: 'rn',
167
+ ru: 'ru',
168
+ sg: 'sg',
169
+ sa: 'sa',
170
+ si: 'si',
171
+ sk: 'sk',
172
+ sl: 'sl',
173
+ se: 'se',
174
+ sm: 'sm',
175
+ sn: 'sn',
176
+ sd: 'sd',
177
+ so: 'so',
178
+ st: 'st',
179
+ es: 'es',
180
+ sq: 'sq',
181
+ sc: 'sc',
182
+ sr: 'sr',
183
+ ss: 'ss',
184
+ su: 'su',
185
+ sw: 'sw',
186
+ sv: 'sv',
187
+ ty: 'ty',
188
+ ta: 'ta',
189
+ tt: 'tt',
190
+ te: 'te',
191
+ tg: 'tg',
192
+ tl: 'tl',
193
+ th: 'th',
194
+ ti: 'ti',
195
+ to: 'to',
196
+ tn: 'tn',
197
+ ts: 'ts',
198
+ tk: 'tk',
199
+ tr: 'tr',
200
+ tw: 'tw',
201
+ ug: 'ug',
202
+ uk: 'uk',
203
+ ur: 'ur',
204
+ uz: 'uz',
205
+ ve: 've',
206
+ vi: 'vi',
207
+ vo: 'vo',
208
+ wa: 'wa',
209
+ wo: 'wo',
210
+ xh: 'xh',
211
+ yi: 'yi',
212
+ yo: 'yo',
213
+ za: 'za',
214
+ zh: 'zh',
215
+ zu: 'zu',
216
+ } as const;
217
+
218
+ export type SupportedLanguage = (typeof supportedLanguages)[keyof typeof supportedLanguages];
219
+
220
+ export const UTCFormat: RegExp =
221
+ /(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT/;
222
+
223
+ export const pollKeys =
224
+ /(Type: |The poll outcome is finalized when the following nodes agree: |Voting deadline: |Poll: |Option 1: |Option 2: |Option 3: |Option 4: |Option 5: |Allow free text votes: |Who can vote: |Description: |Country scope: |City scope: |Legal form scope: |Domain scope: |All entities with the following property: |As observed by: |Link to query defining who can vote: )/g;
225
+
226
+ export const organisationVerificationKeys =
227
+ /(Type: |Description: |Name: |English name: |Country: |Legal entity: |Legal form: |Department using the domain: |Owner of the domain: |Foreign domain used for publishing statements: |Province or state: |Business register number: |City: |Longitude: |Latitude: |Population: |Logo: |Employee count: |Reliability policy: |Confidence: |Public key: )/g;
228
+
229
+ export const personVerificationKeys =
230
+ /(Type: |Description: |Name: |Date of birth: |City of birth: |Country of birth: |Job title: |Employer: |Owner of the domain: |Foreign domain used for publishing statements: |Picture: |Verification method: |Confidence: |Reliability policy: )/g;
231
+
232
+ export const voteKeys = /(Type: |Poll id: |Poll: |Option: )/g;
233
+
234
+ export const disputeAuthenticityKeys =
235
+ /(Type: |Description: |Hash of referenced statement: |Confidence: |Reliability policy: )/g;
236
+
237
+ export const disputeContentKeys =
238
+ /(Type: |Description: |Hash of referenced statement: |Confidence: |Reliability policy: )/g;
239
+
240
+ export const responseKeys = /(Type: |Hash of referenced statement: |Response: )/;
241
+
242
+ export const PDFSigningKeys = /(Type: |Description: |PDF file hash: )/g;
243
+
244
+ export const ratingKeys =
245
+ /(Type: |Subject type: |Subject name: |URL that identifies the subject: |Document file hash: |Rated quality: |Our rating: |Comment: )/;
@@ -0,0 +1,236 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert';
6
+ import {
7
+ buildStatement,
8
+ parseStatement,
9
+ buildPollContent,
10
+ buildVoteContent,
11
+ buildOrganisationVerificationContent,
12
+ buildPersonVerificationContent,
13
+ buildDisputeAuthenticityContent,
14
+ buildDisputeContentContent,
15
+ buildResponseContent,
16
+ buildPDFSigningContent,
17
+ buildRating,
18
+ } from './protocol';
19
+ import { verifySignedStatement } from './signature';
20
+
21
+ // ESM-compatible __dirname
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ const fixturesDir = path.join(__dirname, '../fixtures');
26
+
27
+ function getFixtureDirs(): string[] {
28
+ if (!fs.existsSync(fixturesDir)) {
29
+ return [];
30
+ }
31
+ return fs
32
+ .readdirSync(fixturesDir, { withFileTypes: true })
33
+ .filter((dirent) => dirent.isDirectory())
34
+ .map((dirent) => dirent.name);
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ function buildContentFromInput(contentObj: any): string {
39
+ if (!contentObj || typeof contentObj !== 'object') {
40
+ throw new Error('Content must be an object with a type field');
41
+ }
42
+
43
+ switch (contentObj.type) {
44
+ case 'poll':
45
+ return buildPollContent({
46
+ poll: contentObj.poll,
47
+ options: contentObj.options || [],
48
+ deadline: contentObj.deadline ? new Date(contentObj.deadline) : undefined,
49
+ scopeDescription: contentObj.scopeDescription,
50
+ allowArbitraryVote: contentObj.allowArbitraryVote,
51
+ });
52
+ case 'vote':
53
+ return buildVoteContent({
54
+ pollHash: contentObj.pollHash,
55
+ poll: contentObj.poll,
56
+ vote: contentObj.vote,
57
+ });
58
+ case 'organisation_verification':
59
+ return buildOrganisationVerificationContent({
60
+ name: contentObj.name,
61
+ englishName: contentObj.englishName,
62
+ country: contentObj.country,
63
+ city: contentObj.city,
64
+ province: contentObj.province,
65
+ legalForm: contentObj.legalForm,
66
+ department: contentObj.department,
67
+ domain: contentObj.domain,
68
+ foreignDomain: contentObj.foreignDomain,
69
+ serialNumber: contentObj.serialNumber,
70
+ confidence: contentObj.confidence,
71
+ reliabilityPolicy: contentObj.reliabilityPolicy,
72
+ employeeCount: contentObj.employeeCount,
73
+ pictureHash: contentObj.pictureHash,
74
+ latitude: contentObj.latitude,
75
+ longitude: contentObj.longitude,
76
+ population: contentObj.population,
77
+ publicKey: contentObj.publicKey,
78
+ });
79
+ case 'person_verification':
80
+ return buildPersonVerificationContent({
81
+ name: contentObj.name,
82
+ countryOfBirth: contentObj.countryOfBirth,
83
+ cityOfBirth: contentObj.cityOfBirth,
84
+ ownDomain: contentObj.ownDomain,
85
+ foreignDomain: contentObj.foreignDomain,
86
+ dateOfBirth: new Date(contentObj.dateOfBirth),
87
+ jobTitle: contentObj.jobTitle,
88
+ employer: contentObj.employer,
89
+ verificationMethod: contentObj.verificationMethod,
90
+ confidence: contentObj.confidence,
91
+ picture: contentObj.picture,
92
+ reliabilityPolicy: contentObj.reliabilityPolicy,
93
+ publicKey: contentObj.publicKey,
94
+ });
95
+ case 'dispute_authenticity':
96
+ return buildDisputeAuthenticityContent({
97
+ hash: contentObj.hash,
98
+ confidence: contentObj.confidence,
99
+ reliabilityPolicy: contentObj.reliabilityPolicy,
100
+ });
101
+ case 'dispute_content':
102
+ return buildDisputeContentContent({
103
+ hash: contentObj.hash,
104
+ confidence: contentObj.confidence,
105
+ reliabilityPolicy: contentObj.reliabilityPolicy,
106
+ });
107
+ case 'response':
108
+ return buildResponseContent({
109
+ hash: contentObj.hash,
110
+ response: contentObj.response,
111
+ });
112
+ case 'pdf_signing':
113
+ return buildPDFSigningContent({
114
+ hash: contentObj.hash,
115
+ });
116
+ case 'rating':
117
+ return buildRating({
118
+ subjectName: contentObj.subjectName,
119
+ subjectType: contentObj.subjectType,
120
+ subjectReference: contentObj.subjectReference,
121
+ documentFileHash: contentObj.documentFileHash,
122
+ rating: contentObj.rating,
123
+ quality: contentObj.quality,
124
+ comment: contentObj.comment,
125
+ });
126
+ default:
127
+ throw new Error(`Unknown content type: ${contentObj.type}`);
128
+ }
129
+ }
130
+
131
+ describe('Fixture Validation', () => {
132
+ const fixtureDirs = getFixtureDirs();
133
+
134
+ if (fixtureDirs.length === 0) {
135
+ it('no fixtures found', () => {
136
+ // No fixture directories found
137
+ assert.ok(true);
138
+ });
139
+ return;
140
+ }
141
+
142
+ for (const dir of fixtureDirs) {
143
+ describe(`Fixture: ${dir}`, () => {
144
+ const inputPath = path.join(fixturesDir, dir, 'input.json');
145
+ const outputPath = path.join(fixturesDir, dir, 'output.txt');
146
+
147
+ if (!fs.existsSync(inputPath)) {
148
+ it('should have input.json', () => {
149
+ assert.fail(`Missing input.json in ${dir}`);
150
+ });
151
+ return;
152
+ }
153
+
154
+ if (!fs.existsSync(outputPath)) {
155
+ it('should have output.txt', () => {
156
+ assert.fail(`Missing output.txt in ${dir}`);
157
+ });
158
+ return;
159
+ }
160
+
161
+ it('output.txt should match built statement from input.json', async () => {
162
+ const input = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
163
+ const expectedOutput = fs.readFileSync(outputPath, 'utf-8');
164
+
165
+ if (input.signature) {
166
+ const isValid = await verifySignedStatement(expectedOutput);
167
+ assert.strictEqual(isValid, true);
168
+ return;
169
+ }
170
+
171
+ let content: string;
172
+ if (typeof input.content === 'string') {
173
+ content = input.content;
174
+ } else {
175
+ content = buildContentFromInput(input.content);
176
+ }
177
+
178
+ // Build statement
179
+ const builtStatement = buildStatement({
180
+ domain: input.domain,
181
+ author: input.author,
182
+ time: new Date(input.time),
183
+ tags: input.tags,
184
+ content,
185
+ representative: input.representative,
186
+ supersededStatement: input.supersededStatement,
187
+ translations: input.translations,
188
+ attachments: input.attachments,
189
+ });
190
+
191
+ assert.strictEqual(builtStatement, expectedOutput);
192
+ });
193
+
194
+ it('output.txt should not contain double newlines', () => {
195
+ const output = fs.readFileSync(outputPath, 'utf-8');
196
+ assert.ok(!/\n\n/.test(output));
197
+ });
198
+
199
+ it('output.txt should be parseable', () => {
200
+ const output = fs.readFileSync(outputPath, 'utf-8');
201
+ const parsed = parseStatement({ statement: output });
202
+
203
+ assert.ok(parsed.domain);
204
+ assert.ok(parsed.author);
205
+ assert.ok(parsed.content);
206
+ assert.strictEqual(parsed.formatVersion, '5');
207
+ });
208
+
209
+ it('round-trip: parse(output.txt) should match input.json structure', () => {
210
+ const input = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
211
+ const output = fs.readFileSync(outputPath, 'utf-8');
212
+ const parsed = parseStatement({ statement: output });
213
+
214
+ assert.strictEqual(parsed.domain, input.domain);
215
+ assert.strictEqual(parsed.author, input.author);
216
+ assert.strictEqual(new Date(parsed.time).toISOString(), new Date(input.time).toISOString());
217
+
218
+ if (input.tags) {
219
+ assert.deepStrictEqual(parsed.tags, input.tags);
220
+ }
221
+ if (input.representative) {
222
+ assert.strictEqual(parsed.representative, input.representative);
223
+ }
224
+ if (input.supersededStatement) {
225
+ assert.strictEqual(parsed.supersededStatement, input.supersededStatement);
226
+ }
227
+ if (input.attachments) {
228
+ assert.deepStrictEqual(parsed.attachments, input.attachments);
229
+ }
230
+ if (input.translations) {
231
+ assert.deepStrictEqual(parsed.translations, input.translations);
232
+ }
233
+ });
234
+ });
235
+ }
236
+ });
@@ -0,0 +1,219 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { sha256, verify, fromUrlSafeBase64, toUrlSafeBase64 } from './hash';
4
+
5
+ describe('Hash utilities', () => {
6
+ describe('sha256', () => {
7
+ it('should hash a simple string', () => {
8
+ const input = 'hello world';
9
+ const hash = sha256(input);
10
+
11
+ // Verify it's URL-safe (no +, /, or =)
12
+ assert.ok(!hash.includes('+'));
13
+ assert.ok(!hash.includes('/'));
14
+ assert.ok(!hash.includes('='));
15
+
16
+ // Verify consistent output
17
+ const hash2 = sha256(input);
18
+ assert.strictEqual(hash, hash2);
19
+ });
20
+
21
+ it('should produce correct hash for known input', () => {
22
+ const input = 'hello world';
23
+ const expectedHash = 'uU0nuZNNPgilLlLX2n2r-sSE7-N6U4DukIj3rOLvzek';
24
+ const hash = sha256(input);
25
+ assert.strictEqual(hash, expectedHash);
26
+ });
27
+
28
+ it('should handle empty string', () => {
29
+ const hash = sha256('');
30
+ assert.ok(hash);
31
+ assert.strictEqual(typeof hash, 'string');
32
+ });
33
+
34
+ it('should handle unicode characters', () => {
35
+ const input = '你好世界 🌍';
36
+ const hash = sha256(input);
37
+ assert.ok(hash);
38
+ assert.strictEqual(typeof hash, 'string');
39
+
40
+ // Verify consistency
41
+ const hash2 = sha256(input);
42
+ assert.strictEqual(hash, hash2);
43
+ });
44
+
45
+ it('should handle Buffer input', () => {
46
+ const data = Buffer.from('hello world');
47
+ const hash = sha256(data);
48
+
49
+ // Should produce same hash as string input
50
+ const stringHash = sha256('hello world');
51
+ assert.strictEqual(hash, stringHash);
52
+ });
53
+
54
+ it('should produce different hashes for different inputs', () => {
55
+ const hash1 = sha256('hello');
56
+ const hash2 = sha256('world');
57
+ assert.notStrictEqual(hash1, hash2);
58
+ });
59
+
60
+ it('should produce 43-character URL-safe base64 string', () => {
61
+ const hash = sha256('test');
62
+ // SHA-256 produces 256 bits = 32 bytes
63
+ // Base64 encoding: 32 bytes * 4/3 = 42.67, rounded up = 43 chars (without padding)
64
+ assert.strictEqual(hash.length, 43);
65
+ });
66
+ });
67
+
68
+ describe('verify', () => {
69
+ it('should verify correct hash', () => {
70
+ const content = 'hello world';
71
+ const hash = sha256(content);
72
+ const isValid = verify(content, hash);
73
+ assert.strictEqual(isValid, true);
74
+ });
75
+
76
+ it('should reject incorrect hash', () => {
77
+ const content = 'hello world';
78
+ const wrongHash = 'incorrect_hash_value_here_1234567890';
79
+ const isValid = verify(content, wrongHash);
80
+ assert.strictEqual(isValid, false);
81
+ });
82
+
83
+ it('should reject hash for different content', () => {
84
+ const content1 = 'hello world';
85
+ const content2 = 'goodbye world';
86
+ const hash1 = sha256(content1);
87
+ const isValid = verify(content2, hash1);
88
+ assert.strictEqual(isValid, false);
89
+ });
90
+
91
+ it('should work with Buffer', () => {
92
+ const data = Buffer.from('test data');
93
+ const hash = sha256(data);
94
+ const isValid = verify(data, hash);
95
+ assert.strictEqual(isValid, true);
96
+ });
97
+ });
98
+
99
+ describe('fromUrlSafeBase64', () => {
100
+ it('should convert URL-safe base64 to standard base64', () => {
101
+ // Use a URL-safe string that contains both - and _ characters
102
+ const urlSafe = 'abc-def_ghi';
103
+ const standard = fromUrlSafeBase64(urlSafe);
104
+
105
+ // Should replace - with + and _ with /
106
+ assert.ok(standard.includes('+'));
107
+ assert.ok(standard.includes('/'));
108
+ // Should add padding
109
+ assert.strictEqual(standard.endsWith('='), true);
110
+ });
111
+
112
+ it('should add correct padding', () => {
113
+ // Test different padding scenarios
114
+ const testCases = [
115
+ { input: 'abc', expectedPadding: 1 },
116
+ { input: 'abcd', expectedPadding: 0 },
117
+ { input: 'abcde', expectedPadding: 3 },
118
+ { input: 'abcdef', expectedPadding: 2 },
119
+ ];
120
+
121
+ testCases.forEach(({ input, expectedPadding }) => {
122
+ const result = fromUrlSafeBase64(input);
123
+ const paddingCount = (result.match(/=/g) || []).length;
124
+ assert.strictEqual(paddingCount, expectedPadding);
125
+ });
126
+ });
127
+
128
+ it('should handle strings without special characters', () => {
129
+ const input = 'abcdefghijklmnop';
130
+ const result = fromUrlSafeBase64(input);
131
+ assert.ok(result);
132
+ });
133
+ });
134
+
135
+ describe('toUrlSafeBase64', () => {
136
+ it('should convert standard base64 to URL-safe', () => {
137
+ const standard = 'uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=';
138
+ const urlSafe = toUrlSafeBase64(standard);
139
+
140
+ // Should not contain standard base64 special characters
141
+ assert.ok(!urlSafe.includes('+'));
142
+ assert.ok(!urlSafe.includes('/'));
143
+ assert.ok(!urlSafe.includes('='));
144
+
145
+ // Should contain URL-safe replacements
146
+ assert.ok(urlSafe.includes('-'));
147
+ });
148
+
149
+ it('should remove padding', () => {
150
+ const withPadding = 'abc=';
151
+ const result = toUrlSafeBase64(withPadding);
152
+ assert.ok(!result.includes('='));
153
+ assert.strictEqual(result, 'abc');
154
+ });
155
+
156
+ it('should be reversible with fromUrlSafeBase64', () => {
157
+ // Start with a standard base64 string with special chars
158
+ const original = 'test+data/with==';
159
+ const urlSafe = toUrlSafeBase64(original);
160
+ const restored = fromUrlSafeBase64(urlSafe);
161
+
162
+ // Should restore to equivalent base64 (padding might differ slightly)
163
+ assert.strictEqual(restored.replace(/=+$/, ''), original.replace(/=+$/, ''));
164
+ });
165
+ });
166
+
167
+ describe('Round-trip conversions', () => {
168
+ it('should maintain hash integrity through URL-safe conversion', () => {
169
+ const content = 'test content for hashing';
170
+ const hash = sha256(content);
171
+
172
+ // Convert to standard base64 and back
173
+ const standard = fromUrlSafeBase64(hash);
174
+ const backToUrlSafe = toUrlSafeBase64(standard);
175
+
176
+ assert.strictEqual(backToUrlSafe, hash);
177
+ });
178
+
179
+ it('should verify hash after conversion', () => {
180
+ const content = 'verification test';
181
+ const hash = sha256(content);
182
+
183
+ // Convert and back
184
+ const standard = fromUrlSafeBase64(hash);
185
+ const urlSafe = toUrlSafeBase64(standard);
186
+
187
+ // Should still verify
188
+ const isValid = verify(content, urlSafe);
189
+ assert.strictEqual(isValid, true);
190
+ });
191
+ });
192
+
193
+ describe('Edge cases', () => {
194
+ it('should handle very long strings', () => {
195
+ const longString = 'a'.repeat(10000);
196
+ const hash = sha256(longString);
197
+ assert.ok(hash);
198
+ assert.strictEqual(hash.length, 43);
199
+ });
200
+
201
+ it('should handle special characters', () => {
202
+ const special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
203
+ const hash = sha256(special);
204
+ assert.ok(hash);
205
+
206
+ const isValid = verify(special, hash);
207
+ assert.strictEqual(isValid, true);
208
+ });
209
+
210
+ it('should handle newlines and whitespace', () => {
211
+ const withNewlines = 'line1\nline2\r\nline3\ttab';
212
+ const hash = sha256(withNewlines);
213
+ assert.ok(hash);
214
+
215
+ const isValid = verify(withNewlines, hash);
216
+ assert.strictEqual(isValid, true);
217
+ });
218
+ });
219
+ });