@sd-jwt/core 0.1.0 → 0.1.1-next.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/CHANGELOG.md +17 -0
- package/LICENSE +201 -0
- package/README.md +106 -0
- package/dist/index.d.mts +659 -0
- package/dist/index.d.ts +659 -0
- package/dist/index.js +1906 -0
- package/dist/index.mjs +1846 -0
- package/package.json +59 -47
- package/src/decode/decode.ts +376 -0
- package/src/decode/index.ts +1 -0
- package/src/decoy.ts +15 -0
- package/src/flattenJSON.ts +99 -0
- package/src/generalJSON.ts +148 -0
- package/src/index.ts +945 -0
- package/src/jwt.ts +190 -0
- package/src/kbjwt.ts +81 -0
- package/src/present/index.ts +1 -0
- package/src/present/present.ts +210 -0
- package/src/sdjwt.ts +354 -0
- package/src/test/decode/decode.spec.ts +202 -0
- package/src/test/decoy.spec.ts +30 -0
- package/src/test/flattenJSON.spec.ts +122 -0
- package/src/test/generalJSON.spec.ts +198 -0
- package/src/test/index.spec.ts +974 -0
- package/src/test/jwt.spec.ts +301 -0
- package/src/test/kbjwt.spec.ts +565 -0
- package/src/test/pass.spec.ts +7 -0
- package/src/test/present/present.spec.ts +305 -0
- package/src/test/sdjwt.spec.ts +384 -0
- package/src/test/types/type.spec.ts +88 -0
- package/src/test/utils/base64url.spec.ts +33 -0
- package/src/test/utils/disclosure.spec.ts +170 -0
- package/src/test/utils/error.spec.ts +15 -0
- package/src/types/index.ts +2 -0
- package/src/types/type.ts +249 -0
- package/src/types/verification-error.ts +55 -0
- package/src/utils/base64url.ts +6 -0
- package/src/utils/disclosure.ts +101 -0
- package/src/utils/error.ts +25 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/strict-json.ts +17 -0
- package/test/app-e2e.spec.ts +292 -0
- package/test/array_data_types.json +31 -0
- package/test/array_full_sd.json +21 -0
- package/test/array_in_sd.json +13 -0
- package/test/array_nested_in_plain.json +29 -0
- package/test/array_none_disclosed.json +17 -0
- package/test/array_of_nulls.json +15 -0
- package/test/array_of_objects.json +65 -0
- package/test/array_of_scalars.json +19 -0
- package/test/array_recursive_sd.json +35 -0
- package/test/array_recursive_sd_some_disclosed.json +63 -0
- package/test/complex.json +43 -0
- package/test/header_mod.json +48 -0
- package/test/json_serialization.json +48 -0
- package/test/key_binding.json +48 -0
- package/test/no_sd.json +36 -0
- package/test/object_data_types.json +62 -0
- package/test/recursions.json +117 -0
- package/test/rfc9901-validation.spec.ts +150 -0
- package/tsconfig.json +7 -0
- package/vitest.config.mts +4 -0
- package/build/base64url.d.ts +0 -28
- package/build/base64url.js +0 -40
- package/build/base64url.js.map +0 -1
- package/build/hasherAlgorithm.d.ts +0 -70
- package/build/hasherAlgorithm.js +0 -75
- package/build/hasherAlgorithm.js.map +0 -1
- package/build/index.d.ts +0 -11
- package/build/index.js +0 -20
- package/build/index.js.map +0 -1
- package/build/jwt/compact.d.ts +0 -6
- package/build/jwt/compact.js +0 -27
- package/build/jwt/compact.js.map +0 -1
- package/build/jwt/error.d.ts +0 -2
- package/build/jwt/error.js +0 -7
- package/build/jwt/error.js.map +0 -1
- package/build/jwt/index.d.ts +0 -3
- package/build/jwt/index.js +0 -20
- package/build/jwt/index.js.map +0 -1
- package/build/jwt/jwt.d.ts +0 -204
- package/build/jwt/jwt.js +0 -324
- package/build/jwt/jwt.js.map +0 -1
- package/build/keyBinding/index.d.ts +0 -1
- package/build/keyBinding/index.js +0 -18
- package/build/keyBinding/index.js.map +0 -1
- package/build/keyBinding/keyBinding.d.ts +0 -56
- package/build/keyBinding/keyBinding.js +0 -99
- package/build/keyBinding/keyBinding.js.map +0 -1
- package/build/sdJwt/compact.d.ts +0 -8
- package/build/sdJwt/compact.js +0 -39
- package/build/sdJwt/compact.js.map +0 -1
- package/build/sdJwt/decoys.d.ts +0 -2
- package/build/sdJwt/decoys.js +0 -35
- package/build/sdJwt/decoys.js.map +0 -1
- package/build/sdJwt/disclosureFrame.d.ts +0 -7
- package/build/sdJwt/disclosureFrame.js +0 -89
- package/build/sdJwt/disclosureFrame.js.map +0 -1
- package/build/sdJwt/disclosureMapping.d.ts +0 -44
- package/build/sdJwt/disclosureMapping.js +0 -289
- package/build/sdJwt/disclosureMapping.js.map +0 -1
- package/build/sdJwt/disclosures.d.ts +0 -14
- package/build/sdJwt/disclosures.js +0 -59
- package/build/sdJwt/disclosures.js.map +0 -1
- package/build/sdJwt/error.d.ts +0 -2
- package/build/sdJwt/error.js +0 -7
- package/build/sdJwt/error.js.map +0 -1
- package/build/sdJwt/index.d.ts +0 -7
- package/build/sdJwt/index.js +0 -24
- package/build/sdJwt/index.js.map +0 -1
- package/build/sdJwt/presentationFrame.d.ts +0 -4
- package/build/sdJwt/presentationFrame.js +0 -73
- package/build/sdJwt/presentationFrame.js.map +0 -1
- package/build/sdJwt/sdJwt.d.ts +0 -204
- package/build/sdJwt/sdJwt.js +0 -383
- package/build/sdJwt/sdJwt.js.map +0 -1
- package/build/sdJwt/swapClaim.d.ts +0 -3
- package/build/sdJwt/swapClaim.js +0 -90
- package/build/sdJwt/swapClaim.js.map +0 -1
- package/build/sdJwt/types.d.ts +0 -5
- package/build/sdJwt/types.js +0 -3
- package/build/sdJwt/types.js.map +0 -1
- package/build/sdJwtVc/error.d.ts +0 -2
- package/build/sdJwtVc/error.js +0 -7
- package/build/sdJwtVc/error.js.map +0 -1
- package/build/sdJwtVc/index.d.ts +0 -2
- package/build/sdJwtVc/index.js +0 -19
- package/build/sdJwtVc/index.js.map +0 -1
- package/build/sdJwtVc/sdJwtVc.d.ts +0 -47
- package/build/sdJwtVc/sdJwtVc.js +0 -125
- package/build/sdJwtVc/sdJwtVc.js.map +0 -1
- package/build/signatureAndEncryptionAlgorithm.d.ts +0 -105
- package/build/signatureAndEncryptionAlgorithm.js +0 -110
- package/build/signatureAndEncryptionAlgorithm.js.map +0 -1
- package/build/types/disclosure.d.ts +0 -5
- package/build/types/disclosure.js +0 -3
- package/build/types/disclosure.js.map +0 -1
- package/build/types/frame.d.ts +0 -5
- package/build/types/frame.js +0 -3
- package/build/types/frame.js.map +0 -1
- package/build/types/hasher.d.ts +0 -14
- package/build/types/hasher.js +0 -3
- package/build/types/hasher.js.map +0 -1
- package/build/types/index.d.ts +0 -6
- package/build/types/index.js +0 -23
- package/build/types/index.js.map +0 -1
- package/build/types/present.d.ts +0 -2
- package/build/types/present.js +0 -3
- package/build/types/present.js.map +0 -1
- package/build/types/saltGenerator.d.ts +0 -17
- package/build/types/saltGenerator.js +0 -3
- package/build/types/saltGenerator.js.map +0 -1
- package/build/types/signer.d.ts +0 -2
- package/build/types/signer.js +0 -3
- package/build/types/signer.js.map +0 -1
- package/build/types/utils.d.ts +0 -5
- package/build/types/utils.js +0 -3
- package/build/types/utils.js.map +0 -1
- package/build/types/verifier.d.ts +0 -14
- package/build/types/verifier.js +0 -3
- package/build/types/verifier.js.map +0 -1
- package/build/utils/index.d.ts +0 -2
- package/build/utils/index.js +0 -19
- package/build/utils/index.js.map +0 -1
- package/build/utils/traverse.d.ts +0 -8
- package/build/utils/traverse.js +0 -29
- package/build/utils/traverse.js.map +0 -1
- package/build/utils/utils.d.ts +0 -7
- package/build/utils/utils.js +0 -109
- package/build/utils/utils.js.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import Crypto from 'node:crypto';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { Jwt } from '../jwt';
|
|
4
|
+
import type { Signer, Verifier } from '../types';
|
|
5
|
+
import { base64urlEncode, SDJWTException } from '../utils';
|
|
6
|
+
|
|
7
|
+
describe('JWT', () => {
|
|
8
|
+
test('create', async () => {
|
|
9
|
+
const jwt = new Jwt({
|
|
10
|
+
header: { alg: 'EdDSA' },
|
|
11
|
+
payload: { foo: 'bar' },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(jwt.header).toEqual({ alg: 'EdDSA' });
|
|
15
|
+
expect(jwt.payload).toEqual({ foo: 'bar' });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('returns decoded JWT when correct JWT string is provided', () => {
|
|
19
|
+
const jwt = `${base64urlEncode(
|
|
20
|
+
JSON.stringify({ alg: 'HS256', typ: 'JWT' }),
|
|
21
|
+
)}.${base64urlEncode(
|
|
22
|
+
JSON.stringify({ sub: '1234567890', name: 'John Doe' }),
|
|
23
|
+
)}.signature`;
|
|
24
|
+
const result = Jwt.decodeJWT(jwt);
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
header: { alg: 'HS256', typ: 'JWT' },
|
|
27
|
+
payload: { sub: '1234567890', name: 'John Doe' },
|
|
28
|
+
signature: 'signature',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('throws an error when JWT string is not correctly formed', () => {
|
|
33
|
+
const jwt = 'abc.def';
|
|
34
|
+
expect(() => Jwt.decodeJWT(jwt)).toThrow('Invalid JWT as input');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('throws an error when JWT parts are missing', () => {
|
|
38
|
+
const jwt = `${base64urlEncode(
|
|
39
|
+
JSON.stringify({ alg: 'HS256', typ: 'JWT' }),
|
|
40
|
+
)}`;
|
|
41
|
+
expect(() => Jwt.decodeJWT(jwt)).toThrow('Invalid JWT as input');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('set', async () => {
|
|
45
|
+
const jwt = new Jwt();
|
|
46
|
+
jwt.setHeader({ alg: 'EdDSA' });
|
|
47
|
+
jwt.setPayload({ foo: 'bar' });
|
|
48
|
+
|
|
49
|
+
expect(jwt.header).toEqual({ alg: 'EdDSA' });
|
|
50
|
+
expect(jwt.payload).toEqual({ foo: 'bar' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('sign', async () => {
|
|
54
|
+
const { privateKey } = Crypto.generateKeyPairSync('ed25519');
|
|
55
|
+
const testSigner: Signer = async (data: string) => {
|
|
56
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
57
|
+
return Buffer.from(sig).toString('base64url');
|
|
58
|
+
};
|
|
59
|
+
const jwt = new Jwt({
|
|
60
|
+
header: { alg: 'EdDSA' },
|
|
61
|
+
payload: { foo: 'bar' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const encodedJwt = await jwt.sign(testSigner);
|
|
65
|
+
expect(typeof encodedJwt).toBe('string');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('verify', async () => {
|
|
69
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
70
|
+
const testSigner: Signer = async (data: string) => {
|
|
71
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
72
|
+
return Buffer.from(sig).toString('base64url');
|
|
73
|
+
};
|
|
74
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
75
|
+
return Crypto.verify(
|
|
76
|
+
null,
|
|
77
|
+
Buffer.from(data),
|
|
78
|
+
publicKey,
|
|
79
|
+
Buffer.from(sig, 'base64url'),
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const jwt = new Jwt({
|
|
84
|
+
header: { alg: 'EdDSA' },
|
|
85
|
+
payload: { foo: 'bar' },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const encodedJwt = await jwt.sign(testSigner);
|
|
89
|
+
const newJwt = Jwt.fromEncode(encodedJwt);
|
|
90
|
+
const verified = await newJwt.verify(testVerifier);
|
|
91
|
+
expect(verified).toStrictEqual({
|
|
92
|
+
header: { alg: 'EdDSA' },
|
|
93
|
+
payload: { foo: 'bar' },
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await newJwt.verify(() => false);
|
|
97
|
+
} catch (e: unknown) {
|
|
98
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('encode', async () => {
|
|
103
|
+
const { privateKey } = Crypto.generateKeyPairSync('ed25519');
|
|
104
|
+
const testSigner: Signer = async (data: string) => {
|
|
105
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
106
|
+
return Buffer.from(sig).toString('base64url');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const jwt = new Jwt({
|
|
110
|
+
header: { alg: 'EdDSA' },
|
|
111
|
+
payload: { foo: 'bar' },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const encodedJwt = await jwt.sign(testSigner);
|
|
115
|
+
const newJwt = Jwt.fromEncode(encodedJwt);
|
|
116
|
+
const newEncodedJwt = newJwt.encodeJwt();
|
|
117
|
+
expect(newEncodedJwt).toBe(encodedJwt);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('decode failed', () => {
|
|
121
|
+
expect(() => Jwt.fromEncode('asfasfas')).toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('encode failed', async () => {
|
|
125
|
+
const jwt = new Jwt({
|
|
126
|
+
header: { alg: 'EdDSA' },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
jwt.encodeJwt();
|
|
131
|
+
} catch (e: unknown) {
|
|
132
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('getUnsignedToken failed', async () => {
|
|
137
|
+
const { privateKey } = Crypto.generateKeyPairSync('ed25519');
|
|
138
|
+
const testSigner: Signer = async (data: string) => {
|
|
139
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
140
|
+
return Buffer.from(sig).toString('base64url');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const jwt = new Jwt({
|
|
144
|
+
header: { alg: 'EdDSA' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await jwt.sign(testSigner);
|
|
149
|
+
} catch (e: unknown) {
|
|
150
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('wrong encoded field', async () => {
|
|
155
|
+
const { privateKey } = Crypto.generateKeyPairSync('ed25519');
|
|
156
|
+
const testSigner: Signer = async (data: string) => {
|
|
157
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
158
|
+
return Buffer.from(sig).toString('base64url');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const jwt = new Jwt({
|
|
162
|
+
header: { alg: 'EdDSA' },
|
|
163
|
+
payload: { foo: 'bar' },
|
|
164
|
+
encoded: 'asfasfafaf.dfasfafafasf', // it has to be 3 parts
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await jwt.sign(testSigner);
|
|
169
|
+
} catch (e: unknown) {
|
|
170
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('verify failed no signature', async () => {
|
|
175
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
176
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
177
|
+
return Crypto.verify(
|
|
178
|
+
null,
|
|
179
|
+
Buffer.from(data),
|
|
180
|
+
publicKey,
|
|
181
|
+
Buffer.from(sig, 'base64url'),
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const jwt = new Jwt({
|
|
186
|
+
header: { alg: 'EdDSA' },
|
|
187
|
+
payload: { foo: 'bar' },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await jwt.verify(testVerifier);
|
|
192
|
+
} catch (e: unknown) {
|
|
193
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('verify with issuance date in the future', async () => {
|
|
198
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
199
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
200
|
+
return Crypto.verify(
|
|
201
|
+
null,
|
|
202
|
+
Buffer.from(data),
|
|
203
|
+
publicKey,
|
|
204
|
+
Buffer.from(sig, 'base64url'),
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const jwt = new Jwt({
|
|
209
|
+
header: { alg: 'EdDSA' },
|
|
210
|
+
payload: { iat: Math.floor(Date.now() / 1000) + 100 },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await jwt.verify(testVerifier);
|
|
215
|
+
} catch (e: unknown) {
|
|
216
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
217
|
+
expect((e as SDJWTException).message).toBe(
|
|
218
|
+
'Verify Error: JWT is not yet valid',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('verify with not before in the future', async () => {
|
|
224
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
225
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
226
|
+
return Crypto.verify(
|
|
227
|
+
null,
|
|
228
|
+
Buffer.from(data),
|
|
229
|
+
publicKey,
|
|
230
|
+
Buffer.from(sig, 'base64url'),
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const jwt = new Jwt({
|
|
235
|
+
header: { alg: 'EdDSA' },
|
|
236
|
+
payload: { nbf: Math.floor(Date.now() / 1000) + 100 },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await jwt.verify(testVerifier);
|
|
241
|
+
} catch (e: unknown) {
|
|
242
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
243
|
+
expect((e as SDJWTException).message).toBe(
|
|
244
|
+
'Verify Error: JWT is not yet valid',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('verify with expired', async () => {
|
|
250
|
+
const { publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
251
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
252
|
+
return Crypto.verify(
|
|
253
|
+
null,
|
|
254
|
+
Buffer.from(data),
|
|
255
|
+
publicKey,
|
|
256
|
+
Buffer.from(sig, 'base64url'),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const jwt = new Jwt({
|
|
261
|
+
header: { alg: 'EdDSA' },
|
|
262
|
+
payload: { exp: Math.floor(Date.now() / 1000) },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await jwt.verify(testVerifier, {
|
|
267
|
+
currentDate: Math.floor(Date.now() / 1000) + 100,
|
|
268
|
+
});
|
|
269
|
+
} catch (e: unknown) {
|
|
270
|
+
expect(e).toBeInstanceOf(SDJWTException);
|
|
271
|
+
expect((e as SDJWTException).message).toBe(
|
|
272
|
+
'Verify Error: JWT is expired',
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('verify with skew', async () => {
|
|
278
|
+
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
|
|
279
|
+
const testVerifier: Verifier = async (data: string, sig: string) => {
|
|
280
|
+
return Crypto.verify(
|
|
281
|
+
null,
|
|
282
|
+
Buffer.from(data),
|
|
283
|
+
publicKey,
|
|
284
|
+
Buffer.from(sig, 'base64url'),
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const jwt = new Jwt({
|
|
289
|
+
header: { alg: 'EdDSA' },
|
|
290
|
+
payload: { exp: Math.floor(Date.now() / 1000) - 1 },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const testSigner: Signer = async (data: string) => {
|
|
294
|
+
const sig = Crypto.sign(null, Buffer.from(data), privateKey);
|
|
295
|
+
return Buffer.from(sig).toString('base64url');
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
await jwt.sign(testSigner);
|
|
299
|
+
await jwt.verify(testVerifier, { skewSeconds: 2 });
|
|
300
|
+
});
|
|
301
|
+
});
|