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.
- package/README.md +409 -0
- package/dist/constants.d.ts +225 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +227 -0
- package/dist/constants.js.map +1 -0
- package/dist/esm/constants.d.ts +225 -0
- package/dist/esm/constants.d.ts.map +1 -0
- package/dist/esm/hash.d.ts +37 -0
- package/dist/esm/hash.d.ts.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +2104 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/protocol.d.ts +30 -0
- package/dist/esm/protocol.d.ts.map +1 -0
- package/dist/esm/signature.d.ts +49 -0
- package/dist/esm/signature.d.ts.map +1 -0
- package/dist/esm/types.d.ts +115 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/utils.d.ts +14 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/hash.d.ts +37 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +99 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +30 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +677 -0
- package/dist/protocol.js.map +1 -0
- package/dist/signature.d.ts +49 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +169 -0
- package/dist/signature.js.map +1 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +96 -0
- package/dist/utils.js.map +1 -0
- package/package.json +66 -0
- package/src/constants.ts +245 -0
- package/src/fixtures.test.ts +236 -0
- package/src/hash.test.ts +219 -0
- package/src/hash.ts +99 -0
- package/src/index.ts +5 -0
- package/src/organisation-verification.test.ts +50 -0
- package/src/person-verification.test.ts +55 -0
- package/src/poll.test.ts +28 -0
- package/src/protocol.ts +871 -0
- package/src/rating.test.ts +25 -0
- package/src/signature.test.ts +200 -0
- package/src/signature.ts +159 -0
- package/src/statement.test.ts +101 -0
- package/src/types.ts +185 -0
- package/src/utils.test.ts +140 -0
- package/src/utils.ts +104 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { parseRating, buildRating } from './protocol';
|
|
4
|
+
|
|
5
|
+
const randomUnicodeString = () =>
|
|
6
|
+
Array.from({ length: 20 }, () => String.fromCharCode(Math.floor(Math.random() * 65536)))
|
|
7
|
+
.join('')
|
|
8
|
+
.replace(/[\n;>=<"''\\]/g, '');
|
|
9
|
+
|
|
10
|
+
describe('Rating building', () => {
|
|
11
|
+
it('build & parse function compatibility: input=parse(build(input))', () => {
|
|
12
|
+
const [subjectName, subjectReference, comment, quality] = Array.from(
|
|
13
|
+
{ length: 4 },
|
|
14
|
+
randomUnicodeString
|
|
15
|
+
);
|
|
16
|
+
const rating = Math.ceil(Math.random() * 5);
|
|
17
|
+
const ratingContent = buildRating({ subjectName, subjectReference, rating, comment, quality });
|
|
18
|
+
const parsedRating = parseRating(ratingContent);
|
|
19
|
+
assert.strictEqual(parsedRating.subjectName, subjectName);
|
|
20
|
+
assert.strictEqual(parsedRating.subjectReference, subjectReference);
|
|
21
|
+
assert.strictEqual(parsedRating.quality, quality);
|
|
22
|
+
assert.strictEqual(parsedRating.rating, rating);
|
|
23
|
+
assert.strictEqual(parsedRating.comment, comment);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
generateKeyPair,
|
|
5
|
+
signStatement,
|
|
6
|
+
verifySignature,
|
|
7
|
+
buildSignedStatement,
|
|
8
|
+
parseSignedStatement,
|
|
9
|
+
verifySignedStatement,
|
|
10
|
+
} from './signature';
|
|
11
|
+
import { buildStatement, parseStatement } from './protocol';
|
|
12
|
+
|
|
13
|
+
describe('Signature Functions', () => {
|
|
14
|
+
let publicKey: string;
|
|
15
|
+
let privateKey: string;
|
|
16
|
+
const testStatement = `Stated protocol version: 5
|
|
17
|
+
Publishing domain: example.com
|
|
18
|
+
Author: Test Author
|
|
19
|
+
Time: Thu, 15 Jun 2023 20:01:26 GMT
|
|
20
|
+
Statement content:
|
|
21
|
+
This is a test statement
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
before(async () => {
|
|
25
|
+
const keys = await generateKeyPair();
|
|
26
|
+
publicKey = keys.publicKey;
|
|
27
|
+
privateKey = keys.privateKey;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('generateKeyPair', () => {
|
|
31
|
+
it('should generate a valid Ed25519 key pair in URL-safe base64', async () => {
|
|
32
|
+
const keys = await generateKeyPair();
|
|
33
|
+
assert.ok(keys.publicKey);
|
|
34
|
+
assert.ok(keys.privateKey);
|
|
35
|
+
assert.ok(/^[A-Za-z0-9_-]+$/.test(keys.publicKey));
|
|
36
|
+
assert.ok(/^[A-Za-z0-9_-]+$/.test(keys.privateKey));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('signStatement', () => {
|
|
41
|
+
it('should sign a statement and return URL-safe base64 signature', async () => {
|
|
42
|
+
const signature = await signStatement(testStatement, privateKey);
|
|
43
|
+
assert.ok(signature);
|
|
44
|
+
assert.strictEqual(typeof signature, 'string');
|
|
45
|
+
assert.ok(/^[A-Za-z0-9_-]+$/.test(signature));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('verifySignature', () => {
|
|
50
|
+
it('should verify a valid signature', async () => {
|
|
51
|
+
const signature = await signStatement(testStatement, privateKey);
|
|
52
|
+
const isValid = await verifySignature(testStatement, signature, publicKey);
|
|
53
|
+
assert.strictEqual(isValid, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should reject an invalid signature', async () => {
|
|
57
|
+
const signature = await signStatement(testStatement, privateKey);
|
|
58
|
+
const tamperedStatement = testStatement.replace('Test Author', 'Hacker');
|
|
59
|
+
const isValid = await verifySignature(tamperedStatement, signature, publicKey);
|
|
60
|
+
assert.strictEqual(isValid, false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should reject a signature with wrong public key', async () => {
|
|
64
|
+
const signature = await signStatement(testStatement, privateKey);
|
|
65
|
+
const { publicKey: wrongPublicKey } = await generateKeyPair();
|
|
66
|
+
const isValid = await verifySignature(testStatement, signature, wrongPublicKey);
|
|
67
|
+
assert.strictEqual(isValid, false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('buildSignedStatement', () => {
|
|
72
|
+
it('should build a properly formatted signed statement', async () => {
|
|
73
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
74
|
+
assert.ok(signedStatement.includes(testStatement));
|
|
75
|
+
assert.ok(signedStatement.includes('---'));
|
|
76
|
+
assert.ok(signedStatement.includes('Statement hash:'));
|
|
77
|
+
assert.ok(signedStatement.includes('Public key:'));
|
|
78
|
+
assert.ok(signedStatement.includes('Signature:'));
|
|
79
|
+
assert.ok(signedStatement.includes('Algorithm: Ed25519'));
|
|
80
|
+
assert.ok(signedStatement.includes(publicKey));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('parseSignedStatement', () => {
|
|
85
|
+
it('should parse a valid signed statement', async () => {
|
|
86
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
87
|
+
const parsed = parseSignedStatement(signedStatement);
|
|
88
|
+
|
|
89
|
+
assert.ok(parsed !== null);
|
|
90
|
+
assert.strictEqual(parsed?.statement, testStatement);
|
|
91
|
+
assert.ok(parsed?.statementHash);
|
|
92
|
+
assert.strictEqual(parsed?.publicKey, publicKey);
|
|
93
|
+
assert.ok(parsed?.signature);
|
|
94
|
+
assert.strictEqual(parsed?.algorithm, 'Ed25519');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return null for invalid format', () => {
|
|
98
|
+
const invalidStatement = 'This is not a signed statement';
|
|
99
|
+
const parsed = parseSignedStatement(invalidStatement);
|
|
100
|
+
assert.strictEqual(parsed, null);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should reject statement with tampered hash', async () => {
|
|
104
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
105
|
+
const tamperedStatement = signedStatement.replace(
|
|
106
|
+
/Statement hash: [A-Za-z0-9_-]+/,
|
|
107
|
+
'Statement hash: invalid_hash_here'
|
|
108
|
+
);
|
|
109
|
+
const parsed = parseSignedStatement(tamperedStatement);
|
|
110
|
+
assert.strictEqual(parsed, null);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reject statement with unsupported algorithm', async () => {
|
|
114
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
115
|
+
const tamperedStatement = signedStatement.replace('Algorithm: Ed25519', 'Algorithm: RSA');
|
|
116
|
+
const parsed = parseSignedStatement(tamperedStatement);
|
|
117
|
+
assert.strictEqual(parsed, null);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('verifySignedStatement', () => {
|
|
122
|
+
it('should verify a valid signed statement', async () => {
|
|
123
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
124
|
+
const isValid = await verifySignedStatement(signedStatement);
|
|
125
|
+
assert.strictEqual(isValid, true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should reject a tampered signed statement', async () => {
|
|
129
|
+
const signedStatement = await buildSignedStatement(testStatement, privateKey, publicKey);
|
|
130
|
+
const tamperedStatement = signedStatement.replace('Test Author', 'Hacker');
|
|
131
|
+
const isValid = await verifySignedStatement(tamperedStatement);
|
|
132
|
+
assert.strictEqual(isValid, false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Integration with buildStatement and parseStatement', () => {
|
|
137
|
+
it('should work with statements built using buildStatement', async () => {
|
|
138
|
+
const statement = buildStatement({
|
|
139
|
+
domain: 'example.com',
|
|
140
|
+
author: 'Test Author',
|
|
141
|
+
time: new Date('2023-06-15T20:01:26Z'),
|
|
142
|
+
content: 'This is a test statement',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const signedStatement = await buildSignedStatement(statement, privateKey, publicKey);
|
|
146
|
+
const isValid = await verifySignedStatement(signedStatement);
|
|
147
|
+
assert.strictEqual(isValid, true);
|
|
148
|
+
|
|
149
|
+
const parsed = parseSignedStatement(signedStatement);
|
|
150
|
+
assert.strictEqual(parsed?.statement, statement);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should automatically verify signature when parsing with parseStatement', async () => {
|
|
154
|
+
const statement = buildStatement({
|
|
155
|
+
domain: 'example.com',
|
|
156
|
+
author: 'Test Author',
|
|
157
|
+
time: new Date('2023-06-15T20:01:26Z'),
|
|
158
|
+
content: 'This is a test statement',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const signedStatement = await buildSignedStatement(statement, privateKey, publicKey);
|
|
162
|
+
|
|
163
|
+
const parsed = parseStatement({ statement: signedStatement });
|
|
164
|
+
assert.strictEqual(parsed.domain, 'example.com');
|
|
165
|
+
assert.strictEqual(parsed.author, 'Test Author');
|
|
166
|
+
assert.strictEqual(parsed.content, 'This is a test statement');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should reject tampered signed statement in parseStatement', async () => {
|
|
170
|
+
const statement = buildStatement({
|
|
171
|
+
domain: 'example.com',
|
|
172
|
+
author: 'Test Author',
|
|
173
|
+
time: new Date('2023-06-15T20:01:26Z'),
|
|
174
|
+
content: 'This is a test statement',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const signedStatement = await buildSignedStatement(statement, privateKey, publicKey);
|
|
178
|
+
const tamperedStatement = signedStatement.replace('Test Author', 'Hacker');
|
|
179
|
+
|
|
180
|
+
assert.throws(() => {
|
|
181
|
+
parseStatement({ statement: tamperedStatement });
|
|
182
|
+
}, /Statement hash mismatch|Invalid cryptographic signature/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should still parse unsigned statements (backward compatibility)', () => {
|
|
186
|
+
const statement = buildStatement({
|
|
187
|
+
domain: 'example.com',
|
|
188
|
+
author: 'Test Author',
|
|
189
|
+
time: new Date('2023-06-15T20:01:26Z'),
|
|
190
|
+
content: 'This is a test statement',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Should parse successfully without signature
|
|
194
|
+
const parsed = parseStatement({ statement });
|
|
195
|
+
assert.strictEqual(parsed.domain, 'example.com');
|
|
196
|
+
assert.strictEqual(parsed.author, 'Test Author');
|
|
197
|
+
assert.strictEqual(parsed.content, 'This is a test statement');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/signature.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal signature utilities using @noble/ed25519
|
|
3
|
+
* Works in both browser and Node.js environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as ed from '@noble/ed25519';
|
|
7
|
+
import { sha512 } from '@noble/hashes/sha2.js';
|
|
8
|
+
import { toUrlSafeBase64, fromUrlSafeBase64, sha256, base64ToBytes } from './hash';
|
|
9
|
+
import type { CryptographicallySignedStatement } from './types';
|
|
10
|
+
|
|
11
|
+
// Set up sha512 for @noble/ed25519
|
|
12
|
+
ed.hashes.sha512 = (message: Uint8Array) => sha512(message);
|
|
13
|
+
|
|
14
|
+
const ALGORITHM = 'Ed25519'; // Fully specifies: EdDSA signature scheme with Curve25519
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a new Ed25519 key pair for signing statements
|
|
18
|
+
* @returns Object containing publicKey and privateKey as URL-safe base64
|
|
19
|
+
*/
|
|
20
|
+
export const generateKeyPair = async (): Promise<{ publicKey: string; privateKey: string }> => {
|
|
21
|
+
const privateKey = ed.utils.randomSecretKey();
|
|
22
|
+
const publicKey = await ed.getPublicKey(privateKey);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
publicKey: toUrlSafeBase64(bytesToBase64(publicKey)),
|
|
26
|
+
privateKey: toUrlSafeBase64(bytesToBase64(privateKey)),
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sign a statement with a private key
|
|
32
|
+
* @param statement - The statement text to sign
|
|
33
|
+
* @param privateKeyUrlSafe - Private key in URL-safe base64 format
|
|
34
|
+
* @returns URL-safe base64-encoded signature
|
|
35
|
+
*/
|
|
36
|
+
export const signStatement = async (
|
|
37
|
+
statement: string,
|
|
38
|
+
privateKeyUrlSafe: string
|
|
39
|
+
): Promise<string> => {
|
|
40
|
+
const privateKeyBytes = base64ToBytes(fromUrlSafeBase64(privateKeyUrlSafe));
|
|
41
|
+
const messageBytes = new TextEncoder().encode(statement);
|
|
42
|
+
|
|
43
|
+
const signature = await ed.sign(messageBytes, privateKeyBytes);
|
|
44
|
+
|
|
45
|
+
return toUrlSafeBase64(bytesToBase64(signature));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verify a statement signature
|
|
50
|
+
* @param statement - The statement text that was signed
|
|
51
|
+
* @param signatureUrlSafe - URL-safe base64-encoded signature
|
|
52
|
+
* @param publicKeyUrlSafe - Public key in URL-safe base64 format
|
|
53
|
+
* @returns true if signature is valid, false otherwise
|
|
54
|
+
*/
|
|
55
|
+
export const verifySignature = async (
|
|
56
|
+
statement: string,
|
|
57
|
+
signatureUrlSafe: string,
|
|
58
|
+
publicKeyUrlSafe: string
|
|
59
|
+
): Promise<boolean> => {
|
|
60
|
+
try {
|
|
61
|
+
const publicKeyBytes = base64ToBytes(fromUrlSafeBase64(publicKeyUrlSafe));
|
|
62
|
+
const signatureBytes = base64ToBytes(fromUrlSafeBase64(signatureUrlSafe));
|
|
63
|
+
const messageBytes = new TextEncoder().encode(statement);
|
|
64
|
+
|
|
65
|
+
return await ed.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a signed statement
|
|
73
|
+
* @param statement - The statement text to sign
|
|
74
|
+
* @param privateKeyUrlSafe - Private key in URL-safe base64 format
|
|
75
|
+
* @param publicKeyUrlSafe - Public key in URL-safe base64 format
|
|
76
|
+
* @returns Signed statement with appended signature fields
|
|
77
|
+
*/
|
|
78
|
+
export const buildSignedStatement = async (
|
|
79
|
+
statement: string,
|
|
80
|
+
privateKeyUrlSafe: string,
|
|
81
|
+
publicKeyUrlSafe: string
|
|
82
|
+
): Promise<string> => {
|
|
83
|
+
const statementHash = sha256(statement);
|
|
84
|
+
const signature = await signStatement(statement, privateKeyUrlSafe);
|
|
85
|
+
return (
|
|
86
|
+
statement +
|
|
87
|
+
`---\n` +
|
|
88
|
+
`Statement hash: ${statementHash}\n` +
|
|
89
|
+
`Public key: ${publicKeyUrlSafe}\n` +
|
|
90
|
+
`Signature: ${signature}\n` +
|
|
91
|
+
`Algorithm: ${ALGORITHM}\n`
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a signed statement
|
|
97
|
+
* @param signedStatement - The signed statement text
|
|
98
|
+
* @returns Parsed CryptographicallySignedStatement object or null if invalid
|
|
99
|
+
*/
|
|
100
|
+
export const parseSignedStatement = (
|
|
101
|
+
signedStatement: string
|
|
102
|
+
): CryptographicallySignedStatement | null => {
|
|
103
|
+
const regex =
|
|
104
|
+
/^([\s\S]+?)---\nStatement hash: ([A-Za-z0-9_-]+)\nPublic key: ([A-Za-z0-9_-]+)\nSignature: ([A-Za-z0-9_-]+)\nAlgorithm: ([^\n]+)\n$/;
|
|
105
|
+
const match = signedStatement.match(regex);
|
|
106
|
+
|
|
107
|
+
if (!match) return null;
|
|
108
|
+
|
|
109
|
+
const statement = match[1];
|
|
110
|
+
const statementHash = match[2];
|
|
111
|
+
const publicKey = match[3];
|
|
112
|
+
const signature = match[4];
|
|
113
|
+
const algorithm = match[5];
|
|
114
|
+
|
|
115
|
+
// Verify statement hash matches
|
|
116
|
+
const computedHash = sha256(statement);
|
|
117
|
+
if (computedHash !== statementHash) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Verify algorithm is supported
|
|
122
|
+
if (algorithm !== ALGORITHM) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
statement,
|
|
128
|
+
publicKey,
|
|
129
|
+
signature,
|
|
130
|
+
statementHash,
|
|
131
|
+
algorithm,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Verify a signed statement
|
|
137
|
+
* @param signedStatement - The signed statement text
|
|
138
|
+
* @returns true if signature is valid, false otherwise
|
|
139
|
+
*/
|
|
140
|
+
export const verifySignedStatement = async (signedStatement: string): Promise<boolean> => {
|
|
141
|
+
const parsed = parseSignedStatement(signedStatement);
|
|
142
|
+
if (!parsed) return false;
|
|
143
|
+
|
|
144
|
+
return await verifySignature(parsed.statement, parsed.signature, parsed.publicKey);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert bytes to base64 string
|
|
149
|
+
* @param bytes - Uint8Array to convert
|
|
150
|
+
* @returns Base64 string
|
|
151
|
+
*/
|
|
152
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
153
|
+
// Use btoa if available (browser), otherwise use Buffer (Node.js)
|
|
154
|
+
if (typeof btoa !== 'undefined') {
|
|
155
|
+
return btoa(String.fromCharCode(...Array.from(bytes)));
|
|
156
|
+
} else {
|
|
157
|
+
return Buffer.from(bytes).toString('base64');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { parseStatement, buildStatement } from './protocol';
|
|
4
|
+
|
|
5
|
+
const randomUnicodeString = () =>
|
|
6
|
+
Array.from({ length: 20 }, () => String.fromCharCode(Math.floor(Math.random() * 65536)))
|
|
7
|
+
.join('')
|
|
8
|
+
.replace(/[\n;>=<"''\\]/g, '');
|
|
9
|
+
|
|
10
|
+
describe('Statement building', () => {
|
|
11
|
+
it('build & parse function compatibility: input=parse(build(input))', () => {
|
|
12
|
+
const [domain, author, representative, content, supersededStatement] = Array.from(
|
|
13
|
+
{ length: 5 },
|
|
14
|
+
randomUnicodeString
|
|
15
|
+
);
|
|
16
|
+
const tags = Array.from({ length: 4 }, randomUnicodeString);
|
|
17
|
+
const contentWithTrailingNewline = content + (content.match(/\n$/) ? '' : '\n');
|
|
18
|
+
const time = new Date('Sun, 04 Sep 2022 14:48:50 GMT');
|
|
19
|
+
const statementContent = buildStatement({
|
|
20
|
+
domain,
|
|
21
|
+
author,
|
|
22
|
+
time,
|
|
23
|
+
content: contentWithTrailingNewline,
|
|
24
|
+
representative,
|
|
25
|
+
supersededStatement,
|
|
26
|
+
tags,
|
|
27
|
+
});
|
|
28
|
+
const parsedStatement = parseStatement({ statement: statementContent });
|
|
29
|
+
assert.strictEqual(parsedStatement.domain, domain);
|
|
30
|
+
assert.strictEqual(parsedStatement.author, author);
|
|
31
|
+
assert.strictEqual(parsedStatement.time?.toUTCString(), time.toUTCString());
|
|
32
|
+
assert.strictEqual(parsedStatement.content, content);
|
|
33
|
+
assert.strictEqual(parsedStatement.representative, representative);
|
|
34
|
+
assert.strictEqual(parsedStatement.supersededStatement, supersededStatement);
|
|
35
|
+
assert.deepStrictEqual(parsedStatement.tags?.sort(), tags.sort());
|
|
36
|
+
});
|
|
37
|
+
it('build & parse statement with attachments', () => {
|
|
38
|
+
const domain = 'example.com';
|
|
39
|
+
const author = 'Test Author';
|
|
40
|
+
const time = new Date('Thu, 15 Jun 2023 20:01:26 GMT');
|
|
41
|
+
const content = 'Statement with attachments';
|
|
42
|
+
const attachments = ['abc123_-XYZ.pdf', 'def456-_ABC.jpg', 'xyz789_hash.docx'];
|
|
43
|
+
|
|
44
|
+
const statementContent = buildStatement({
|
|
45
|
+
domain,
|
|
46
|
+
author,
|
|
47
|
+
time,
|
|
48
|
+
content,
|
|
49
|
+
attachments,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const parsedStatement = parseStatement({ statement: statementContent });
|
|
53
|
+
assert.strictEqual(parsedStatement.domain, domain);
|
|
54
|
+
assert.strictEqual(parsedStatement.author, author);
|
|
55
|
+
assert.strictEqual(parsedStatement.content, content);
|
|
56
|
+
assert.deepStrictEqual(parsedStatement.attachments, attachments);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('reject statement with more than 5 attachments', () => {
|
|
60
|
+
const domain = 'example.com';
|
|
61
|
+
const author = 'Test Author';
|
|
62
|
+
const time = new Date();
|
|
63
|
+
const content = 'Too many attachments\n';
|
|
64
|
+
const attachments = [
|
|
65
|
+
'hash1.pdf',
|
|
66
|
+
'hash2.pdf',
|
|
67
|
+
'hash3.pdf',
|
|
68
|
+
'hash4.pdf',
|
|
69
|
+
'hash5.pdf',
|
|
70
|
+
'hash6.pdf',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
assert.throws(() => {
|
|
74
|
+
buildStatement({
|
|
75
|
+
domain,
|
|
76
|
+
author,
|
|
77
|
+
time,
|
|
78
|
+
content,
|
|
79
|
+
attachments,
|
|
80
|
+
});
|
|
81
|
+
}, /Maximum 5 attachments allowed/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reject attachment with invalid format', () => {
|
|
85
|
+
const domain = 'example.com';
|
|
86
|
+
const author = 'Test Author';
|
|
87
|
+
const time = new Date();
|
|
88
|
+
const content = 'Invalid attachment\n';
|
|
89
|
+
const attachments = ['invalid file name.pdf'];
|
|
90
|
+
|
|
91
|
+
assert.throws(() => {
|
|
92
|
+
buildStatement({
|
|
93
|
+
domain,
|
|
94
|
+
author,
|
|
95
|
+
time,
|
|
96
|
+
content,
|
|
97
|
+
attachments,
|
|
98
|
+
});
|
|
99
|
+
}, /Attachment 1 must be in format 'base64hash.extension' \(URL-safe base64\)/);
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { SupportedLanguage } from './constants';
|
|
2
|
+
|
|
3
|
+
export type LegalForm =
|
|
4
|
+
| 'local government'
|
|
5
|
+
| 'state government'
|
|
6
|
+
| 'foreign affairs ministry'
|
|
7
|
+
| 'corporation';
|
|
8
|
+
|
|
9
|
+
export type PeopleCountBucket =
|
|
10
|
+
| '0-10'
|
|
11
|
+
| '10-100'
|
|
12
|
+
| '100-1000'
|
|
13
|
+
| '1000-10,000'
|
|
14
|
+
| '10,000-100,000'
|
|
15
|
+
| '100,000+'
|
|
16
|
+
| '1,000,000+'
|
|
17
|
+
| '10,000,000+';
|
|
18
|
+
|
|
19
|
+
// Type guards
|
|
20
|
+
export function isLegalForm(value: string): value is LegalForm {
|
|
21
|
+
return [
|
|
22
|
+
'local government',
|
|
23
|
+
'state government',
|
|
24
|
+
'foreign affairs ministry',
|
|
25
|
+
'corporation',
|
|
26
|
+
].includes(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isPeopleCountBucket(value: string): value is PeopleCountBucket {
|
|
30
|
+
return [
|
|
31
|
+
'0-10',
|
|
32
|
+
'10-100',
|
|
33
|
+
'100-1000',
|
|
34
|
+
'1000-10,000',
|
|
35
|
+
'10,000-100,000',
|
|
36
|
+
'100,000+',
|
|
37
|
+
'1,000,000+',
|
|
38
|
+
'10,000,000+',
|
|
39
|
+
].includes(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isRatingValue(value: number): value is RatingValue {
|
|
43
|
+
return [1, 2, 3, 4, 5].includes(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type StatementTypeValue =
|
|
47
|
+
| 'statement'
|
|
48
|
+
| 'organisation_verification'
|
|
49
|
+
| 'person_verification'
|
|
50
|
+
| 'poll'
|
|
51
|
+
| 'vote'
|
|
52
|
+
| 'response'
|
|
53
|
+
| 'dispute_statement_content'
|
|
54
|
+
| 'dispute_statement_authenticity'
|
|
55
|
+
| 'rating'
|
|
56
|
+
| 'sign_pdf';
|
|
57
|
+
|
|
58
|
+
export type Statement = {
|
|
59
|
+
domain: string;
|
|
60
|
+
author: string;
|
|
61
|
+
time: Date;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
content: string;
|
|
64
|
+
representative?: string;
|
|
65
|
+
supersededStatement?: string;
|
|
66
|
+
formatVersion?: string;
|
|
67
|
+
translations?: Partial<Record<SupportedLanguage, string>>;
|
|
68
|
+
attachments?: string[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type CryptographicallySignedStatement = {
|
|
72
|
+
statement: string;
|
|
73
|
+
statementHash: string;
|
|
74
|
+
publicKey: string;
|
|
75
|
+
signature: string;
|
|
76
|
+
algorithm: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type Poll = {
|
|
80
|
+
deadline: Date | undefined;
|
|
81
|
+
poll: string;
|
|
82
|
+
scopeDescription?: string;
|
|
83
|
+
options: string[];
|
|
84
|
+
allowArbitraryVote?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type OrganisationVerification = {
|
|
88
|
+
name: string;
|
|
89
|
+
englishName?: string;
|
|
90
|
+
country: string;
|
|
91
|
+
city?: string;
|
|
92
|
+
province?: string;
|
|
93
|
+
legalForm: LegalForm;
|
|
94
|
+
department?: string;
|
|
95
|
+
domain: string;
|
|
96
|
+
foreignDomain?: string;
|
|
97
|
+
serialNumber?: string;
|
|
98
|
+
confidence?: number;
|
|
99
|
+
reliabilityPolicy?: string;
|
|
100
|
+
employeeCount?: PeopleCountBucket;
|
|
101
|
+
pictureHash?: string;
|
|
102
|
+
latitude?: number;
|
|
103
|
+
longitude?: number;
|
|
104
|
+
population?: PeopleCountBucket;
|
|
105
|
+
publicKey?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type withOwnDomain = {
|
|
109
|
+
ownDomain: string;
|
|
110
|
+
foreignDomain?: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type withForeignDomain = {
|
|
114
|
+
foreignDomain: string;
|
|
115
|
+
ownDomain?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type PersonVerification = {
|
|
119
|
+
name: string;
|
|
120
|
+
countryOfBirth: string;
|
|
121
|
+
cityOfBirth: string;
|
|
122
|
+
dateOfBirth: Date;
|
|
123
|
+
jobTitle?: string;
|
|
124
|
+
employer?: string;
|
|
125
|
+
verificationMethod?: string;
|
|
126
|
+
confidence?: number;
|
|
127
|
+
picture?: string;
|
|
128
|
+
reliabilityPolicy?: string;
|
|
129
|
+
publicKey?: string;
|
|
130
|
+
} & (withOwnDomain | withForeignDomain);
|
|
131
|
+
|
|
132
|
+
export type Vote = {
|
|
133
|
+
pollHash: string;
|
|
134
|
+
poll: string;
|
|
135
|
+
vote: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type DisputeAuthenticity = {
|
|
139
|
+
hash: string;
|
|
140
|
+
confidence?: number;
|
|
141
|
+
reliabilityPolicy?: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type DisputeContent = {
|
|
145
|
+
hash: string;
|
|
146
|
+
confidence?: number;
|
|
147
|
+
reliabilityPolicy?: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type ResponseContent = {
|
|
151
|
+
hash: string;
|
|
152
|
+
response: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type PDFSigning = {
|
|
156
|
+
hash: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export type RatingSubjectTypeValue =
|
|
160
|
+
| 'Organisation'
|
|
161
|
+
| 'Policy proposal'
|
|
162
|
+
| 'Treaty draft'
|
|
163
|
+
| 'Research publication'
|
|
164
|
+
| 'Regulation'
|
|
165
|
+
| 'Product';
|
|
166
|
+
|
|
167
|
+
export type RatingValue = 1 | 2 | 3 | 4 | 5;
|
|
168
|
+
|
|
169
|
+
export type Rating = {
|
|
170
|
+
subjectType?: RatingSubjectTypeValue;
|
|
171
|
+
subjectName: string;
|
|
172
|
+
subjectReference?: string;
|
|
173
|
+
documentFileHash?: string;
|
|
174
|
+
rating: RatingValue;
|
|
175
|
+
quality?: string;
|
|
176
|
+
comment?: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export type Bounty = {
|
|
180
|
+
motivation?: string;
|
|
181
|
+
bounty: string;
|
|
182
|
+
reward: string;
|
|
183
|
+
judge: string;
|
|
184
|
+
judgePay?: string;
|
|
185
|
+
};
|