@nodefony/user 10.0.0-alpha.4 → 10.0.0-alpha.6
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/LICENSE +201 -543
- package/README.md +1 -1
- package/dist/index.js +4 -1
- package/dist/nodefony/errors/WeakPasswordError.js +12 -4
- package/dist/nodefony/service/UserService.js +35 -10
- package/dist/nodefony/src/admin/UserAdminApi.js +10 -2
- package/dist/nodefony/src/password/commonPasswordHashes.js +24 -0
- package/dist/nodefony/src/password/passwordHash.js +25 -0
- package/dist/nodefony/src/password/passwordPolicy.js +188 -0
- package/dist/nodefony/src/password/seedFailure.js +35 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/contracts/IPasswordBlocklist.d.ts +25 -5
- package/dist/types/nodefony/contracts/index.d.ts +1 -1
- package/dist/types/nodefony/errors/WeakPasswordError.d.ts +10 -3
- package/dist/types/nodefony/service/UserService.d.ts +8 -4
- package/dist/types/nodefony/src/admin/UserAdminApi.d.ts +11 -1
- package/dist/types/nodefony/src/password/commonPasswordHashes.d.ts +21 -0
- package/dist/types/nodefony/src/password/passwordHash.d.ts +7 -0
- package/dist/types/nodefony/src/password/passwordPolicy.d.ts +60 -0
- package/dist/types/nodefony/src/password/seedFailure.d.ts +51 -0
- package/docs/ajouter-des-champs.md +1 -1
- package/docs/index.md +86 -16
- package/package.json +10 -10
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -8,9 +8,12 @@ import { USER_DEFAULT_ORDER, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON,
|
|
|
8
8
|
import { USER_COLUMNS, USER_TABLE_NAME, assertUserContract, attachExtraColumns, missingUserColumns } from "./nodefony/src/userContract.js";
|
|
9
9
|
import { InMemoryUserRepository } from "./nodefony/src/InMemoryUserRepository.js";
|
|
10
10
|
import { listUserStores, registerUserStore } from "./nodefony/src/userStoreRegistry.js";
|
|
11
|
+
import { truncatedPasswordHash } from "./nodefony/src/password/passwordHash.js";
|
|
12
|
+
import { DEFAULT_PASSWORD_POLICY, PasswordPolicy } from "./nodefony/src/password/passwordPolicy.js";
|
|
11
13
|
import { UserNotFoundError } from "./nodefony/errors/UserNotFoundError.js";
|
|
12
14
|
import { WeakPasswordError } from "./nodefony/errors/WeakPasswordError.js";
|
|
13
15
|
import { mergeProfileIntoMetadata, profileFromClaims, projectProfile, validateProfilePatch } from "./nodefony/src/userProfile.js";
|
|
14
16
|
import { UserService } from "./nodefony/service/UserService.js";
|
|
17
|
+
import { describeSeedFailure } from "./nodefony/src/password/seedFailure.js";
|
|
15
18
|
import { USER_REVOKED_EVENT, createUserAdminApi, registerUserAdminApi, toUserSummary } from "./nodefony/src/admin/UserAdminApi.js";
|
|
16
|
-
export { AnonymousUser, Argon2idEncoder, BaseUser, BcryptEncoder, InMemoryUserRepository, MigratingEncoder, ROLE_ANONYMOUS, USER_COLUMNS, USER_DEFAULT_ORDER, USER_REVOKED_EVENT, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON, USER_SORTABLE_FIELDS_IN_MEMORY, USER_TABLE_NAME, UserNotFoundError, UserService, WeakPasswordError, anonymousUser, assertUserContract, attachExtraColumns, createUserAdminApi, encoderFromConfig, listUserStores, mergeProfileIntoMetadata, missingUserColumns, profileFromClaims, projectProfile, registerUserAdminApi, registerUserStore, toUserSummary, validateProfilePatch };
|
|
19
|
+
export { AnonymousUser, Argon2idEncoder, BaseUser, BcryptEncoder, DEFAULT_PASSWORD_POLICY, InMemoryUserRepository, MigratingEncoder, PasswordPolicy, ROLE_ANONYMOUS, USER_COLUMNS, USER_DEFAULT_ORDER, USER_REVOKED_EVENT, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON, USER_SORTABLE_FIELDS_IN_MEMORY, USER_TABLE_NAME, UserNotFoundError, UserService, WeakPasswordError, anonymousUser, assertUserContract, attachExtraColumns, createUserAdminApi, describeSeedFailure, encoderFromConfig, listUserStores, mergeProfileIntoMetadata, missingUserColumns, profileFromClaims, projectProfile, registerUserAdminApi, registerUserStore, toUserSummary, truncatedPasswordHash, validateProfilePatch };
|
|
@@ -4,12 +4,20 @@ import { nodefonyError } from "nodefony";
|
|
|
4
4
|
* Mot de passe refusé par la politique (connu-compromis / interdit) — `code = 400`.
|
|
5
5
|
*
|
|
6
6
|
* Levée par `UserService.createUser`/`changePassword` quand le
|
|
7
|
-
* {@link IPasswordBlocklist} branché rejette le candidat. Le message
|
|
8
|
-
*
|
|
7
|
+
* {@link IPasswordBlocklist} branché rejette le candidat. Le message NOMME la
|
|
8
|
+
* règle enfreinte quand la politique sait la dire, et rien d'autre : ni la
|
|
9
|
+
* source de la liste, ni le mot de passe — il finirait dans un journal.
|
|
9
10
|
*/
|
|
10
11
|
var WeakPasswordError = class extends nodefonyError {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/** La règle enfreinte, telle que la politique l'a nommée (`null` si muette). */
|
|
13
|
+
violation;
|
|
14
|
+
/**
|
|
15
|
+
* @param violation - règle enfreinte, en français. Omise, le message reste
|
|
16
|
+
* générique — c'est le cas d'une implémentation qui ne rend qu'un booléen.
|
|
17
|
+
*/
|
|
18
|
+
constructor(violation) {
|
|
19
|
+
super(violation != null && violation.length > 0 ? `Mot de passe refusé : ${violation}` : "Mot de passe refusé par la politique de l'application", 400);
|
|
20
|
+
this.violation = violation ?? null;
|
|
13
21
|
}
|
|
14
22
|
};
|
|
15
23
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { USER_FACETS } from "../src/userFilters.js";
|
|
2
|
+
import { PasswordPolicy } from "../src/password/passwordPolicy.js";
|
|
2
3
|
import { UserNotFoundError } from "../errors/UserNotFoundError.js";
|
|
3
4
|
import { WeakPasswordError } from "../errors/WeakPasswordError.js";
|
|
4
5
|
import { profileFromClaims } from "../src/userProfile.js";
|
|
@@ -28,12 +29,16 @@ var UserService = class extends AbstractCrudService {
|
|
|
28
29
|
encoder;
|
|
29
30
|
#dummyHash = null;
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Politique de mot de passe (NIST SP 800-63B §5.1.1.2) — consultée à la
|
|
33
|
+
* création et au changement, jamais au login.
|
|
34
|
+
*
|
|
35
|
+
* **Posée PAR DÉFAUT**, et c'est le point : un défaut `null` ne se voit pas,
|
|
36
|
+
* et personne ne l'écrasait — `security:user:add compta --password abc`
|
|
37
|
+
* réussissait. L'application REMPLACE cet objet pour durcir ou pour brancher
|
|
38
|
+
* sa propre source ; la mettre à `null` désactive tout contrôle, et c'est
|
|
39
|
+
* alors un geste explicite, pas un oubli.
|
|
35
40
|
*/
|
|
36
|
-
passwordBlocklist =
|
|
41
|
+
passwordBlocklist = new PasswordPolicy();
|
|
37
42
|
/**
|
|
38
43
|
* @param repository - source de persistance des utilisateurs (credential inclus).
|
|
39
44
|
* @param encoder - encodeur de mot de passe (hash/verify/needsRehash).
|
|
@@ -51,7 +56,7 @@ var UserService = class extends AbstractCrudService {
|
|
|
51
56
|
* @returns l'utilisateur persisté (id généré, hash stocké).
|
|
52
57
|
*/
|
|
53
58
|
async createUser(input) {
|
|
54
|
-
if (input.plainPassword != null) await this.#assertNotBlocked(input.plainPassword);
|
|
59
|
+
if (input.plainPassword != null) await this.#assertNotBlocked(input.plainPassword, input.identifier);
|
|
55
60
|
const password = input.plainPassword != null ? await this.encoder.hash(input.plainPassword) : null;
|
|
56
61
|
return this.create({
|
|
57
62
|
identifier: input.identifier,
|
|
@@ -142,7 +147,9 @@ var UserService = class extends AbstractCrudService {
|
|
|
142
147
|
* @returns l'utilisateur mis à jour, ou `null`.
|
|
143
148
|
*/
|
|
144
149
|
async changePassword(id, plainPassword) {
|
|
145
|
-
await this
|
|
150
|
+
const target = await this.findById(id);
|
|
151
|
+
if (target === null) return null;
|
|
152
|
+
await this.#assertNotBlocked(plainPassword, target.identifier);
|
|
146
153
|
const password = await this.encoder.hash(plainPassword);
|
|
147
154
|
const updated = await this.repository.updateOne({ id }, { password });
|
|
148
155
|
if (updated !== null) this.fire("onPasswordChanged", updated);
|
|
@@ -261,9 +268,27 @@ var UserService = class extends AbstractCrudService {
|
|
|
261
268
|
if (Object.keys(oauthProfile).length > 0) data.metadata = { profile: oauthProfile };
|
|
262
269
|
return this.create(data);
|
|
263
270
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Refuse un candidat que la politique écarte — et NOMME la règle enfreinte
|
|
273
|
+
* quand la politique sait la dire (`violation`, membre optionnel du contrat).
|
|
274
|
+
*
|
|
275
|
+
* `null` = aucune politique branchée : l'application l'a retiré sciemment.
|
|
276
|
+
*
|
|
277
|
+
* @param plain - mot de passe candidat.
|
|
278
|
+
* @param identifier - identifiant du compte visé, quand l'appelant le connaît :
|
|
279
|
+
* sans lui, la règle « le mot de passe répète l'identifiant » ne peut pas
|
|
280
|
+
* mordre, et les deux portes d'écriture n'appliqueraient pas la même loi.
|
|
281
|
+
*/
|
|
282
|
+
async #assertNotBlocked(plain, identifier) {
|
|
283
|
+
const policy = this.passwordBlocklist;
|
|
284
|
+
if (policy === null) return;
|
|
285
|
+
const subject = identifier != null ? { identifier } : void 0;
|
|
286
|
+
if (policy.violation !== void 0) {
|
|
287
|
+
const violation = await policy.violation(plain, subject);
|
|
288
|
+
if (violation !== null) throw new WeakPasswordError(violation);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (await policy.isBlocked(plain, subject)) throw new WeakPasswordError();
|
|
267
292
|
}
|
|
268
293
|
fail(identifier, reason) {
|
|
269
294
|
this.fire("onAuthenticationFailure", identifier, reason);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listUserStores } from "../userStoreRegistry.js";
|
|
2
2
|
import { USER_FACETS, USER_FILTERS, USER_STATS_FILTERS } from "../userFilters.js";
|
|
3
|
+
import { DEFAULT_PASSWORD_POLICY } from "../password/passwordPolicy.js";
|
|
3
4
|
import { WeakPasswordError } from "../../errors/WeakPasswordError.js";
|
|
4
5
|
import { mergeProfileIntoMetadata, projectProfile, validateProfilePatch } from "../userProfile.js";
|
|
5
6
|
import { parseFilters, parsePageQuery } from "nodefony";
|
|
@@ -9,8 +10,15 @@ import { parseFilters, parsePageQuery } from "nodefony";
|
|
|
9
10
|
* Les garde-fous anti-lockout protègent **ce** rôle (jamais déchoir le dernier).
|
|
10
11
|
*/
|
|
11
12
|
const ADMIN_ROLE = "ROLE_NODEFONY_ADMIN";
|
|
12
|
-
/**
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Longueur minimale d'un mot de passe self-service.
|
|
15
|
+
*
|
|
16
|
+
* DÉRIVÉE de la politique, jamais écrite ici : un plancher recopié devient un
|
|
17
|
+
* plancher qui diverge, et c'est alors la porte la plus laxiste qui fait loi.
|
|
18
|
+
* Ce contrôle ne remplace pas la politique — il rend seulement un 400 lisible
|
|
19
|
+
* AVANT de toucher au service (le refus complet, lui, vient de `changePassword`).
|
|
20
|
+
*/
|
|
21
|
+
const MIN_PASSWORD_LENGTH = DEFAULT_PASSWORD_POLICY.minLength;
|
|
14
22
|
/** Convertit un timestamp (`Date`/string ISO/number) en epoch ms — `null` si invalide. */
|
|
15
23
|
function toEpoch(value) {
|
|
16
24
|
if (value === void 0 || value === null) return null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region nodefony/src/password/commonPasswordHashes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Empreintes des mots de passe les plus courants — **fichier GÉNÉRÉ, ne pas éditer**.
|
|
4
|
+
*
|
|
5
|
+
* Regénérer :
|
|
6
|
+
* `node scripts/generate-password-blocklist.mjs <fichier-source>`
|
|
7
|
+
*
|
|
8
|
+
* Contenu : les 4 premiers octets du SHA-1 de chaque mot de passe, triés, en
|
|
9
|
+
* base64 d'un `Uint32Array` gros-boutien. Le clair n'est PAS embarqué — ni
|
|
10
|
+
* lisible, ni reconstructible depuis ce fichier.
|
|
11
|
+
*
|
|
12
|
+
* Sources :
|
|
13
|
+
* - `xato-net-10-million-passwords-10000.txt` — 9999 entrées, SHA-256 `c63d5e4ccc31344d662583cc39ca4bd5…`
|
|
14
|
+
*
|
|
15
|
+
* Corpus : SecLists (`danielmiessler/SecLists`), **licence MIT** — attribution
|
|
16
|
+
* portée ici, telle que la licence l'exige. Le corpus lui-même n'est pas
|
|
17
|
+
* redistribué : seules ces empreintes le sont.
|
|
18
|
+
*/
|
|
19
|
+
/** 9999 empreintes triées (39996 octets décodés). */
|
|
20
|
+
const COMMON_PASSWORD_HASHES_BASE64 = "AAJrhQAMoqsADnk9ABXQNgApXWwAKZpAACt+zAArvroAOkVQAD0RWABDwL4AUQotAGGd/ABjRbEAaDnSAGiUyABpNLkAaZIGAG4dSgBvL9wAcFfTAHN2JwB04SEAfQk5AH7t9gB/iw0Aj6paAJl8TQCaVvAAnHVXAKf+DgCqrFQAq+12AKwk+AC3uBEAxnl7AMe1UQDK/RIA3tPXAOodpAD5fLUA/UtFAQERvwEED2QBE/AyARdpHQEZb/cBHJRfASfJCAEvRDQBODXHAT6JdQFGThYBRnYBAUpfUgFM8+EBT3wQAVTA2gFawMgBYJU5AWc8nQFozX8BblfIAYPslgGEDVUBikhnAY9NfwGdsL8BqEp+Aaz0PQGtPFABswesAbp5kgG9vhUByFmhActhSAHeC8gB379GAeue+wHwe+sB9GAVAfYnEQH2yGEB+iakAgg/RQIItjACFjsEAhjmDAIpGdsCP3kmAkCi7gJCL0UCSnjkAksBkQJTiTICVfv8AlfdMAJe7ZQCafUNAmzskAJzBMkCc8yOAnwRRgJ++qYCgVEUAoYkxQKGhjUCh+8tAo72YgKQZxYCnrFeAqKodgKkUjwCrEhFArEA3gK8Uo8Cw5uyAsRDJALNPTYC2iXDAuAYKgLgqZkC4n93AuphPgLyAo4DB4SVAxuq8gMn94oDKub7A1IOBANUTTIDVUf2A1kbXgNa5psDXVxSA3WqWgN4XU4DfhJMA4ss1AOMNA8DkaHlA5LUVwOeCp8Dn1l5A5971gOsK7ADsdmLA7JUGAO123IDwGW6A8HezgPDdEYD0UbCA9FqmQPV2tcD1nwmA95sVwPtQewD9yV2A/iEmAP98TIECvJDBA8G/QQP66gEG1pJBCfDJAQ0104EN0ftBDimyAQ41IQEOlWCBEH3+QRFB8gERwDfBFNb7wRa1EkEcp/KBHfXIAR/+CUEgI1LBIkN2ASRVRQEoHRaBKT85wSmaKMEtp5QBLp1uQS/iG4ExyJ0BNmNKATctpsE4hG+BObzvATojIgE7kJfBPCBdAT3TLkFBKVXBQYArAUGf0oFBt1FBQmJSQUNhZwFD04yBRUi0AUcWlwFIQI3BSo8FgUrm/MFL1UTBTC0+AUyNFcFNcEMBUpTRwVOqYgFWWEGBVsfRgVeoMsFZtQ2BWvZIQVvEYIFcLkvBYID8wWNZWoFliBFBZYq0wWbOqsFm4uIBZz+HwWdPEIFoF+rBaFf0gWioa0Fph0lBatcNQWx81YFtTCtBcONqAXKxw8Fy07HBdHOAQXewRoF6QTqBe8rgAX3oh4F/nRhBgd1oAYNw7AGDwAvBhVZHwYfE5EGIa/JBicVJQYsKqgGL6WpBjKZFQY4SnAGOPWvBjvbPAY9zyEGRFA8Bk3v8gZOv7AGUHAxBlVcZQZki+YGZWKLBmx+TQZ8srQGiULIBpqCwgaf06QGwlkIBssCkwbb95wG5RJ9BvBr7AbzDZAG+pBdBv/XfQcHCrsHDU+XBxa5AgcrSVIHLKumBzfCLQdP5oEHV6BlB1n1bAdeNjkHYPDaB3RU7Qd+vVwHiWM6B5E7WQeTt3UHlM03B5jr7Aei4lYHtNzOB7a2tQe3FkMHwewWB9gOrwfijO4H74eRB/TAxAf1fqMIA99PCAXblggR4s0ILMmDCDmr7ghBn5UISFz8CEo1AQhKtAkIX7eUCGRWFAhlTqYIZzIuCG2NnAhw55MIf2KzCIAtcAiAgGUIgIY6CIPz2AiKAs8IjhahCI5KLgiUeOIInMvVCKC48wirFygIuVfFCLqVkQi7Km4IvFvtCMANwgjDJHYIxAYJCNRXZQjX3vEI45dmCOdNHgj244cI++WiCP2twgkDoUsJG1A1CR4XqQkhKi8JJflHCSZLqAknsbwJKECTCStEOQk8r3MJP/JdCUBR/QlAdjkJQfYHCUf8yQlMJvQJUM/TCVO9PglY0okJWySECVs5cQlcKnYJXQbmCV4pPQlfnBwJY5kgCWsg2QlvwRIJcsfjCXMFcwlzGw0JdKv+CYAJJgmI8mgJi8UcCZmm3gmcJ94Jqe0sCao6qgm8MoYJwGlTCcFnKQnH+B4JytgvCc3ZRAnZbIUJ6EkDCe3OvAn17esJ+DaJCfkFMgn+sTcJ/7sgCgIiHgoFEpIKC2S+ChJU4goY0UoKGtG5Chsyigog058KJVogCifhLQormCcKLk2oCi9Klgo0iTQKO6GvCjwVeQpCtaEKQra5CkrZGQpUvJYKWJ2lCl1WcwpiBIEKYp0KCmO1XApkAO8KZuEHCmrwnwqJTbcKis7XCowKBgqSMQcKlbShCpvl9wqnplEKqcYGCrCbQgq2DucKt5knCrjcQwrF7p4KzH+tCs037QrX27IK6eTeCvVOHwr59rMK/eVmCwO7MgsGrDQLCD5WCxL8VgsXm4wLHD6cCyGzTwsm738LMhobCzLmXQszB2gLUN1+C1TBOwteZT8LaP2VC3+/NAuAJhELhzhrC4ocrguMW0gLjgsfC5OgUwuXEjkLl1S6C5lk4wucJiULqWd1C6yccQu7ZC4Lu7p3C8BmpwvA1B4LwvTyC82a9wvSuIQL1PEkC9fqRgvaKqAL6QmMC/ncFAv6DOsMBupoDA2m7AwQpEgMFM7+DB5LRwwq2i0MNHpQDDs+lww93PwMSg3aDExrEgxPtZUMWNqdDGLL2wxrD5wMbQGCDHAgSQxyZV0Mc1PmDH/c2gyGHCAMr5gQDLJhnQyybiIMs77DDMH2OwzE3g0MzAhIDNCxkgzVLb4M1hxVDNj8LAzgy2gM55EeDOtViQzvEZsNAdWJDQIdJw0Y4rYNJwOIDSftPA0pjioNLMdHDTQHbw05W7oNSqlVDV+WWw1hLBINcv8JDXOMRA15Nf4NeixXDYnhjg2RHTkNk+BtDZVtQQ2VboYNlqF+DZoltg2f8ewNpNvZDaUHrg2u7CENut0GDb1MLg3BVtINzDzEDdtYdw3lLFoN6UuTDfHOrA3ykxIN9bMvDf9I8w4DxiAOEiIYDhWR6g4YStwOGPRMDh1Nlg4hSHAOIyRrDirf/A44IHYOPWt9Dj4TRw5ElYQORJqmDk1NTA5RSgYOVaw3DlbATw5mtJ0OgYv6DoQXsA6F4SEOiNoXDoo62Q6PY3AOmfv+DqBPqA6jWgwOqKdKDrCs+g6zxFcOtNwaDrzcew7FloQOyWHDDs3seg7PAgIOz7w4Dtv8PQ7cpLEO4d7IDua9Kg7skDYO9UkEDv7FHw8NvQgPD8ihDw/Pqg8SVBoPFY5kDxi4tg8d79UPNPf0D1JhJA9bMMAPZaCnD2eJCA9qhHMPbF0CD3X5sA99DQgPkXh8D6E+nA+37FEPujgcD8pxvw/YS74P3UWxD+dEgQ/rw2MP7KcgD+z+eQ/xH7AP+pMmD/2tjRAACG0QAehwEA8dwxAUzlwQHLyRECcSxxA2zNoQORPpED1emhA/68oQRROOEExROxBOAzEQVxi0EFuKjBBqtOEQb//HEHe2rhCGsicQiOtKEIzs8xCZGeUQm1xyEJ+3TBCgfNsQplKqEK1OcBCwubwQtXCRELsJ+RC8SGoQwlZlEMKPnBDEuJYQ0LVeEOTzgRDqvR4Q7m2TEPa0shD5u48Q/WRLEP2XjREM5E0RDfPHERGKLxEdWGMRJz1XESu3kREt/xwRPlkVEUFKQxFCsz4RSMQHEUpC1xFRtVYRU28LEVlHhxFhXHQRYeb/EWXPvRFpt6cRc8vJEXzz4hGDHQURhJa+EYfAtRGUmDERmQuaEZ7ylBGgk40Royk/EaPgWRG5k+ERz5j8Edv2bRHeozQR4dDhEePgcxHmCw4R5iclEejhohH47lYR/ZjBEf7jNBIAcDUSB7kDEggm7hIc+mUSHgUiEjtsVRJH648STmKkEloSRxJfaL0SZ+pUEmtSfhJvxpoSfn4LEoNRExKVivwSlqJrEpbmlRKb3b4SoWGFEqTEjhKz9zcSxSg3EsYoPhLKdIUS2GC+EtqFdxLeqW8S31XfEuWSlhLl1m0S6Sk+Eu3VkBLzmVITEAmOExRdGBMVPawTG3jsExxTjRMiXwcTN9q+Ezm8uRM+PWsTRop/E0dfWxNj1GQTZDodE21qOBNuPPETeY6TE3vvfhOBilYThb6uE4gl7ROLw/8TjXMlE5BHDBOb7D0TnpbDE5+IrBOigosTopMGE6X58hOoUGUTsZh0E9eEDhPbYcMT3gaUE96IiRPkdcgT5en9E+fCxhPzAr4T9WaiE/e9DxP554UT/i8VFAEs0RQFGFkUBd9mFAmGohQQtWEUEWeKFBiiQBQY6OkUGinVFB+HvhQkdPkUOKAYFEMdrRRZafEUXsmaFGGw2BRkkasUZqvIFGjr5BRruPwUdSLjFHhH1xR9t+4UfoEwFIJaZhSC+0QUhQemFIYnCBSKu98UjHTwFI4O/RSWqmkUmTAyFJtr3xSidfsUpS3AFKrIihSxBGgUsRDuFLmzShTAZHQUwNjXFN615RTgb3AU5CnVFOjYORTrFOwVCor3FQ/58RUQVqIVFZQoFRfVRRUb0pkVKqIsFT+iOBVLlskVUa+uFVY1VhVfBCIVYs/zFW9ZqRVyvTAVc/PBFX4NpBV/qPUVj0/lFZKvfhWYXnMVnZSAFZ/x1hWgcwoVpERLFaST+hWoamYVroP+Fa7TyhWv1yYVzJ51FdPcWRXYNLMV3AX4Fdw55hXj1QoV6ruBFf3DuhYBDacWAnPdFgXcKhYU73EWLRWgFi3M5hYv0lkWNNU8FjYPjxY+Zb4WQMWDFkXueBZQh84WUtkSFlM0fhZb02QWYoIgFmKoIBZjmAMWat98FmwXGRZuug8WbyCSFnXwhBZ2hqcWe2xKFn3k2haBDrkWgxvDFoeEohaQu2oWsjxQFrtrwha7+aIWvct2FsG2bhbFYJAWzFT/FuFMKBbi10wW54l7FuzAVhbuqKoW8ZCVFvYE/Bb3ZvIW+HKgFvnAxxcDc60XBpNKFwfsZBcP1+gXEx+XFxdVLhccvn4XOCPDFzrjwBc7aiMXPZFoF0jK6hdJw4kXS6bBF1FHMhdSSJUXWHUoF1qPeBdc0k0XahQNF2177hd1hm8Xd2w9F4K+VheG47oXi2LnF44y8BeZKWEXo+SLF7nhxhfTlgIX3vD5F+ewmhfonDUX9y/gF/qqqRgEv8kYEJowGBf1Hhgf6JcYKPcrGCtstRgvPcQYMeucGDOFLxg2wCIYNyNyGE0h3hherJUYajM1GGqOkxiMz6sYnStNGKPcYhilzXkYqISuGKmMNRi23ZYYwPEDGMKGBBjE34sYyF6PGMh4VRjJ1GIYyun6GMyFLBjOaUMY0VZBGNPyNRjhHt8Y5A4UGOg4whjr0jUY+IemGPuHDhkK1xwZEGWRGRtSChkl95MZN8TCGTi3lxk5RJQZP/7aGUhKghlIXjYZS42LGVcEyxlkfbIZZuaUGXEWGhmGV5UZiuJbGZYmyBmZ5IkZm7xqGaJFMBmkTjEZp6e2Ga2OnRm1hUMZtpuIGb3ymhnCIg4Zyx8sGdPO+RnX6TkZ3UZuGeCoQhnzGe8Z+KjaGf3PvxoCagkaDYGtGhU8qBohFXoaKA7DGivwrRo9ABwaU+uLGlgT3BphIWsaYZNoGmpdkhpzr54adBUUGnRYthqEzqUahWWpGowk+hqULqsalUYoGpVRshqV5HAam5UIGpvn/xqiXq0aqKmgGrKTGhq9LEcaxh9VGsn9+hrMKVEa01XHGuYiSBrti3sa7ipmGvF+cxrzc2Ia+059GwXWNxsInUkbDY1yGxUODxsZwAEbHCajGy1D6Rsth54bMHfVGzc07Rs8zNobRLMBG0gBWBtJ13AbS8cMG1EDNRtY7RYbWj79G1sVsBtcv8wbYCxFG2CQkRthiF0bZpM0G2+azRt65GQbfBU4G31n+Rt/CEEbhDOhG5vxkxuhtbUbozIGG6eXdRu1jMgbuGSaG9eZ/hvcEvAb4qRMG+srpRvu94Ab75ULG/H9uhwBijAcCO+5HBbBpxwbniYcHboHHCTW4hwnP3McKOxiHCnPDBwwa9wcMrjxHDT3exw9i7McPe5qHEBMaRxMtI4cV2sDHGAnwBxg07YcbX2GHG/ECRx5XcQcjAjbHJBZFxy1vVocuXCBHMrOoRzUJJEc4UFjHP+pih0GoNcdHddlHSu6XR0vVuYdMiOEHTMt9R1P4t0dVyrLHVsYBx1fKdgdZHHJHWqOJR1uHPcdeZ0vHX00WB1+6dEdgTkRHYQISh2IdeodnX6BHaCZTR2ivZwdpwhIHahAJB2xEWgdsSvpHcFN3h3ENcwdx9ZjHczcEh3OjKod0JuqHdJg1h3TbD0d1GQsHd62uB3n4/kd+yG3Hhf9iB4qK4IeKixOHi9Nvx42M8keNj8+HjZffx43Id8eP2ejHkHJgR5K3lIeTsBnHmHU6R5rtEIebn9FHoXfxR6Ki34ejgHyHpHQvh6Y4lwem5LnHq8hkh6wuNwesPd5HrTGuR631cIewTasHsHjZh7PDTse36ZaHuKvxx7kWWMe53YKHufhOB7pnNQe62W6HvQa9B71luUe/PqrHwFgBx8S4G0fHTdyHx2BXR8fse4fJM6oHy7SBR8zLlQfNWaIH0ngQh9KBOUfSuBHH1MvVh9VI6gfWbmkH2Wsgh9szSsfbN1/H3Hg9B90ZI4fdHPkH31yzB9+PUQfgkKtH4LJQh+D6PAfhuUVH4jLYx+KwQ8fjK19H5J/RB+av5Efou9HH6/FEx+ye94fszgfH7xrKh/IVBEfysVKH8vlEh/RtFEf1lXyH9xXzx/et6Ef6A5QH/Civh//jHsgBSqIIBAoHSAbjyAgHOsFICxhMSAwC9YgQDahIEqwhCBNG2ggUTObIF194SBrFKEgbIBBIG8KmSBvhuYgb8sgIHZ4syCJY1QgjXiKIKCyoyCnrTAgrGGVIK1wFSCvJVYgtQIhILcPCiC8Kewgvu1hIMGUvSDB4AMgxWVtIMXYsCDH9eIg1J4xINdf4SDqvl0g67oMIPmpACD/P9ghCij1IRwRQCEicCIhI4POIS1OTCEyzg4hM43lITdY9yE6/vIhO6+xIUxBgCFS9a8hU0ZmIVQuOCFZekchXmiZIV6JeiFixuwhc7ikIX//OCGgkiIhonsKIaMyMyG8m1IhvNaGIb8h+CHASlAhxRA4IcyzyiHRfwwh0tHWIfaGXiIF5BYiBny1IgqunyIMAxQiGONYIhpPPiIdL+QiHqEnIis+ESI7t40iQtoOIkXMCSJKlbciUnFaImIxwiJmX5wiZ+ksImwJbiJ0oHIidV1YInWwUSJ/ONsigP5yIoNwJCKUK3wimGJfIpm0iSKjZuUirGMIIrwh8SK8i/0ivW8bIr9dSiLEAtsixUVAIst5WCLQXQQi02LwItkcWSLizngi8QAyIvNduiL9kAEjBxKFIwh02CMOCd4jEZ9ZIxSy4yMrq7AjLsfCIzE2hiM5assjPKDzIz7FvSNSS+kjWs/fI2aM0iNxTVwjdQm7I3X6YyN3bg0jgdnXI4N/NiOGm3MjhuTzI4ce3SOKGEMjimpTI5DPEyOU7qwjn0VKI6BTjyOgteQjoXUZI6YYkiOzbqQjtYWuI78AvSPBv2YjxoNLI8m2DyPePREj37RXI+WR6CPpRUsj7OpSI/KRbiPz/Xcj+J47I/vyPCP9p5EkBjOqJAZaviQO84ckFhxGJCDs0iQhMkgkJ3gwJDX/5yQ8oiwkP1GWJE+3WyRbN7skXR6KJHX8sCR3McckgZHFJIUQEySJAhMkjTWfJJidjiSY/pIkm6NgJKFzPCStqNAkranyJLKHhCS1X+gkuGYYJLtFxiS++UgkwK5hJMmxXyTTxv4k1dNKJNbl8yTjuQ4k6WBeJPwYdCT8GX4k/LfjJQnmCSUOd/ElDsWjJR44LyUgaxglLGx3JTMiOSU4Fm4lOdPfJT+6rCVIJOclT3aXJVVJqCVcy44lX1PGJWRtbyVzHGAldVDaJYBvRCWEb/8lh4y8JZKBZiWTEuEll2AHJZrtXSWdysAln5hKJa5HESWvDR4lryW3Ja/39CWx790ltM6hJbcysyW+aJQlyrnFJdZpxyXcKCsl3GTcJd19UiYRNo0mIcE1JiS7gyYp+20mK0K3Jj0AgiZFBwomUVbdJlOS3CZbEdQmW8VMJmSsEyZplMsmbHF3Jm+D0iZ4QM8meJO8JoC2dCaEo3cmiJjeJomlqiaNK4wmkJswJpQl7CaUMDUmlKUPJpUpVCaZN40mmgP0Jp/jcyaqwZwmqzg+JrGQQya7614myxr2JtM2hybaIKom5iT/Jujj0ybsjQAm70XEJvPNIyb1gK4nAguHJwSy8icZYXInGncJJx6aVichx08nIcwSJzoMeyc9QHUnPWnvJ0H12CdUWQAnV6U4J1mS6CdacAAnXl1fJ2BmbidhOnUnfBe/J3xFkyeAI9Ynhs7ZJ4fdFSebQjAnrmDGJ7VOZSe+X+onwPQUJ8qw8ifLFsIn4YEoJ+TjYSfnLbon6wxvJ+vIbifs3mMn+nbwKAqVCygLq8YoDs0UKBGKPigSnPooE0jEKBU6Cygh7cooJ3iBKChFWShDRtwoS+HQKEzpdihcAXsoYRejKGMMtihn2f0oa5t7KGxCoihtFPcogyBaKIcNpyiQ8yUokbrOKJG88iib2S8ookcQKKU8biinGLQoq5FbKLkrVijXx+oo2wfeKODTIijlFpEo6+4nKO01GSjzHiko9/3kKPqMQykKXRgpC3UYKRL6Xikcay0pKVn2KSsJASkwKKopMS1dKTQWzik2SiMpOQlPKT/XvylDpC8pT4h7KVHhASlY60Epe+sBKX4UeSmLmKIpkTuYKZwthSmt8sMpxksUKdFYgynfV3Ip5bmoKfKjKCn0170p/KDNKgDAtSoMqBcqDM92Kg5AByoSuf0qF6nLKhfOFCoj4goqQbZ3KksXsSpOFgAqUN35KlQYiSpWnfwqZsHoKmntgCpsKpoqf6NaKop1kCqUtQoqnNtxKp9o5CqmCo8qsFkdKrjjNiq9VeAqw5CdKsWE8SrHlSwqzCrXKtsGByrkuqUq7VQEKu7egCrvDgcq8rAiKv+u9SsCCScrAtvBKwtPtysMqoMrDQifKxdG4ysayt0rIlFVKyLzUismEIgrJ1bZKykMwysu414rNnthKznbACtD+4srRpgjK1wE+itcJA4rXELyK10F0ytol5krdD6lK3a8ZSt+3NgrfuD0K4YotyuP42IrrZcRK7a5hivB7LQrxgOMK8dUWCvPWNMr4e3EK+iK4Cvt2tsr8AsHLAME8iwFi58sC0P+LBfk2ywm1uIsMJT6LDFXviw77LwsPZpNLD3Gqiw/4SYsQlZnLFSJLCxp9QEsdfknLHY9cix78bcsf5/SLI7LYCyQVagsmWA+LLUK0iy/H4MsytQ7LM5KkizQcKQs2qYjLN5ezyzfgtIs4ve2LOwI0SztPugs8g8+LPZzoiz742Ms/lNKLQBIFy0Cnb0tEVclLRLRvC0c1yYtIaVPLSe2LC0tqZotMFLbLTVKLy04vYctOyrmLUN70i1RKNctXNNQLV6+8i1i7/8tejTJLYUCPS2FCW4tiFqoLY1Zai2RNmgtl1LqLZfGay2hTd0tol9OLaNv8y2o2t4tq73zLayB1i2s648tr8yqLbbSHS24OeAtvC/SLdK+1y3vMJ4t8rVHLfRh2S3/T8kuAPQVLgTzPy4aALQuIRA9LitlMy4ve5kuNA26LjZqVS48D+4uS4n+LljgwS5iDtgubf+ELm/8ny557QQuiZCoLoofSi6KqRguiubKLowCdy6N7e8ukFT2Lps6Ei6mIBoup9UHLqjbbC6x90cutDbCLr44qy6+7U0uwd21LsWsPy7JywMuziMpLw1m8y8YmfIvIPJNLyT7GC8nxZcvKlZnLyu5Fy8ySgEvMvYhLzWyEy84CDYvPb+BL0EdOy9G0CYvTFzgL1bArC9ZrfYvY19tL3DiOS93olAvjkH4L5CW+y+Z1gkvoZgbL6NQtS+qlCovteE0L8fxRS/IiEQvzofAL9L4Jy/XGb0v2BQzL9kihy/btTov4wC4L+6gfC/xS1cv/qV+MAotGTAKw6kwD3USMBV37zAnTEcwJ3dQMC5t/jAvGNIwRDmEME3YYzBOE18wTkmKMFBH6TBSFQ8wUxwoMF8KMTBh7xEwYywwMGyoWjCAXqQwhSlgMIhJ/TCX32EwmGyBMJlEfTCikRYwqbUjMK6XSTC1RrcwxVTCMMjHlzDRcOgw35nhMOH+UzDjL80w6ZMlMO74XTDzQGQxAXpyMQ8zVTEWOWcxGTG7MSPwNjEywgcxNM39MTr6UTFC50oxWQEVMWHNxzFnz3Yxbvt8MXkmyTF/HnYxgrVNMYaoFTGKRhoxjMitMZAGkTGYt3AxmeoFMakBDDGqWawxrTALMa16vjG+T2Mxw1NJMcgcJjHP3k0x5J4ZMfNyozH1H64x/C3yMf49pDIAblUyBmbNMgrSZzILynEyDRpHMg72ozIQYO4yEGMBMhs7ZzIfa34yM5YRMjXlWjJAXogyRq7hMkcRDDJMCzAyTR3MMk0rwDJV/toyVi2yMmTTZDJtF38ybeciMm7f2zJxVqsydRQrMnvH1jKA50EyjjljMpKzTTKSzPoylUh1Mp7TZDKg8XEyqXu5MrFOZDKyaicys0kTMsIa0jLIu/8yyTNXMsuRODLQGpQy1z0ZMuFFsDLmxcIy+EuVMviX8DMRvWYzEeRTMzxeVTNDM00zRBprM0fthjNIUbYzUhjqM1b7CzNe5AEzZAzDM2n23TNsbSwzbQgUM3Qy5DN0q5wzeH2QM3jh2zN5e+Uzee4yM6SOxzOp4mkztQGlM7q0oTPDkogzxrzdM8fYWzPVBJMz9UTpM/knNDQAkHA0EadbNBedUzQYI8Y0GHcrNB+IfDQh7N40JIqyNCVkvDQ2Tzs0OvuHNEIM9TRQQtk0UOilNFEgQjRYtPE0Z9L4NGoaPjRsOSo0bTSgNG3l+DRvWYY0b3iBNHgBhTR594o0f/kQNIFiEDSGJyw0i/NvNI9YiTSPr9Q0kYjVNJX/aTSdER40o0XpNKg+kzS9EAU0v49LNNLIpzTaE2k03I4YNOgUXjTrTE407EQDNO3y0jTwaf01AV3SNQTsszUFhIU1CuZtNRQpyjUXi5M1HR1ONSErkDUnK/g1LMb/NS94KTU8Pnk1P9VBNUEG5DVP8EM1WdesNVnvwzVhUpQ1Y010NWdeaDV8zmk1iPtcNZtw4TWij8U1osb6NauUPTWxYdA1sZ+oNbRfkjW8bCg1xM21NcwBpTXRYFM10bNiNdX2WDXZ+bM14pvgNeUq0jXpwkk17VQGNe2L/jXw6+E2BnRpNg5G8TYiPlw2JNuINiab/TYnKNE2KStbNjJX1DY/Ytc2UI3GNlgmOzZewXo2YhiNNmJ04TZrjOQ2b1t7NnSVHjaArXY2gU0ANor9mDaL2JM2jDzJNo+XaTaSv6Q2oWhANqMuljaju+A2p6ybNqfulDarSqo2vU9zNsU/pDbPVyY20YWKNttu8TbcaXg24o9xNuRzyDbkiGI26OVQNuzJIjbufVI28qEMNvNuQTb20pk2+yfyNwAXOTcB5jI3CM8jNw+GkjcR7s43JydGN02cYTdTKag3X/WeN2pA/Dd3YB83e2Q6N4NzYzeLu3U3kuTTN5eE5DeandA3nXeKN54PaTeedcg3rF4RN6zfeDe3R4k3xQCHN8bFezfLT3830jH9N9LvKDfUFpk33PkEN+bTNjf5Ogc3+kJEOAEbuzgDjhc4DMA4OBZk8TgZHB84GknHOBvgCTgck/o4HcDeOClIazgvQ5I4NK+/ODgmVTg/1784QsMdOEZL8DhGTvE4SoqfOFeyWDhjxY44aiMfOHLpMTh7oZQ4fx/kOIKOmTiFbfc4ldqFOKpT3ji6pvY4wANaOMaJnDjQ+Ro42Im0OOe/hDjttDk48yBGOPiZEjj8jK45BrNDOQiZXDkgj/45JX4DOSt/lTlGI9M5WTZlOWZSGzlof945aT/UOW5ppTlwcmo5c2Z5OXScODl2iqg5eNAJOX9yMTmYD0M5pTHZOaj45zmvtss5xmJvOcyzLTnOv605zwyfOdF7oTnfpVI55dU/Oeot4DnsXho57vMDOfb5Uzn5RnM5/rJSOgG+FzoCttI6A1RrOgOfXjoG/J06Ct88OhZBfjoaxDc6GxF1Oh2cvjohKV06IunPOi3dpTowgjE6MQ9uOjmQuTpNZ+s6bw9zOnHgiTp57vE6n8L9OqyRwTqtdJw6sfPMOrTVozq1qQM6uTgPOsPmVDrKj1E6zQvoOtlAHTrbT0U65QnAOuUvmzroDOc69yr4OwHT6zsNiMk7DcyqOw9MMDsXWOA7GR2UOxns1jsi4eM7I20nOyY4yjsr4uI7MNvuO0hRDztJIc47TAxlO08zZzte9vc7cSqdO3JzqTt99E07f/e4O4CzijuLUEg7jtmNO5K90jud4J87netGO6OXTjun9Xc7qqeSO7YQEDu3gTQ7ujrtO7rbwzu70Vw7wzrbO8YeeTvG8iA7yjkcO9w+xjvgBwc76ANXO+08oDvyfqA78tfeO/hY/zv72do7/cuwPAGEJjwI9DE8CzVJPA4zQDwPgCw8FFTnPBl14jwbWtc8IvUIPCS//jwlnxY8JbAWPCgM6DwzFQc8OWyoPEFPtzxJ2cw8SoDbPEvU0Dxq5tU8a9zdPGwo+zxy1Bk8dnxBPHdtrDyDgxI8jsSHPJCRizyVkHg8qIZ8PKz9nDyzzAE8u7e3PLzZCjzJf9k8zeA+POKKijzy57E898jQPQm6RD0L0oI9DzudPROLCj0WwA49H2iIPSimvD0x3sQ9MiGyPUgkPD1IKS49TyvwPVFF3D1f77A9YVtWPW6y6D1vrXE9e08jPYOFCT2HK789kgnEPZePMT2bk8E9ov+hPaVBVT2o7hc9qUmTPbj0jT283Ys9wFraPcrVOz3SOVc906dEPdY1qD3WyZE93ln/Pd9kED3k+QE96HYtPfOrzT30cII9/rmCPgYzRz4P0WY+EDbcPh7g8T4jxVU+JXOnPivGrz4s4q8+LpX1PjJocT445c4+PzNUPlEdpz5RRNY+VRFFPlbmpD5ZA2s+X/bQPmcmaz5s9PI+bxNEPnqqeT59DvY+gLo/PoOxPT6QlgI+kUVrPpePvz6mi4U+rDKcPq5ArT6wcmI+ut4/PsWsUD7KEPM+zvrhPs9sBD7hvuE+61F0Pv1i7j8BHOs/DzcPPxT9uT8VuZU/F9fsPxls+z8hxdM/I9W7PyQnkz8mAxE/LA2XPzMemT81TE0/NmkBPzxYrj89fTc/SjFzP0sIBj9NDSQ/Tk8FP3MIxz9zWw4/fOWNP320Rj+BF8E/pAVPP69+1T+9A44/v1P/P8dP0D/Pwfc/0WsnP9Zhlj/h2Rs/9XR0P/Y+9D/5Wsk//v0pP/+t3UABQgNAAzRfQAy0XUAM4MRAEC2/QBCqrkASPpxAHQleQCS4DUAtXnZAL0/ZQC9YkkA29XdAN+p6QDtmbkA9mRdAS8IgQFko7kBaNuFAY1hNQGUvdEBm8/ZAaPCIQGoNGEB5HkFAfTsCQH3mfkB+f75AfviOQICq2ECCLLFAgn+DQIPqjkCEdmxAh6ezQIwmCECMadZAjck7QJtaokCelRlAtwwVQL0AFUDFFpRAxSepQNSsoUDk5rRA90NgQPfJlUD5JNhA/a7dQP4/rkD+eHRA/6FzQQAT9kEAkh1BARQQQQIjxkEHLOVBBz8XQQpFJUEgk0JBIvpdQSUMFEEpOtBBK1K2QTW6ZUE6IwdBPWZfQVUQHEFVgL5BWwCQQWKlykFiztZBZq9qQW0GU0FutPtBcKwqQXqUsEF9QQ5Bf5xjQYHuy0GIDuNBiHNqQYiKE0GNlAZBjuvPQZN9jUGlGABBpTr/QbCOT0GzAnxBuod4QbqrukG+elFB2RCxQeJtukHlie5B7iIAQfhIN0H/5UVCDZcjQg/MY0IUGiBCFnXLQii830IzE31CNSJ7QjlUv0JH0CpCSyJ1Qk+kOUJa8SpCYWSBQmJb/kJmXyZCaF8RQn8wQ0KEmt5CjObxQpDB4kKalcBCnEqbQp9GQUKgceVCo8GHQq7xcUKyZTlCtD2QQsZfGkLP6FRC0BVDQtDc7kLR+SRC5LsUQuY6lELsTvdC70nwQvGbi0LyWzlC9cTcQvwtlUMD9/tDDimTQxCoJEMSNglDE2S2QxSSX0MhbzVDKN+kQyk0JEMqZoFDQhbbQ0irm0NkORdDaWmnQ2vw2ENuSwdDhz1AQ5Vu/EOVp0VDoYUSQ6bgeEPAo7tDzecbQ88uJUPffwND5+heQ+iWFUP2i3xD92wmRAD+SUQGB1JECAQORAjWaUQQ2ZxEEayURBHH5EQViMJEFqVARCE/n0QvaHBEMnOKRDZG0UQ/Ag5ERFo5RETUJkRFKPxEVO0xRFkrTkRZaW9EYKo2RGSUsURvb+REfH4DRIRLikSHD5FEiTigRI2al0SZOM1EmrVTRKB6HESkAwREqMPfRLh48kS+OwtEwZYbRMmzD0TUdidE1TaQRN/IYkTh2RdE6/7SROzgv0TvBzhE8HFZRP6X8kUBw7BFAiKXRRmAf0Udz5lFLVmVRTMjuEU2FNJFOt17RUQfNEVIm1hFTHvaRVK4/0VS0fdFVcdnRVdgvEVXymZFW77hRWA1t0VlAUxFZ8BTRWrRt0VsGf1Fd3TGRYC6mUWCzQJFh5bkRYmZw0WNC0hFjX6GRY67QUWTcTFFmAk0RZjP0UWiHktFoxryRaiAyUWyr7FFtySYRcBztkXDAZJFyFhqRc2QQ0XOEg9F0H98RdHy0EXSz6dF18YERdkOf0XaEMpF4Y2tReeOGUXozhdF+hUbRfuSFUYB4flGC43eRhAXREYQuI9GFAoeRhR2WEYa/XFGIK3mRiZna0YpX8tGNNyqRjeIfUY9bhFGQU7uRkTE1EZVrXJGVlraRl6hwEZkvC1Ga8jORm5ZBkZuwqpGbyTJRnxFXUaNoIRGjbA8Ro7ly0aQ00lGk9hRRpTGCEaZg/lGqAjPRqyDOEa5fmlGwW37RsgMQkbI6N9G0IYARtNQ7UbVUdpG3MuNRtzU3Ubd7zZG4aqpRuMaykbj13JG5QXJRub0BUbtIV1G8p+YRvOPLkb2M/BG+uyzRwXRRUcGGDhHErA4RxLNlEcTWcRHLaK5Ry3Hc0cwGjpHMbNlRztvmUc8LQ1HREatR0g3MEdLpntHS7ejR06X0EdO6e5HWEutR1p040diojNHaZnQR3o2wUd/vYVHhi6WR5JcTEed20NHo5rsR73YMUe++gtHwLmQR8HcRUfDvdJHzG1lR9mWmUfaGSpH3dD4R+aBgEfo1DxH9iZsSAWODEgI7FlIFtq/SBkC7EgaFV5IISTUSCIlakgmrBRIKbrsSC+99kg4gP5IORzWSD+szEhBPdBISATqSExMSkhhdBJIZMy0SGtXWkhru+NIdUP/SIJFC0iOOZxIlF6hSJr+Ikicj9RIo2YdSKXFJkit3gVIv5NnSMYfIEjKEIpI1H+jSN0UlUjnfW9I7LGSSOz9zkju5UVI78SFSQUo80kP81NJEI7/SRO9Q0kjx4NJJ5waSSlMkkk67HlJRy88SVKfZUlVdCtJXT8ISWO7BUlsN9dJeQ+4SXnWGUmASORJh2lbSZa8eUmZkVpJoEukSaEZwkmjN1FJo4MvSaRqf0moe1lJrmT3SbLz50m27X1JzniRSdHpU0ncHLBJ5C3ySeoNQEnsustJ8ldBSfKxjUn2l6hJ9z+hSgKBEEoHOQJKDN5xShSw0EoVTv5KGYEvSiHUiko0h+VKNoCTSjvalkpAZdlKSPRJSk38/EpOFI1KTyL7SlFG0UpSZGxKW1tJSmA1wkp3+TBKevWiSnwDg0qCy21KkF1+SpiKsUqY5mBKm5VkSqAfh0qoP7VKsHFtSrVwMUq5MYRKviVGSsQU9ErGxrhK0ReKStUyikrVg69K13oVSt7X5krfpXlK61nZSvNt0ksNjRJLK7SESy7jWUs76GFLQNj+S0sEUktNG7NLTnOUS10Qx0tehL5LXr26S1/cbktlkp1LaWMnS2uQv0tsybxLd74vS3+tT0uBEhlLg3PQS4z8EUuUpv9Ls8R5S7TKdUu/LdxLwx4IS8VB4kvNBnlL1DaWS+MNmEvjNy1L6q1iS+ti3kvvvftL8bM+S/REKkv2bCNL/gKdTAXx/EwNJGlMG1JATC9lmUxGvHlMUAmtTFGzpExqq2NMhZxCTIXkYEyQBSpMlmTSTJlKdEyags5MntbcTKy1skyudoFMwZqvTM1DV0zb9mdM4MuhTOVcl0zuGk1M9bxZTQu2OU0PtHVNEx2mTRP8xk0b16VNIR/6TSfq5k0uX9pNMrvwTTtDwE071KRNPQqNTUkvSU1L21tNVdDJTV0VH018RqFNi01uTYydDU2NR1lNjzXpTZAStE2csqZNpUhRTaZ+002ywd9NwhYlTcIyoE3GQpVNzEFzTdArfk3QkEBN0mBQTdxdhE3mnuZN6tFqTfKfh03y2k5N/44pTgZ5J04HnQVOCckcThE9RE4XpEhOGCc3ThmbSk4eD9ROIRDiTimgKk4rxHpOLfUCTj4BuU5J2FROWCMVTl5m20542sVOewiVToYUCU6Lxp1OjO7ATpFHXU6ZO7dOph+UTqeNCU6oQshOqHLfTrEIZ060kj9OtnDPTrehdk7D88xOyETaTtC0vU7fGd1O7xTtTu9y3E7vc3NO9lu+TvdidE8KpS1PD7zTTxTAj08dJtZPISQ+Tyaur08pNahPK2nvTytvRU86wGpPROLPT0UTGE9LCe1PS5QFT01co09RQttPVxgdT1gs+k9czSRPaN/bT25sx095zEpPfzWMT4FIaU+O8IlPkK9mT5bupE+geHdPqDevT7NNek++SkZPv7NTT8UEvU/QKq9P0GU8T9UF+E/mp99P56kNT+yWrk/wU+hP8Y8AT/GjPk/4iq1P/PMhUAOghVAJyOFQFi+AUBq1RFAfSlpQJH1hUCZI3lA01HpQPMckUEEB8FBCHBNQS8DdUFFo4lBeg2tQXqvfUGltv1Bte0JQc20oUIvplFCNNmBQjf7PUJAWcFCWKh9QqViaUKt3GVCt3UhQr5I8ULfvpFDELgdQ0tUiUNhkkFDYtKlQ2v93UOCw+VDz8BxQ9t2RUPdTKVEMf0pRDXS2URbkBlEX+NtRJCPLUSxYXlE0ZD5RP43pUUVfzVFHlsZRUkHTUVMPQ1FTOq1RXdkZUV3dXVFgkVZRZia/UW+j/VF70ABRgwgBUYfaC1GJ1pRRkXmVUZdonFGbw/BRnnhZUaCTCVGiyXRRpLE7UaodklGruWNRs6a3Ubw9PVHEUIFRyBMYUdA1x1HoIsVR8UYrUfIJz1Hy09VR+DaqUfhW+lH4sfpR+Qw4Uf7l2VIA9NpSA+RvUgV25FIPSCJSHheNUisnalI1gD5SOCmXUj/ZjFJDzKVSSl5VUk8Su1JUeS1SVq0tUmCwvFJtfU9ScHKlUn3Gh1KAkzxShb2oUozvh1KOw5xSkVpHUp1fRVKjQM9SpDFVUq2gklK5D0RSvUVYUsTB51LGbhJSzed4UtCB71LTZj5S2aLMUtuQV1Lh4TlS78tsUvCsn1Lw9T1TAPRBUwI8YVMCqhtTG3rMUzW4FVM6s1pTOyFzUztDZlNDp4FTTpktU1D2fVNkn25TZ8BsU2pxmVNsCzNTbBvUU3PlzFN9i6JTgw5AU4SSWFOGwedTpWh8U6ZK7lOquMdTr7prU7ChslO2ygRTtyL+U9LuPlPUMhJT3dKyU982n1PhHrdT6iISU/PAuVP2w3lUApYcVAU6elQOGBRUEFNeVBmDCVQcxylUI0snVCRqgVQmqLhUOBWyVENag1RNARpUZpVHVG7pwVRwcZ9Uc/iKVHZEklR4WzVUefL6VJFM+VSRwR9UnGyoVKPtClSoRF5Urbx2VK7FylSxzfVUtbTrVLub1VS/PcJUz2dbVN8uhlTobfJU6pvpVO+qZlT7/FlU/OdNVQBUxFUFHyVVBYzsVQYQ9FUQfhlVEiDeVRSugVUVVkVVFqBoVR3NwVUgrAxVJNFnVSUColUopcBVOeVEVUXZ51VJqhdVSgD6VU0CkFVNvwtVXQHdVWkyKVVrXnVViLZIVY37zVWpfqFVs0DpVbWg91W/6rpVyCVxVcirIlXLYkBVzZISVdyK6FXedS5V48ToVfI7qFX2c7xV/BVGVgJrmFYHDBhWB1GEVh0WIlYgSaVWIaCxViQa6FYlQM1WJZ3RVjHG1lY0zM1WNM0yVjhk0lZF8C9WTXWDVl7pD1Zjv1VWZTMbVmeOa1ZzBBpWdedoVoCV7laDXuNWhJGZVogrvlaLFWBWkIY/VpKMJFahU1xWpYCtVqdjW1awnp1WtegTVrssiVbAYIFWw+m/VsQlk1bMliVWzzw2VtHV8lbTktVW08lJVtlhVVbhTWtW46i8VuzgFVbuiQJW9GKnVwBDWVcGTxFXB9hsVxKgvlcUtGpXFUOGVx5xZlchqhtXKYK7Vy+9DFcwJTdXMs6sVzaJT1c7drNXRJ+RV0VuCVdJVwpXUChbV1BuYlddgl1XZ7GLV2m6c1drjtZXiFyLV4nP1FecimBXnbfWV58VpVek58pXrXlkV70CV1fF8uJXyi2tV8z0vFfPraVX0F+lWAALj1gByLRYA3wAWAbpQVgRfiRYM8OSWDrcilg9IK5YPctRWFIn/VhSnOZYY5c+WGYww1hpL9RYbUyRWHGtwliAu4RYgOEEWIH0uFiHJRRYh5XBWIhdOViNYr5YjrY+WI7SEliQBeJYkgt0WJMdzViZaWtYrZgxWLZt/Fi+nixY0KubWNSImljX+9lY2S6DWN/OtVkDNHhZDfvCWRf2rFkbUXtZHDslWRyyf1kjImFZJIxNWSZ9Z1kp5YpZLlv3WTW38VlA4xNZXtkDWWGPwllnJ8hZaXiCWW25ullwU3FZeHs8WXkibVmE2/dZl2cEWZ64Ulmnhm9ZqYuJWanDYlms1CBZsvPpWccQdVnIJvxZyaF+WdnTSFndrkpZ32cjWd+uN1niIaRZ5HISWepK2lnsZB9Z7XSJWe/WtVnxwwRZ+5l1WgC/1FoBXaFaAnQVWgkt9loPFGxaFQTmWiO6N1ooJPdaLUFfWkGUUlpEuTRaRrglWkdEY1pHgCJaTafyWk8mslpTJgtaVJE4WljYSFpdeklaaLL9Wmp0mFptuCxadtmlWnl97Fp6KGJaemoYWoBMm1qAThlagM+OWoEfhFqEIL1ahJlfWpmQblqk2wVaqG4FWq+JBVq4YGFauIzfWrpDd1rAHXtawJC+WsFzOlrXrJRa2T1fWuOnQVrko1xa54M8WufJklruurFa+cOVWvqumFr8Ndxa/ldeWxWv1VsXrclbHVsVWzI1lVs2lE9bSwZ+W1Dz5ltVFd5bV+JBW1hDOltlg9ZbcV8dW3PaeVt8T7BbfxafW4ium1uM4jBbkFTTW5R+aVuZeSZbnTy9W5/lWFuhy0xbomiNW6cnMFuqYeRbrEPAW61rLFutejdbvVBGW8GCSVvEeclbyB5lW8/gDlvQW8lb1g+uW9n3JFvc08Bb4WkeW+Q22VvpNIBb6rFHW++wdFv4Jklb+ZuUW/oQcFv6aG9b+3EWW/0IvVwFd1xcB9HkXAgUCFwKT8dcDhgkXBEvd1wX+gNcJn2PXCnyuFwvpvJcM2R8XDhtP1xFoFZcTtd1XE8VSlxUMX1caCwtXG2e3Fxu9PRccsmDXHPxclx3dQhceWlpXH0oPVyKehJcklY7XJOtBFyWiKVcmVu7XJ9zr1yifnVcpTuPXKmJAFyxKdNcseYkXLq9Q1zFBztcx7w9XMncf1zPTqtc1YjNXNuwg1zpTPxc6cTfXOwXW1z+I5Zc/lFyXQfPXl0OuX5dED06XS6k9F0xj05dRKewXU8asV1SRD9dUl6FXV/wFV1hfF5dZrlLXWcMc11ww9FdcflPXXQKAV10rgldeJfaXYYAN12GV6NdjfmxXZXLJ12g4n1do0qqXaxfI124w75dvdEAXdOrFV3U69pd1ssNXgfjoF4UYRVeG8kNXh28r14hkoteI6hHXiQoXV4ul8NeNcofXj3Zg15Pl7JeVn43XnpdfV6EvPRehTsmXpvTKV6ncjJers5fXrNgqF60Y+5euWXdXrlxLF7GEhReziQAXtJa917acTxe216eXt8lel7gfW5e5+77Xu44OF7vge5e8CXrXvCfm18EM5hfBKiEXweZgV8KOydfE2EEXx3jQl8kd4JfKXRaXyl5L18tbWBfM6N1XzWrOV85k3NfQ9J2X0UFDV9QRDtfUKhMX2DO419n5j5fdfvMX3ZTUF94cOdfhdaZX4YFaV+JaF9fifybX5OVw1+aQSVfoVWBX6Kk21+jObtfudGLX8YJQl/HYXhfx+OLX81gzl/STFBf0qNuX9xn0l/czwlf3znHX99tCl/oLAxf6IQvX+4AI1/7YJtf/tqXYAE87GAJgs9gDJqpYBDHKWAYwdBgGsPjYB8YiWAh5EtgKW89YDSIFGA2INhgO4UFYD5pB2BKfzxgUJmZYF0R5GBlY6Zga1WJYHUzg2B7y4Vgf1a8YIyycWCOK0RgkqAyYJzoomCjEbFgoxYpYKSIRGCl3uBgqNZeYKuQ4WCvrONgtpMyYMWWOmDG0ndgyQXbYMzaj2DQCpFg6lVBYOt+X2Dx/b9g8gwAYP1ckGEAk1FhCeliYRfkWmEcNmphHEe0YR1qTWEfnoBhIikZYSVT42El/MphKThBYSpR1WEsbaVhLZ7DYTD/ZGFKzBRhTgCmYU8S+WFRk/lhUnC6YVkj2GFfllRhbiBiYW4mKmF0qG5hdo24YXj7l2GCkulhhI2iYY3N+2GYW/BhmQKoYa2dFGG3yP1hulUcYbuNKWG8LOdhxBKMYcmysWHT1uxh40fYYeYrIWHn3Ylh7LYzYf92wGIVZVZiHmG3YicSCmIpo+RiM3rvYjxzpmJB5UdiTCKoYlYAI2JXtVdiXxOdYmzU7mJvSNJicMHmYntqLWKFJxtihxDeYodjO2KJEu5ii1csYpllAGKbO6xioBNyYqGLiWKlamRiqGmCYqvEtGKzeERitIe8Ys27VWLOAPFi3QjWYuLJEGLlLSpi5+NVYusNsWLwtgBi8pAuYvMMSGL42tVi+sL0Yv3VHmMIcmxjCvVJYx4HxWMgsBxjJgERYykxw2MqhgJjK6QrYyzeW2MuqShjMpVCY0Np3mNImthjUkAUY2U6SWNnxI1jaMm6Y2jxWmNqntZjb128Y3GDymNzBQpjdQKbY30fXGN9jFxjhCxSY4RuC2OJItJjk7zfY5YYyGOl/TtjpoJdY6tPG2OriWhjq5EMY6z2jGO0kshjtzRNY8pHAWPWLUpj2ehBY90KNGPltw1j7hoHY/bYZmP8iABkD7BhZB+FbWQg7U1kMYGCZDH1d2QzHZ1kNWvPZDX2g2Q/7FBkQsuBZEhQy2RKx59kUXp6ZFOVp2Rbj7pkYSB6ZGNP0mRjVDBkZexIZGe6o2RpyuJkc9vZZHswQmSBSjtkgbNQZILojGSEDxZkhoq1ZIdfzGSQ5n5kkS1WZKi/9mSybrtks8Z5ZL1ZO2TkJCZk6g3HZQKJd2UEq3xlFvExZR9dTGUhGtllIfwlZSL8lWUl8uZlJ/cFZSr0DmUv1BplNieBZTwtsmVCLpRlRKHxZU2mqWVXNdBlXdkqZV+DvmVgHShlZ2htZXGr/mVy6c9lfObCZYuz9GWPjwplkcouZZMZTGWTvspllmigZZbsHWWeHLBloQnpZbAjWWWwmOllsYjFZbN6m2Wz3SJlvu7WZcJramXOENFl1Ye2Zd4jiGXiHqBl46QiZfPYMmX8MR9mAoNiZgKlpWYGCbFmCIXSZg2Ad2YPV6pmEXClZhMNu2YdGHxmLVmHZjCLe2Ywyn1mM8dkZkNThWZFyGZmS6KDZlYBimZZPmFmXOKbZl0cIWZiFlNma+u5Zm3SamZ6JbBmfgzmZn5iT2aD/C9miIEDZol+kmaL4Gxmjb2IZp34C2aefXFmqhy5ZrA/C2a3hthmziStZs7WlmbSB0tm2p87Ztvnk2bf/JZm6p8zZu1Fzmb3nYpnBRpPZxUyfmcWbJJnJZhqZyXJ4mcq8jpnN2CIZz2mwWdAJ+FnRIRXZ0X6r2dRMZZnU/CEZ13GEWdleIJncQbDZ3+ujWeAUGlngsg9Z4Zqd2eU0htnnB0aZ6LCPmekyExnqdQtZ7X6SGe4tt1nuM6CZ7k2eWe6BR1nvEBLZ73BD2fA/Mpnwaf+Z8ZhPWfXRuln2WdMZ90yL2fg4ipn5BXNZ+XjMmfy0Atn9e78Z/nipmgCJuBoDQtsaBEbLWggEHJoII32aCNoBGgq9WVoMAUfaDN/fmg6yoxoQT+0aEh7hGhQehNoUbOYaF3xgWhfbwxoY5paaGZycGhmn+JoaXB9aHABCGh3gjJogBZraIgMG2ibdHtom4x+aKFRHGiqlnNorB8paLjr/Gi5EfJouwS9aLt10mi8cCVoxD4QaMRqYGjJW25oyf2yaNAz5GjX5DZo4w0OaOwZF2jtdgNo7pVBaPbB0mj8mVFpCIsBaREKiGkWltZpJV54aSh2aWko6ElpLUZ0aTIWzWkyhBppNBBaaTZJCWk8qsJpQCAoaVanv2lakadpXUEbaV9KcGl1o/VpfUK6aYT9GmmLgsRpjcMbaZgqGGme/J1pwMQgacHQqmndDRdp33m+ad+W2WnrZfJp9jcEafy3S2oFJxNqCNYmahHNLWoSdTRqEn2pajE+k2oxRyxqNwJoaj9DiWpBwQlqT+ixalPWGGpXendqW5Z9amJBW2ppIRZqf+/9aoIFqGqCeilqgzhEaofAlGqH7AZqkK8SaqreYGqzrblqtO3pareNFmq5491qvHQ7asjAm2rTqUdq1Hk0atgJk2rZPxVq2fglauWmI2rmOO5q8rtHave7GWsFu+BrBgxGawgBXGsJcHBrG1QlayKQSmslkWlrLfcZay6U2GszpN5rN5m+az5ExmtD5shrSfXva1Rlv2tVYuFrXJT8a2Je1Gt+rHZrhetRa4YQnmubAZlrnjeTa6J6xmuoCXdrtXx+a81dlGvQZ2dr4xcja+XonWvmm7Vr9uHQa/oIvmwHT6lsFfcxbB4GKWwiEqlsJqmWbDarM2xL+0dsVYA9bGFvfGxjE8RscLLVbHHYkWyAt4ZshxYHbIoN9WyPHE5sk+VNbJc+iGyi9UFss4wObLYFsGy/vEdsy02qbPNHVW0EpmhtDru9bRBUOm0ScLBtFTEObRmaym0ikG5tJEeLbSgEjG0yEd5tO5bQbUBjWW1H3BdtWkWSbVwcc21kpoptZ3rQbWh9j21q8LFtbjBhbXRxQ2182bttgB+ybYzYJG2PpSFtrhWwbbCttG20Fe5ttrpEbbnDbW29dNdtvfX1bcZp8W3nOC1t783Obe/sym3ybd1uABLFbgOSTG4E8I9uGPlJbhpDjG4q0i5uL55hbj9a125MoNxuUFu5blrN4G5kQaZuZr8Cbmz1em6ho2VusZm1brlTL26/N/VuwJ/rbtMu327WLqNu6vrvbuvMvG73xMFvCw/gbxtovm8pUrxvMY1QbzOwjm83M+dvQz5db1jiMW9bAVhvYOUSb2wz8G9uaNFvbr0kb4J3bm+Np/xvkBmlb5Zblm+Xr8Nvpfu8b7JiOG+4jAxvzwClb+BvjW/qLqJv6tkKcAPIlnAFwqlwBfxtcA7cfHAWd1twGzibcCdVh3ArePRwK5vPcDUvQXA8UYpwPxFecEDOM3BLO1ZwS4XkcFkTPHBz0PpwdTLmcH0UkXCEL31wjwTAcJdXxHCbgo5wpMyecKx2KXC43LlwubMRcMER73DIgdRwzNkAcM/q6nDdhA9w6LbhcPEy/nDx7wFw+XF9cPl8uHD9Ke1w/8KBcQXbNXEHcmJxEO2kcRxz9nEyavVxQkIrcUhfDXFIaGNxSIoScUwyWHFO6g9xXxrgcWGiQHFj0oJxZ+CmcXAbynFw4zpxcP1McXPkHnF+OMVxhS19cYxJN3GNR/xxlvFQcafKhXGyEWFxvf/ZccMfbXHcvNZx427Xcemf6HHutS9x860TcfSXeHH4fWxx+OeXcfo9anH/u2RyAX4zcgnhLnIQ0RdyEf+uchKp4HIY2ItyGkW2ci3NR3IxVmVyOUbvckBjJ3JBwUtyWgV3cloncHJfbqpyYRVQcmPWeHJkjPVyaRS5cm0BanKH1Elyh9vJcoiZu3KI7dBylMCIcpaMIXKlsmJyqVAEcqyiFXKt84BysDQycrBS3XLAdllyxb3pctBUx3LVGR1y1iivcuVUOXLmNsFy+eZhcvrSPXMACa5zAHNdcwYZQXMZHYZzHRHFcyHlQXMmKtBzKdj5cyqU2HMtGOdzM1wiczTOf3M4dqBzRK3Uc0ThhXNINblzSK79c0m/mHNN6ZZzVOQ9c2DXdHNvyrRzeZUqc4bL/XON3otzj78mc5EryXORw7lzmC6Yc5rvSHOcqeVzoQPFc6KZDXOrp8tzrb4+c8f273PMM7lzzQAWc81C53Pae7lz3wYLc+BizHPhK5xz7klYc/BB9HP1Ycd0DjzbdBJqinQTGkh0JcYhdEM6aHRGAZp0TPk7dFN9NHRUEqx0XF/DdF0UpnRjCyB0ZpxIdIrWzHSOFkF0lEDsdJYibHSfvEx0qHGsdLUY7XS1LLh0uDH5dLqjvHS7vvJ0xzvadOT1snTxPsR0/8h0dQBPFHUAY5Z1BdZKdQtTCXUP7i91EFGTdTKO9HUzpCR1M9CHdTmyUXVBvgF1QeSodUPrVHVQtnJ1VMUYdVvYEHVdOk51Yde2dWQNfHVoVJR1aGiOdYH593WHBLJ1iq08dZJuCXWXMKl1oD/CdaChyXWkBsF1paVmdaYyKHWoHLJ1rGDldbKYpHWziiJ1wFM3dcUR43XHZoR1yyo6dc7VknXUX3111MmwddYlIXXX7pp152wIdefhGXXrQYZ168s2de+frnXvsUV18e1RdfNx2XX6gt11+2XQdgaq/HYOfat2ELrodhR+3XYXwpl2F/ZUdhmchXYeiR92Huhmdh/ak3Yhod92KyHydjJFrXY2Gzl2PBiedlJzNXZYLcl2WxYWdmZJd3ZoLz12bL0mdnATznZ2YQ12erS9doFpoXa2Llh2tpe7drfULXa39Jx2u4YCdsHaCHbCQ2t2xYDwdtOf93bVv+R22Qh2dttVWHbkH2l26ZjEdurBU3btGhV29G5KdvXkA3b9lvt3BE23dwVWY3cHPxB3C3V4dwwnR3cToPR3GWAcdxmNjXcgNaN3JqVGdy/CXXc0bQR3Nh/Kd0HMvndEf3d3Si/fd1PYm3dbOXN3W6RPd1u5YXdc04Z3ZmPQd3Yu8Hd5i893fovXd37t/Xd/Frd3hABad4XbhHeIemd3iUdzd5lpRXec5Nl3qVZId63UT3e8mpl3vOn7d8zPDXfmqbp36T5cd+2r+HfueBZ38w3id/yJEngCsSJ4F8UreCM6HXgjZh14JI1ZeC+bEHgyB6R4PT/9eD13c3hOkkB4VLOzeFhIlHhZh2R4WtLseGF3snhiODp4dM7beICXDniRjcp4kbEheJtJYHijcdZ4rprqeK6fbni2hU54tsaneMZ8EnjLx3942PdoeNrcV3jpK+F484QvePi7THj7qeZ4/eL1eQCkIXkRjd95IL/UeSiDxHktKDJ5MV+feTX71XlA83V5R+kueUhdsXlL+ld5USdteVzPnHlhszF5YySIeWN6D3llpmV5ajq5eWubdnlyDbx5d427eXjVlnmMyhZ5lGeAeaxeNnms9TR5r11JebMzyXm2hPt5vidoecZ2b3nLGCJ51aKGedsOQXnblyt53NLled1c3HniR1959tRkefuG4Xn7weV6EGP6ehkCKHodui96L4VJekck2XpNY7F6VaiMelfYiXpaMed6e4jweoL1C3qGsVR6lb72epp4P3qgIPd6oSlneqEp9nqiFHZ6s6i/erUV0XrHjdl6zRV6etNTHXre1bx65zHreux1lnr3jJF6+qCnev3TPnsEkHl7CebbewqrWnsKtFp7DwI2ew8xw3sgLVd7IYSKeyHd5HsjKZd7JBLXey4pqXs00917NyWeezwCL3s9eDN7Qbvre0mBcXtmX/V7ah/De21bMntwdPx7dmSBe3jyeHt/zHh7gne7e4fg9HuJif97li4We5ZM2Xub57F7oxLBe6cfrnup30t7xwvHe8y6SnvRqQd72rhfe90WHHvnfxp78poze/pvo3wEmUR8BSBNfBL39XwWDox8HW3hfB/On3whFDN8Ii+yfCLvh3wo+WR8LBx1fDYHuHxHXtx8SX1PfEn3E3xKjQl8XFekfF0Aq3xgkD58ZMqUfGphxnxqiKV8cRHEfHuE7nyAhQZ8ipYefJIZZXyXzid8n+aDfKItRHzAjBF8yRj5fNlOOXzeS1t84DWffODsi3zkPbd85oNvfOd2zHzoJ3x8+7k2fP6uBn0BbDx9GV5gfSRnKH0pzXB9MfEafTvPTH1F62l9VJrLfVwqLX1dv4d9YO3mfWNGVn1mZCN9com1fYlMtX2PS0t9kNWofZS82H2WKgB9oz9JfajaP329Rkt9x0bffciN0X3abht94/8Kfe542n33JqV9+lkQfgICcX4E03x+DgxAfhOixn4WF+5+LgU/fi+3h34xLZ5+Oeohfj+12X5Bxkh+QyvjfkONwH5E9/R+S+c5flEyRn5S5b5+XMRFfl7hkn6ES+N+ibgXfqNdgX67Jcx+xffgfshjg37PelF+z9j5ftChu37TJNB+8CP0fvVZMn720jN/BgT+fwhxCH8Opxd/E0XCfxNlI38abC9/IBesfyIxvX8r6Z1/L9oZf0Rvcn9LUuJ/TlCsf1D9Sn9uFe9/c6n7f3YZ3394ibp/fbndf37oCn+LZ2p/i2slf59Um3+hIg1/sKQmf7QZ0H/HP1Z/x/nnf8ru73/PI7Z/2Gslf+qO2n/3ZC+AAzXugAwQVIANYtiAERmtgBNt6IAoO5GAKl7rgDOn9YA0xD2APkxBgEXwEoBKdBqAT2VhgFQKRoBe5myAX06GgGCv2oBima6AZV2ogGyD4oByDYSAgP9fgIMg/4COYA6AkCAKgJgPyoCkTLqApWqpgKmnP4CsKoyAsk1RgLojmYDMOrmA1zl0gN/ARoDhJmWA5VwQgQOrA4EEuh2BBukPgRjq0oEcHEaBIiSRgSn1aYEzrEWBNqPSgTjfVIFCeoyBQ5XKgUSP4oFGh32BT/kMgVBuYIFQsziBU8zIgWRoOIFluwiBZuqGgW5f/4FySPuBd2rogXfbKYF46iCBfTHAgYHWmoGJw6eBlBrdgZ1n5YGdfBWBnqCygaIqoIGpL0CBq7dogbBvrIG3gdOBw2SpgcXUm4HOu56B0Mb2gdE9o4HZruqB9wXcgflzGIH+i/6CGjvfghuGO4IsLI2CP6/egkPY24JFZoKCSvj1gk7mioJPOASCV6V3gmRg54JwwRSCciWPgnuCdoKEhhWCh4CegowaF4KT26CCln7FgpmFa4KcOASCnbZkgqqbvYKu9YmCs4TOgrrEz4LFJheCyUyBgsxOHoLM6nqC1r+ugtoOh4Lhn6GC7rofgvKSNILzjaiC9M6ugwerNIMKB/CDC/FQgxhFhYMyl4+DNJGNgzbkDoNBhOSDQ7jtg0s08YNNC+qDTSvIg1QzYoNViYqDWBn5g1knloNlWlWDbLP6g3h/BoN6/caDiHuyg4wmhoOTUbGDlhYJg5jrmIOfHQWDn8CUg6FPf4OyQwuDuERJg7mWrIO/jOmDyLrpg8mGvYPMh86D0SCng9YxHoPeBh+D4H6sg+jO+IPy3XiD+sY3hAwiwIQQbW+EEQmwhBEKQoQSZN2EE7hZhBRiXIQbfVWEIZBHhCr97oQt8OKEMDgPhDRhNYQ73eiEPE5PhFG6ioRdKJmEYOJHhGFKmYRiUYmEY4IlhGgnX4RpHLeEcULmhHHPwYRz19OEfQqGhH31n4SALcuEiDB2hIo8sYSSpR+EpA7jhKr7hIS4A6GEubEwhL6xiITCBYmEwtUvhNEqJYTShKqE0+9FhN5nU4T+KnCFB0LnhQ1dLoUSe2uFE2x5hROWGIUZmTGFHhOmhSkme4U1IMCFORHchT/ix4VIj0OFVFwMhVaLIIVY+zeFY+mYhWlAz4VsUB+FcrgBhXa32IV3fAOFeWVLhXoIVIWCiVSFiVKShZOIDoWU5dyFl8PwhaCCT4Wg6gKFocz2haPNiYWloDqFpehHhasl2IWsI7mFs13XhbXuQoW4ITOFv5gChcEtf4XCdJKFxM9khdDvgoXRy1yF1cjchdfQEoXY12uF2pkFhd00Q4XdhMiF6MAohfB494XxW8WF8q6ihfReFoX5QMeF/o3khgKdJYYLMumGHE9nhh4nYoYigdSGMQ9ahjQHrIY1yC+GODIghjvjKYY8w3SGPa4ThkEd1YZkIa2GcfHFhnYbY4aB+52Gi3Y6ho0JkoaRJo2GlVqYhpkLpoaZP0WGmVoJhpvRu4ah2saGqD0whqjC2oa3YaOGuI10hsnZ7YbKS5SGy7+lhtkxL4bdYbmG3anahuzcXIbxQU+G83a/hvcpTIb6UjOG+rVXhvrrPocBejCHDEd0hw8b8ocQEs2HEHG+hxYlSIcmTcqHK3QShzsvdYdEkySHRkGch0j4XIdPXjeHVKuLh1XmCYdWcVSHWg4Rh2SOD4d+65mHf28+h4AAp4eGcj6HizTHh41FNoeTJISHp4n0h6zDoYes7BeHtDQ1h7hDYYfHCguHyEFKh9BXa4fReCWH1/OTh+HyIYfo20+H9A7KiABXi4gTgZOIFOEEiBxud4gwjWSIN2i2iDwKQ4g+MGeIQ9f5iElQoIhMfAaIU42miFUPaYhgtGuIZhariGfIi4hv7QGIgzr3iIknQoiZw4CImjieiJsPv4icaFOIobQIiKSPjIixwvOIsfiSiLSWyoi4X66IuMFaiLn6E4jE8oaIysswiMrR0IjK73GI2QIZiNuSkojgiiCI5gSJiOm5ZYjp2hyI6jlDiOvov4j6hG6I+s7ciP3VhYkEZfuJCXnDiQpJLIkQgfSJEh3JiRKf3okT28aJFF/OiRXMGYkaSsOJHF/uiRzeIokeB1iJLZaliTQU8Ik5ItaJOmpniUqKE4lbMXyJXze4iWQukIlkr36JZgYxiWnLsIlw6rKJdSQ1iYDeIImHI4qJiyzIiY2Z1omaGbaJna3/iaKu9YmkqsKJsduNibeJMIm9IfiJwjydidG8V4nR54CJ1RElidrLAonkleeJ7yQ3ifCR5Ynw76GJ8qvcifYimon7UR+KAqs3igNQNooc6l6KKm/Sii2gVIow/KmKOCMfikcKjopJ5WyKWXceimXAtYpmrSuKapmnims8XoptewiKcwp5in2nmYqJwz2Kje7Uio4IJ4qQNTOKlpkTip3V4IqkAAGKpwgiiq7gVIqye0yKtqigir14foq/Fb6KyhA+istZBYrOv6+K1L1nitdHQ4rZIqSK2aU7iuw+tYrtEyKK8uOgivWhnIsQm6GLKp1HizlLMos9qMOLPxZ4i1JBx4tg6deLaGGFi2moPotxlz6LcvZji4EnwouFSciLirUWi44kOouOktGLkkiki5hJqYugY/mLsLl2i7Tr1Iu8hsOLvmU7i77bQIvF3oOL2F1Mi90lyYvgdRaL48lDi+k3fovpvCuL6kiki+wEBovsYxyL7Ncsi/Cyg4v3YGaL+Fqmi/iff4wAxI2MARFGjBCOF4wZEBaMGa4BjCWAhYwpyZeMLFsfjC36j4wv2ESMQRV6jEE9EYxF0JCMSDzKjElH6YxJn9aMThFRjFcqsYxcq+OMYAihjGVmN4xq4UKMbfQQjHEEhoxxnnOMcrWOjH0FEIyCnuaMhJDOjIf7g4yJ/MmMinOOjJoR8YydkCmMoYgTjKbirYyqzXmMrejGjLIjfYy0VIyMtl5jjMtUJ4zPMCGMz7jXjNKTX4zW4hiM1wUzjN3rRoz9oLGM/kH6jQV9q40eHmqNH66zjSd/KY0v6KeNMiZ7jTiYHo09ro+NSxmbjVAEyY1W6SSNYeCAjWNk6o1jkmKNZ0fJjW40+Y1vA8mNcFD/jXYw6I2syz2NuqE2jcfAk43RP/SN25QjjdwBYI3hIoGN4aQcjeVdKY3l8piN6dTwjfWaXY36MOCN+8c5jgs+pY4QgEKOGCUGjhsSS44lNBGOMWifjj7p1Y5BpgGOTgQIjlH1345TzJiOVn/+jleNLI5Xwt2OYYKAjmJ6Io5os+WOdWyfjnj4XY55BgmOe/wDjoZPaI6MEAWOnEOTjqkv0I6/YB+OxJgRjs5E147XLg2O7HvEju5bhY7wNDSO8joZjwiBOI8QFRePFOO8jxZNY48hdMiPIvywjyPHgo8uXXCPNGLxj0OZoY9F2Q+PTj48j0+fLY9XujWPWgkbj3PNaY94f0WPfSH0j6ijwo+yiu+Ps6yKj7suv4/Am5GP0oxGj+Zw/o/rvTuP9auYj/2bNY//t+uQA3HFkAVRm5AJM3yQD38CkBc0epAfPNuQIl78kCKH/ZAkzoKQJ8xakDT/npA+EcqQSOrZkFVJ8ZBYcqqQYHIAkGL/T5BmaLaQbxfTkIe/YpCOvN6Qj3BMkJVUKJCc2IqQvEhekMghpJDJ172Qyf//kM8W1pDQFFKQ1EQLkNzEQZDnquaQ6Bv9kPODmZEHBsmRDNMykRTxcpEZA0WRIFgOkS5B4JE0wyCRNnHBkTiQ2pE77MqRQFl3kUnBIJFOzoyRVe9fkWGyaJFm7v+ReIPckXr4xpF+r16Rf/rwkYxF7ZGV+HORl54KkZhjxZGkhPSRqMOEkcAIqpHBBhSRwV/VkcgQ0ZHZxOyR39ndkeOOY5HlwTGR+1wTkftkJ5IAXs+SA1Iqkg2iKJIRniySFGF9khc1nZIZVLWSHGLdkh3FQ5IfIIqSIishkjO29ZI1r9OSQp2CkkZFs5JMY/aSUd15kmOqzZJopeiSbYJBknKF+pJzvzOShvqUkpbko5KZsqaSnTuikp5wLpKiMAKSq4GGkrYyCJK7ApuSvWzcktz/oJLhsl+S6RsykvL9mZL5aUaS/EcukvyVsJMAZPeTB2/+kxLREJMbd+qTNKVUkz+GjJNKrkmTTCvrk02BYpNUQACTVySjk1mk2JNaj7GTZ3Qsk24oTJNvOn6TdASyk3tAcZN7+uqTfkPdk5dgZJOkPcOTpFcLk6S2cJOmb0qTrKUWk7Sel5O6FgiT0DBdk9gy55Pm9eGT6g5Ik+xxspPvXd6T/aiIlAZhnJQT7nCUGLbZlBltHJQZyzmUL8CDlDiNwpRH5ziUUWBKlFFyB5Rh6tOUZAiYlGRm9pRstgyUdF30lHtw+JR8hE2UgWWMlINZbZSE+5SUhZiflI6oXpSQWA6UlW6hlJWCs5SV0gWUmnMllKNR9JSposaUsZoQlLg4T5TDG4GUyMIdlM0WZpTQ++KU1oxBlNovTJTknr2U7ETElO7x+JTv3W+U8WP8lQMDW5UFk7GVB/K/lRE+xpUVliSVHda+lSDcSZUn02eVL5KulUnok5VOV/uVW8LDlWI4S5VvcXKVcaPulXUvhpWSa++Vq715la2bOZWxzgeVtOAulbU67ZW7nimVzsPZldefU5Xet6qV4FoSlgIWh5YK8TKWDa3JlhbPMZYbWwiWJmVxljCWppYyfVWWOHkBljqZUJY7sR6WO7l0lkZQpZZKN+qWTxbOllOvBZZa1CGWWzhzll9CJpZgMuqWY+qalmV/05ZrdHSWcLdilnIYcpZ0lfaWdzMyloWni5aIRXiWjHgFlpBKw5aQ3KiWm4K9lqGgjZaw6qKWvM1Glscb2pbMxK+WzWTJltU3NJbeVUOW3qZElvFkrZb865CW/eiYlv/dcZcMNXSXFofjlx6Wp5cfINqXJlhklygqAJcqE8uXLyiLlzMvepc2WRCXPvODl0woO5dQgJ+XUxhCl1RvPJdiT46XYnK0l2NdWpdqRwKXcOaol5aAn5ei0nCXuRjel7vHlpfAcpKXxMgZl+MToZftf2+X7r9Bl+8wuZfzfxaX9pYgmAV1NJgHfReYCgR/mAqzXJgbGoCYIcVfmCqp0ZgxFhmYON/nmDjkcpg60EiYRKOxmE/27phmHGeYZmTWmGdH4phvFvKYcg8MmHjjYph7T3KYhAujmIUG05iFW36YjwC6mKHr+Zip5iyYqts3mKwXfpi581aYvPM0mMGaF5jH7t2YyOw9mMyQ+JjTVq6Y33aEmO3TP5jt8H6Y9UFDmQw3o5kY+vWZHTU7mStdZpkuaHWZLv2kmTKgLZk5eJWZPU96mUpPGZlV7e2ZWcEMmWV9YZlsjaOZbePLmXAqvpmAC4WZiGMomZQzgpmZa5GZpeX4mbL41Jm5X5SZ0W6ImdLFrJnXLH+Z2N9Gmd+Yi5nk9bmZ5rdJmemVC5nqDWmZ78UKmfUZmpn2DuSaA4+1mhLFD5oVj02aHCpnmiF9SpotA+qaLrt/mjJDj5o6Ih+aOvxAmj3Sp5pH+mKaWoBqmmTRnZp2MfmaeX2pmn6H5JqAUoiagw42mo7GT5q1DyeawMOimsIJIprHFm6a0FInmtlfaZrcehGa3b9Umt5p5JrjQHia5AYTmui+EJrpRzWa6cRLmuq1WZrsNcea/IXEmv7kXJsAgy2bEgufmxL6WZsVPoCbIMTpmyNrLZskizybMwRumzVzkJtGYJSbRtesm0yPQptbZMSbfGipm4wC/puPnG6blqbqm6INoZun7OybqEgim63gSJuyYMebuhmnm8NFSZvEri6bzqRIm+X22JvpWrab/di1nAMqfZwKn1icDRH8nA+MSpwPl32cEHSRnBJgwpwZ47OcGrabnCB7qJwqbkicOznonDu0n5xCHQOcSqx3nE8wgZxR8eGcVHnGnFZRCpxccgWcZcwInGkJjZxsknGcbrETnHgSsZx6V66cfXcnnIgb25yT6/ucl2XznJtzhpyg5h6co6n3nKekMpyqkpGcsXaTnLW1P5zKGTCc1lYWnNZvUJzldwuc9efNnPYXY5z5Xayc+YThnQ+F/p0TEr+dJwyinSkwqZ0u8NOdMSf7nTiWYZ07Vj+dQi9TnU4eI51O4uSdUAJjnVcpq51huoSdZEu8nWVV2Z1lycWda90+nW3tXp11NCyde2CGnX4InZ2dHUOdpzAUnbGbfZ2x6yedu3GgncQxnJ3HImqdyb5xnc9t3J3al1Sd4JQeneTSa53uHsWd8NomnfTtYZ4F5oOeB/HGng97Mp4vhmOeMEsDnjIw/Z44zIueOecsnkIeE55JAkGeTkAqnlGCtp5RqMmeY398nmTmC55rRVmejFVxnoylIJ6Otn2el3ZTnpx3zZ6dok2enbFAnqOxAp6pvTOeqxAunreCDJ66a+GevQLHnr4pkZ7CYhuexCNqnsZ43p7fPu+e4xhDnuNtX57pwNCe9Lq+nvk7ep79cvyfEEMDnxyG6Z8hOH6fIiK3nywP4p8tR7SfL+sPnzW0hp84QTWfPqq1n0tScZ9Mp3CfUeXDn1WL0p9WuBKfXnnTn2BtFZ9o4zSfa/gon3fWDZ9/i1mfiAmhn49+7J+bIBqfnFhUn6Go4p+jCHqfqCUhn6xq5J+ykFGfsx+Nn7sNP5++sWOfxg//n82hs5/PB6Kf2N5fn9kMrZ/ZzImf3jo+n9/8kp/jAzOf7W8En+9nYJ/6vCCf/Wq/oAHMpKAKlTWgC75WoBzNKqAdY8OgIcCWoCbZSqAnGEqgNi1UoDzoqqBFt++gTeGuoFP7LqBV+LSgWqqSoFvYkKBi+0ugakkpoGzVxKB23wOggZ1WoIQ7jqCEdUOghnD/oI1+Y6CPCLqgkg2CoJ1asKChrYegsRF9oLH1/aC3L7+guoWQoMhJ1qDkEO6g5tCZoOr+KKDyKLCg9Op9oPi0J6D60xKg/7eaoQN/FKEQ5rmhE6K6oRXjWqEk+2GhJZtCoTBcoaEyHOChN1eroU7EGKFUn5OhWc24oV3R5aFfaRihX4uBoWLwWKFjWL6hZPhloWxqbKFz9oChdlXWoXpS/KF8mqqhf+0noYg1T6GQyy2hkQ43oZNnNqGajdqhuQnsobmJJqG6JqShyE1qockdHaHL5s2hz2KvodiD/aHeIXqh599SoexhI6H1BRKiB/J1ognPbKIJ50iiCxCxohAZq6IUFDyiHhF4oin0kqIz8OiiPPA/okHtK6JDYnyiQ622okftJ6JIvx2iUqkColQKgKJk0zeiZphoomf326Jqr8Gibdv/onAYiqJwKzGicEpXonI7eKJyl72idXxponkbC6J995WiftUAoolCcqKMxlSilZukopXgvaKeSMOioO6Zoqzl56K172qit0KcorfK3aK4phOiyQHIostfe6LU1+6i2Q9Oot8eZaLmlIii7A6xou8d2qL3/LWi+L+Aov1qQqMDw4ujB+/QowjInqMRI7SjHkTgoyC6eKMk3l2jKwQ4oy8UwaNAQBOjRvMIo0l/PqNMMfujTIYKo04DS6NeA72jX0Ado20aMaNuHy2jdN+do3uPgqOBViqji3rMo5GWbaOyEf2juDF8o7raVaPJp2Gjy3OIo8xwtaPWPAuj4zGLo+hauKPpWqyj8tXzo/QQX6QJfgikEJmupBEzMqQVq1ykIZgupCy77KQu6jekRdTXpFYdPqR0lWykeXBPpHtcyKR806+kf3yTpIhTd6SOFp6kndSSpKDpoqSkjZCkqoYFpKyRTKSu00+ku0TrpMF8d6TDdqykxuCYpMrIIaTVDAyk3C3cpOjKvaT4X36lCc4OpRXFYaUYbqmlGO8fpRl/EKUd2nylJ/RcpS/hLKUw5GylUq28pWt+2qV0nCOldXXRpXa4lqV3R8ylfZTEpYfdnKWP0Z+lkUQcpZrMAqWdlEalnjdepa+7XqWx1+Klsj8zpclbPaXOVu6lzsevpdTilaXeisul4IJNpeHXJaX08til9RivpgFwrqYGJk+mC2ZEphoa3aYcDa+mOM7Apjo756Y/nGGmQRkDpkKneqZEMTimS02nplWtBqZXVR2mXim5pm5NLqZ4pj2mgoOqpoQkhaaLg1GmjTFCppkO2aaaUHqmp2pEpqvKfqa086WmtuoxprfoIKa5ln+mvVD9psLqgabD2uSmx1g7ptgWlKbYu26m34HXpuJ8a6bix3um5tw+pvDaG6bzdaGm+BR9pvo476b/cCum/6SZpv/nIKcBhzmnL3s/pzvH1KdAQ4OnSL9/p018Iqdfu9WndZG+p33WKKeS23qnmqGwp6AtcKei9xSnqeflp68Ejae3QEqnvt7Rp8DBPKfVebqn2Vfdp+6goKf5M9an/7crqAGfWKgFv2moB9COqBLOeagfitCoJUgzqCyT66g2JQuoObVwqEeGpahTwjCoU/DQqFZdA6hZgNCodJD9qIA/nqiCbtCokW/BqJR55aifHtOoo0W+qKZU+qi5TouovH0aqL7mxajFO/SozwecqNAd06janK2o3sNqqOANQqjiqjeo51XHqOhJfKjxvUqo8cBVqPMEqaj4fyqo/qJAqQdBbKkKSGWpC9oRqQw1W6kyfjypNAEeqTz5Pak+K5KpSo/lqVFWfalah0KpX65hqWFL7qlm0QmpcnuxqXWgWamNEUypj6dgqaLoRamlZ6CppYEAqaxIQKmsw3iprzbXqbnx/anCQc6pw6a7qcbvjqnOZzWp1bjNqdcKwqnfeLSp42tvqfN7p6n2iUyqAAKnqg5+hqogkFSqJtfFqit896o/aD2qSl+Bql/TR6poyM6qca7LqnQ6Cqp9W9aqiYSPqov01qqMQTOqpNX4qsCQtqrKyDCqzCjCqs2N+qrTQcWq18TLquoHcar0Kgeq9MYdqvk4ZKr9eWyq/cI4qwLSRasIBHirCyKrqw7bYasQrF+rFly5qxwquKshMtirJK7VqzB2a6s3i4CrPMyNq02NKqtSYc2rUzRMq14ryqtkmLWrZdi5q2uE96twyPSrgfVcq4dEZ6uH0kurk2GDq5TsvKuaQY+rnED4q512XaurPBmrroVNq7KUGKvDjLarxe94q8mLYqvM9UurzhSIq9XPT6vWY3ar7DW7q/seb6v/Z92sBFLarBN8aqwUJvCsGrI9rCFCEawlDkqsK5+6rC8Ua6wwQ+CsNGbhrDs7M6w9pB6sQoC6rER0kqxFFM+sSqQQrFjVsqxc5RusX4QHrGY246xwuW6se+hnrIBt2KyC6rOsjWRYrI5t2ayYB52sptbgrLU1gKy2EmqsuUOsrL4Q5qzLl9qs1yNurODF6azok/us7EaLrPHmXKz+1JytD0nLrRsVO60d7FitHn9ZrR/0jq0hd+ytMR3HrTbIT61Dpw+tRQhhrUYOJq1TM76tXrYqrWHuj61wq5ete9oIrX/d+K2BZ9+tkFZAraWm262oe2yts65Urbo2+a3Bb6Stw+b8rc3ufK3VZLWt1191rdtHKa35zOGt+lnMrfyPCq39k8KuBRkFrg4HPa4QIhquFPrFrhZ7v64W8OKuHZF+riZjbK4sU3WuMHN/rjyrEq5A6iuuQnYOrk7Hva5PJuKuUMPNrlEPfK5a9U2uYMT+rmK1l65rdkKucFJprnSBl66A2HCunnrArp8DO66s1GuurnFbrrSvgK62G6yuvD6+rsXI6q7MXQGu0lC4rtTvO67drrmu4w9qruQgiq7pwOavBJ+Vrw57GK8Txo2vFYNsrxXtVa8YNIivHGftrx3/TK8pQaavKrU4ryxB6685JsOvO47Ar0EHlK9En1evVNVZr1Zh2a9cTq2vcN8rr3U3fq92T5ivhNkfr4kdyK+JeLGvmlLvr67XVK+2I+Cvvef3r7/Fuq/Kw1Sv42ntr+TaSK/l/U+v5k50r/Gt+q/zX1Ov+MMvr/jFx6/7Asuv/GSdr/0z3rACw1WwGvwrsCAV9rAn2MSwOIO3sDmdILA7dDawRRsQsEvyM7BQcBawURZ+sFt19rBcA46wXl/asHa+S7B374GweL9XsHy4YbCOi6awm+hdsLKfEbC2GHewu7nYsMZCTLDIGKqw2N5NsNkLvrDyAIGw9EVxsPVQQbD3cK2w9/EosPtvX7EBF4qxAXqxsQSPM7EFf+qxCoxjsR0GOLEdCpWxHmzpsShdS7Epd7KxLydqsTGGfLFBCLCxSrSAsU/eFbFVF7SxXDQvsW15cLFvqKSxgK+7sYeEYbGJB5CxrR0Nsa3lMbGycfyxs3c6sbRJlrHKYDyxy/J+scwfdrHS4Wax1nJqseMEN7HsD1ax8DQMsfFBjLH0XtGyCBeRsgydh7Ig7i6yIoWMsirRELIsaMWyMq4SsjtLjbI8EgCyP3SMskQmE7JMOpWyTtfbslJyGLJWv1yyV69FslmFC7JfWYeybl69sm7iu7JxkkCydYWCsnxRprJ/jv2yjhQLso5/97KRm1CylxdNsprr1rKkkeKyp2bnsqfSRrKp9dCyquPasrNnW7K50EeyxO5dsumK1rLuYDey9sVusv20n7L/2+uzA4CdswWSGrMGG3ezCkiqsw9BabMTaJyzE7O+sxTNELMXQYWzIoW/syTIALMp65azNdUFszm2Y7M7Xj6zO7Nks0eXobNNz/azW7ONs2PG77NmrUWzbpuOs3HoJ7N0lIyzdl3ls3fEabN91oKzioEJs58AjrOrcMGzrk9ys7zVW7PFLnuzx6wfs8iptbPL5UizzXXss97w+bPg9i+z6LL0s+6Gz7PwZA+z8NL8s/WU4bP7+sC0B/YitAmBqrQNUTG0HpuNtC+AXbQyn7q0OwrRtDxLgrRBSey0QowHtEPeS7RErAa0SDhbtFHTrbR1sHC0fZJptIDAdLSBTiC0h69BtJi/orSaV4C0qaLOtL2Px7S/ZYO0wTVOtMZE7bTOPy200h+HtN3OGLTd43q03zI1tOI/QbTyg0G08wKVtPMJ4rT+13S1DWUHtQ23ubUN2kq1Dh2JtRCjy7UQuRC1FI6mtRVRabUXc561Ifk5tSkWSLUuqZ61NN8ktTgjAbU5u7i1SueJtVLI67VbIKG1XQuBtWSzE7Vs6YK1cG16tXLce7Vz8k61f4KbtYKpPbWNfJO1nEQgtaqKyLWtef61r6mEtb+SkrXPSYu14TqRteRLDrX/wBS2AM6jthCboLYSfCW2F4NPthrHj7Yt6LK2Oq/PtjrhL7ZEwwS2UwMytlOJEbZT4ba2Yu3btmWoVLZwjVm2cR7WtnEjdLZ5G5K2eTxxtoFIebaCb6K2j07DtpKMKbaYnTu2maxttqNKn7akN2S2qgEntqyRebawVGy2thsBtrmqQLbEAHu2xjNntsgEU7bJCf+2zKA8ts06dbbOaFK21EZ5tuUF0LbtniG3Crb4tyqMr7c279q3NzDPtz4YurdMZ/O3Tpytt1GHFLdSVrm3YYFPt2RB9bdo5eW3dLiut364GbeANKq3gNJSt4tkd7eM48W3j8yEt5CW1rec0mK3qAl1t6h1/LexXum3sk09t7v2WLfBJei3w63ct8QLnLfIPbG3yP+4t87sj7fQRc635zV2t+ltW7ftCIG39zxbuADo4bgC84S4BPSiuAqa7bgKvC+4CzyYuBAO17gRRUa4HLbauCA1Erg1n4O4Owi1uEOQjrhD7eO4RX1TuEicPbhbmw24X/p9uGLXwLhmCba4aXJ5uH8n+biGjzm4kvBnuJZAdbi2i8q4vd7MuM69ULjSIIm43exuuOOQ4rjlMf+474TguPBlNrkNlFe5EvdDuRRqFrkUn9W5G2H7uR6Cnbkfgs+5IFkouSHf6LkjNqK5JDRMuSa4yLk3wTS5PuWnuUCK1blOnz25UmR5uV3KBbljfz65bPrFuXHcy7l7B8O5gJA9uYZBXLmMgMy5kNBJuZLeQLmVO8G5mLEwuabgebmn8+S5qvNeuatdNLmzyTq5vEiuucBIgrnJDsS50VlzueIMC7n0N7m5+GY6ufnvnrn+ijK6ADJbugppgboRzly6HTL8uh1DF7ogbQ26IaWzuiLvNLomIJy6J5Seui5Oi7o2uXq6PszBukcGabpRGRy6W/ydul2AJ7pqDi+6batkum23HrpvVky6dwdHun17RLqD+BG6hWeXuosq0bqUGvW6mJ49uqGhR7qubR66vXWIusS0urrMHFC63Po8uvkmJLsCxja7A4pDuwbzYbsH4UW7FdXjuyEVjLsiA6q7Mfv/uzYqRrs6zxS7P4xbu0D3WrtMxqu7UA/Ou1DAVbthRN67bt0lu3jsDrt+dd+7h7R0u5Qqv7uUQye7mk6lu6ijELu92QW7wpt2u9YMZ7vZ1+K737xqu/P7HLv+S/+8AHZVvAe60LwoXKG8L+7WvDCiqbw3md+8OIfWvD+oV7xA3HO8StmKvFO1gbxXHnq8YiYZvHCwUbx09PC8e5JIvHyvvbyGtHG8jFTtvI2GR7yYALm8ohD4vKew8Lypdku8sKbXvLWMkby1uRy8wS/VvMHg8LzPoDe81ZF7vNXpabzbhNq82+s4vOndirzqTPW87lnOvO96BLzyLfy88kldvPNVdLz29WK9AgKnvQhELL0UO7y9FIo4vR8BB70jS6S9JWL3vTAn+r0xcLK9NAT4vTjUwb1D4rC9U63ZvVvaFb1eXrC9YZvhvXFxdr15zr69fUPvvY5btr2gRii9pCo9vaTlRb2za7K9uh6RvcZJo73I5nm9z1IKvdJAyL3iJFe98rWIvfZNBr39l2+9/xptvf9VdL3/9wC+An/Evg6b3r4P0N2+EK0svhIfLb4WV6++GJirviB7d74p0Zi+Mw7ZvjUcOL49PGm+QeGAvk4uhb5Q9RK+XrVqvmbiA75sMky+djMbvo1Zj76Owg2+kg/cvpKRCr6V9Yu+l7iAvpkOHr6isgm+rF+BvqypLL6uU2K+sOOOvrQ3Cb7Cm4S+yxsHvss7yb7MMim+0/mNvtUMar7Z2T6+44+8vuvFir7r8mq+9TvSvviXJL76GkK/AulWvweNkb8HxW2/HtuavyLgzL8pA7W/LF9uvy90nr8wQte/MGFFvzW9E786G8S/Qmstv0gJXb9a/Bi/W0VUv1zymb9fw96/YigPv2MTp79qrPC/bJSCv22Pmb9851S/fRFLv35H77+BRY2/gd+Fv4dZSb+Pl3m/kg1jv5KSF7+cAWm/qegDv6pP0b+1u0e/t4LOv7jbQL+6+LK/zfPmv+VMqr/tiei/8nLpv/ooSL//LdTABJRCwAowV8AgNPLAISwtwCgibsAowhPAMtVHwD6QmcBC2njAYEnQwHKRD8Bz2X3Aef2zwH/pkcCLfMTAlbIiwJmkKsCrWCDAsTf+wLFCGcC1HEbAyByFwNPki8DdQ83A/WZDwQYDOcES6IHBIAw6wSV/aMEmI7PBKbMkwSshPcFCb4PBRu59wU9tGMFdqZfBXvuJwWqrn8FvvlXBdmAWwXdfIcF3jcjBd5IswXt2xcF+fDHBi51FwZW1CMGbZ3TBpBWJwbQtlcHn4gnB9GmXwfuV2cH8cT3CARCRwgT08cITnGXCFz9hwhjjnsIe2JHCJokhwiiT28ItSgzCPfQ/wk0KGcJPXP/CVD//wld0MMJgn9TCbvn2wnXdsMJ4+MXCeq2Mwn/BWcJ/4IzClFytwpg8isKbuOXCnk2cwqTCWMKpUXDCrKtewsLJecLEqJbCxmBTws0CesLcNNzC3DZ4wu24IcL+c7rDBr+8wwe2MsMLZAbDDdVOwxQFscMfd7HDIPZ/wyHdcsMj5DDDLpl/wzLFU8M4c8nDOdZYwzzKmsM/BZvDQeX0w0hKW8NIwXnDTACRw0zC9MNP8BbDVUZDw1aIqMNbBybDafmcw2oY7MN0JMzDfDrYw4H13MOSMYbDmJ5Aw5mc8cOaL9XDrOGAw65Fe8Oua2LDrz2Qw7qNUMPDcHzDyEvww8kpicPKX3jD0+pmw9eYPsPcCODD35cbw+iQQcPudfvD8V0nw/Y+58QF/RHEB1GpxA27AcQT94/EFaWYxBsI+sQefefEI+OFxCVCHcQpbpvENydbxDf4t8Q4n//EO+OzxEHxZMRDhsDER4F0xEiqqcRLWFHEVVgvxF0LlcRd0YDEYDylxGDUW8RxTiTEdFeFxHXwDcSP+L7Em2+8xKft7sSwONDEs4a6xLWowMS1yGvEuTWZxL/rcsTAX/vExsVyxMjl68TOyk/E19hKxOKpFsTtFOLE7TlrxPsDFMT+Kx3FAEV2xQE3scUBwizFIQ7bxSKHK8UyVTHFNEF+xTkVO8U9LxrFPU4oxUKlx8VIBJjFSfCMxUuoiMVQczbFU2bgxVxQhsVh1m7FcBkexXtWtsV+KqfFg5EWxYj6pcWQr6nFmD5IxZqdTcWcAvnFpBOfxakFD8WyISLFyAZtxdT+pMXykTXF9eTXxgJmqMYEtATGDLZtxhc9cMY4w0LGPI7HxkAzZ8ZRRFLGWcbrxloPt8Zdw7nGbmCWxnChsMZ/HP/GkitrxpLWocaYboXGrjA2xrNPwsa23wrGu764xrwpw8bU2WfG4vh8xu+umMb7veXHBSZOxwvFYscNMtTHH9ahxyjW78cq98vHPJn9xz79wMdDpCnHRTuJx1xqvsdin4vHbLtXx4AByceGCFfHjX+xx4+a9seTfEzHn851x6eRTcerOIrHq83Dx7Rvw8e2uEXHu5mMx8WC/MfJnsrH1KYwx9X998fd7eXH4WgVx+ZGs8fmR37H5zoXx/+jvMgO32bID1vByBAZIMgQo/zIJFSzyCT+CsglHYTILj1yyC86wcg/OQfIQNEYyEIMrshdg8XIchtlyHKSUMh5pInIifujyJup2cifLoLIogmiyKUPY8il4O3IqiDKyKtRicivPbTIu+99yNN1bsjU0nDI2DtfyNmcL8jmgh7I8ZONyPLlwsj7VUrI/GegyQcw/ckNJerJESlyyR5GXMkiz67JLXtHyTHKxck0oYnJOQzhyUcXD8lH9FXJTQSTyVInv8lSWd7JXuR2yWH/fMlmV6HJZxmbyW82xcl2cg/Jf6m5yYHRJcmErtDJjaNnyZBNWsmTxDDJld1zyZ1ReMmdwvTJnrORyZ8MU8msYtTJrdZsya8tecmzWZXJtTTKybmQUMm+AU/Jxk4HycdfrMnSM13J2CyRydjznsnkR5XJ9czBygAnjMoEdBrKB9Mlygtik8ogco3KKfKQyjAE08owZQ3KMHG0yjl5o8pBvTbKQevBykVuv8pKlKzKTMDOyk+XVMpSlb/KVwGIylgXgspY7BfKWQLxylycB8pe9i3KYzgOymg/qMpqiUnKcJGOynd7ocqCKfHKkpDRypMMWsqel9DKoVWtyqanZcqnCUbKqkFsyq6dYcqu+PLKu2zcyrvSBMq/uY/K0VJDyuBK/8rjVbbK5VHryuViFcrllw/K51iXyvg+Psr/OkbLBH0myxnFccsnhdzLKa5nyzFzPMs1qwzLOiEGyzpHq8tBjULLRcZxy0oPLctPKuPLU0agy2QLrctlSsjLbUNby3qCw8uA27bLg9+yy4SCscuIG8vLiJRey5Dzc8uZAlfLtzU+y8ehmMvUMNDL2nzCy9sMx8vmSJDL56nJy+hpZsvyUQrL9B9by/cqysv9fwjL/axgzBeAucwmvL7MM0sMzEOp7MxD+9HMRyOZzE34rsxRFj/MU+F9zFcmWMxaDFDMYe5LzGKZ6MxmKlLMbEs6zHFNk8x1rSrMgDtXzIT3vMyJYwbMjk2ZzJD9qcyVIDDMmRZ2zJm5esyjEvrMpQIHzKqNjcy+kbHMwFLvzMOge8zJ0EnM0l+pzNNRi8zajR7M3rN4zOQinczmQ2/M5tMCzPJSy8z4tLPM/7LkzQHQ8c0G+2zNDEJBzRnuns0bM+LNKZwQzS0U/8063d/NQ03EzUX6481IHc7NTciazVK3Ss1VbALNXUnkzW0YHM1zZnfNdm2AzYXgMc2JiWLNiaIKzY0x1c2SgVvNl82NzaBRyc2z//LNtazazb1NZ83E9xXNyEXpzc/8Js3Xki3N5jfDzedUgc3sq+HN8H4czfFJqc31R+3N9rBmzfbZ7839MHHN/lDOzgsWEs4NYNjOLGatzjwras4/1PDOQVH/zkT8Yc5P3+vOVgu0zmpQ9M5qjXDOfn6DzpIh9s6SxQTOlBVRzpiirM6mdVvOqL4Yzq5mzs6vxAXOs4FrzriIWs7BH5XOyvtNzs7D7M7SHgDO30H8zt9Yrs7oJRPO7HLnzvTaCM735ZLO+3e4zwt3Zs8RZx/PFLTyzxTycM8YW0TPK+dIzzeTgM9FzQHPR0mLz1JoSs9SnCnPU9eBz12/Ds9nldrPa4rlz3V5lc95lc3PfJBrz31zu8+Hb+fPjrHlz470f8+R06jPldDRz6tIt8+zWW7PtfQBz7zIFM/AcHTPyb5Jz85Tss/RhUzP0wIYz9Pzd8/UezvP21NYz+a0z8/nT/zP+w0h0ApOVtAO78TQEvaB0BOZtNAVzEbQF2ct0Br+L9Aj0vrQKf+m0DPiKtA7g3HQTBZ10FL4X9BmQ2nQc1yI0HaJ4tB5f37QkIiv0J937tCkh/HQpU0l0KZUNtCqDxvQr8390LPatdC8R2zQvi3E0OZa2tDuNF7Q+Ch90PpVSND7ELzQ/ytM0QoyYtElVpnRKelY0Ss0NdEu/WvRMUne0TFQ/dEyGPHRO5Pq0T5eCdFX5TfRWCLD0WM+MdFks57RaQXV0WlKNdFzpfvRejMe0YDAYNGCD/jRjkR70Z3309Gnb5DRqhzq0a0R2tG+HQXRwe7y0cWm5tHG8ajRznv20dFFvdHnxCDR6tOy0e1VSNHvRzHR+bNQ0f5OYtIP2d7SFvII0iPb4tIyxsTSSNmi0lGH3NJSD37SXerq0m29p9J5N/nSeW300nofEdJ/RGnShsES0oxIHdKNK3fSkJwb0pvxxdKdm5XSnfUT0qCPbdKk0afSpkhA0q4lf9K48w7SvTVJ0sXWGtLHuWzS2+mz0uuG2tLsuurS72wW0vdegtL7vKPS/FEk0wr9UtMNd7zTFPwk0xj0R9MapnfTHB4Z0yA1EtMtv5zTNVXP0za+u9M5WGfTPIC80z/vWNNAJq3TQx/u00RXAtNMxQDTZ3tr022j5tN1OL3Td8q504V/+9OH5DvTh/yN04j4o9OVUCfTmkdQ06fhdtOp+wfTs8Wb07rxNdO+Ue3Tzrvv08+fUNPU+enT2RiY09wgqtPw77TT91K30/n4sNQJTlnUGb6q1CMxt9Ql4KHUMsNS1DM/99Q04ObUNxOL1DrSTtRHacbUSJYx1Eva8NRM7G7UaNRG1Gkf4NR5+g7UibkE1I9DQNSPuFDUkn3a1JXMv9SkywbUpN5P1LOuBNTZpIzU2led1OdDD9TpTnjU+SWw1Pr5btT+WBXVAbqs1QOuT9UJXPHVDMWm1Q89PdUVEdLVFe041SRKM9Uk1yjVJOpH1Sj8o9UtJUDVMHhj1TZS3tU2xPrVQITI1UZ4XtVLdrLVUgBz1VLBXNVVB+fVWZZY1VteiNVo/lDVabuv1WyCoNVtmFPVbmv21Y+Qz9WahGTVob351aJwu9Wnlp/Vr3Qo1b1CLtXGE03VxjrN1cc+otXOesHV63pv1fEuU9Xyk83V8uXH1fQrGNX8btTWAEoP1gVuR9YHJCrWFZK+1hzq29YduDbWJfwx1i7bvtY+uijWQOYi1kJOzNZZwQ7WW75v1mMl2tZweCLWeR3b1nzKWtZ/2uTWgZT21okwL9aJQZXWkgUo1pVdl9ajpDDWpK/U1rjkitbC6cLWz+Xn1tFMjdbkcMvW5uV21ueIONbt15/W7tMr1vU7Mtb4zdXW+NNY1v6hsdcU2EXXIi2D1ykzOtc++STXU9a511s7UtdbqSPXY7fh12cByNdoPlLXaxY113P1cteGnSnXiJVj147DPdeWYHTXmbrm15rEotenftTXqQib16r7htes+GHXuNKN18UKCNfN3OTXzfCf19Dk1tfUzovX1/wq1+GZMNfkaYzX5Omr1+sqpdf8UIHYApV62BTonNgbabPYIUmV2C7Ojdg2KhTYOsgt2EjJcdhPPNrYUWB22GgK6thp23/YbzU52HWsh9iCa73YhvzW2IsQxtiLhPjYjDhn2JFX7NipTaDYti5J2Lh6Hti4+uTYv61L2MT02djKRYLYzIow2M0QudjNfWjY4FaY2OF1vtjubwjY9owa2PhUfNkAyunZFfTp2RZ9jdkff27ZI96f2Swnl9k8tt/ZQm5e2UNY69lIFRjZVvS02VgqBNlbVs7ZYHel2Wd6T9lorTjZaYMe2Wnn4NlsiO7Zb9Rk2XjKK9l7Q67Ziiik2Y8sWNmP9njZk16E2ZP7kdmaFuvZmiEY2ZpkBdmhTqzZoctz2al5LtmuRTrZr1JZ2banu9m4U1/ZxOma2caR0tnPtETZ1xq32dxhftnn3H/Z7j5l2fP3udoAWWnaA6CL2huuwNoxdaPaOIYM2kFjENpFIDnaSHog2k2/vNpUQu3aVmjW2l9Rodp8auHafP112n0ziNp90XLaghHS2oJ5stqF4Dvakuwb2pQqUtqch9raq13H2rp409q+Aqraw5kO2sag19rWmtXa5tuv2vFMedrz7yna9rWF2wDaitsB3HLbA2uM2wQ7INsLovHbC8Ac2wvJYNsVqKbbH9wc2yGPYNsl8vzbODWh20WMDNtLJ1bbadtf23gJMdt4Yyjbhe5x252UotuxBpXbu86y277JG9vBGIDb1s/x29wgatvdbJLb3/Hz2+Ag0tviF3vb48ZI2+goStvtFm3b9jnb2/oxGtwLmrncF5Od3Br3fdweRjXcHlXu3CX53NwyVdncPKU93EPSMNxHcx3cTdRN3FQ9KtxZi83cXy573GPW7NxsTFHcbUvF3HE+BdxySvHcfIOb3IAdddyPZfjckYag3J7QydyjwWncqb/Z3KnxwNyus0ncrzAS3L7TYdy/nc7cyK1S3NZzLdzXxu/c2VU33OA/TtzlrCLc8odF3P+VrN0BkDndCLWO3RAtsd0TzSrdFLlL3RUgX90XT4rdIW+d3ScgRd0o25DdLfpQ3S7bh90wJ7DdMIsy3TeMTN1C7ivdSLrG3UrpSd1WG47dWHdo3V60M91f75zdjrcF3ZJ9gd2UYk/dtKa93bmZvt3K4ffdy0vk3dadbd3ZGJDd2nsg3d7mLt3mYQTd8+aq3fRZl93/uareECfW3hh2Qt4iZu7eM4QH3jRgg944t5PePVvR3kq24t5L7c7eTTnJ3lJMd95X76HeXpLQ3mipUd5rKcvedF233nb+UN6FLf/eih9J3oww0t6Zgvjem4lW3puNMt6gRFPeo4t63qdC4d6r2ETerQOF3rT2bd66PXfex9xV3smq+t7OBUje101m3tmC597mMA7e84h/3vmm597+TpXe/x2D3v/Cy98FXGbfC2ks3wtsQd8iMYPfKYNw3y76Bt8wLOvfM5lN3zcDjN9DrUTfRKHG30v5Lt9R43zfUrT631vPAN9w+bnfcdpr33T/st94NrvfiQzc34zVON+UbvDflyjJ363IVd+wTm/fsqp+37RKpN+1nwzftkhw38GRNN/Dz6ffw+Tw38QTg9/KZYXfzMBv39kykd/bc5Lf3XvO3+Jj2d/i23Tf5QOl3+Vr0t/vkcXf8rtS3/TTYN/+H0/f/o5H3/+ApuALVU3gG8d34CnkH+AuVPXgNlBa4DeGvuA+kyXgSCA34E1cTeBYRz7gaDgb4HQTjeB7zPDgf4xK4IN9ReCTFfrgmWo34KW2F+CrrebgtyPg4LiHtODJV0jgyrQH4Muj8uDRqGLg0qS44NsaNODsE3Lg80/64Pk3VuEB/TXhBcyy4QaZaeEHqV7hDoS+4Q+DFeEQEZ3hG3q94SdILuEnrsPhKHdT4SrkzeEylpzhOtm74TtaWuFG77/hVrvF4V1x3+FjsCXhanHF4XD4AeF19BbheKUa4Xs8cOF9IovhggtI4YLCF+GJXXThi6fl4ZTuQeGVJwXhm4+K4ZvXl+HDs4vhxyZf4ceaWOHKRw3hzuAX4drVluHcXqXh4rAe4eS78eHljKvh72zB4g+AzuITdLHiF/Ye4hv8FOIfxWziIejK4iWJBeIs1GHiLwbY4jwxPuJDxMfiRUb14kviZuJcj3LiX12W4merTOJrECficjo74nTu/+J5Hhzijy6+4pJ0ceKYG0bimYDi4qi72uKy0Tbiyeqv4tNa2eLVJdni22IH4uPMU+Lka8Li6EdS4uqUbuLvNXTi8+Nu4vqpIeMAMTHjAzrx4wphBuMKg8zjC/0M4w7VseMhqP3jI2H84yfH0+Mp0bLjOd5840XrteNHZg3jSrcF406mtONXEmzjV5se41nPvuNb7Objamp143CLAeNz/lTjewMK43878OOI00/jitIU45JOMOOU977jlX3i468VjeOvxbzjts2i48LqNOPD0xzjx/+S48u6iOPNn2Tj0VB/49Vn4+PZKCfj2dlZ49uU0OPv/dPj8QsT4/X67OP/w1bkAF9R5AR7neQF+oPkELgI5BGkkOQioeTkMsIW5DbCFORAmCLkRkTk5FxySORfZWLkZRyf5G/INuRyI6jkfbmh5H5cRuSLVAXkkGxh5JIsXeSS6MzklSQF5JcL6OSZvBTkrwAS5LTNQuS5b9fku2CJ5MGaneTCoJDkyuRV5MxOb+TT0/Dk1JFm5NpTHuTbwpLk29dR5OceUOTt36/k7frL5PiL9OT7eiXlAcOo5QzIVeUbzpHlHgNm5R/drOUmJvXlKrrC5S/8ruU05+LlOSLf5T2SyuVRiozlX4Ab5W0ayOVxBE3lehS75X6/+OWARyflgzDN5YaNWeWLDyjlmMEy5Zq+0+Ws033l2rqD5eR0cOXkpHTl5R3d5en6G+Xx8V/mCn625ijaVeYpx/bmOgAd5kAu5eZCdFfmSFS75lqs6eZbuJXmZZAw5mm27uZuHp7mdTT55oUnd+aILmPmibm+5o4RvuaYZ8rmm6JW5qoJ/ea0fzLmtb8X5rpz3ObC7MLmxcbC5sZ/I+bGm6fmyK/l5snAbObMD7Lmz5sz5tXeaubXpMHm2fHC5uRiqebuP9Xm8SzD5vvjpeb99mXm/z3F5wCR5+cDkInnCZAn5x4Te+cgsmnnJ9FG5zGntuc/MT7nSgLD51Cae+dRE6znX7vX52zv8+d6MVnnfcrO54AoEueQ7P7nnvxF563i8ee11evnub5W570i0ufJW0znylLz59DcbefTYcfn1Tfh5+aUxefqT5ToACwW6AcheegM7Z3oDZZd6BBkQegSbGToErqN6BaVgugkZlLoJIy+6CmjeOgySb3oMk4m6Dvuleg/qjnoQej56EXB5+hKcWPoSuvy6FAgWOhSGRDoV8oj6Fpc3Ohc2H/oYPoh6GferOhw9DXohYZ66IjSveiNZXTojkDv6JG1VOiUJnvop9by6LFl3ei8aC3oxxd16NexYujf5dXo8Rs86Pm7QOj+18XpE87d6RYZcOkZnkPpHV+D6S0+YekwXXPpO0486T5lbuk/6GLpR2JD6VDBUelY+GvpaAs96W5mRul7pq7pe7NU6X80kumDUPzpg3R06YV5EOmGoCDpkz6K6asBLem5Xivpuj066b+yxenH85XpyT9x6c5H4unRaMXp1tow6dy6OenyaObp/xth6gkkEOoRgVjqFBds6hdh6eobN9rqHcji6h3i/+o6VsbqPNl46kJbf+pIdBfqS3CR6k6a1epUFbrqV8IX6lvI7OpeuOLqY6xX6mT/oepz3cbqfgKO6oyhCeqUy3zqpqBB6rDw1uqz0rrquZue6rwSq+rIX3fqySP/6ssNG+rNJhfq1Vzt6uNWa+roNX7q6ZFm6uyYq+runA7q8UoB6vOBeur09Izq9z7S6vdcgOsGjHTrCHeE6yLF4usi/rLrJsTH6ziwsus7DBXrO0XD60NxsutDfvTrVr4E61ngAetxB0/rekbc6312E+uHWBLrh5GU64+M4OuSuNPrmiE065x5jeuggv/roNLc66M9Xeu+Lo7ryba7691WD+viuN7r5Txh6+iST+vuHkTr/HkQ6/0xBewSL1TsGITX7B4RHewef7jsJqqh7ChZNewp3gDsLXdE7DCtx+wxq3XsM3pE7DUut+w+Zh3sP1UF7D/dV+xGG1TsSh/67EyINuxOSoLsTzpA7Fp8Puxb89bsXKoF7GVNnOxlp0DsZ2OK7Gri+uxxF4Xsd4vg7Hv5JuyJ4Frsirnn7I+mEuyrQkbssYwt7LJSBOy1unDst7T07MG+5uzMTTrszeTI7NHxT+zbbf3s3esT7OTmsuzo9Gvs6qhU7PQEee0OkoPtEOuY7Rsbue00blHtNXu77UIcNe1DU07tSH4e7UsBD+1N/xvtVkN37VkEw+1ZZnztWs/o7V0wqO1dgjftf4U27YCOcO2EdWztjzHB7ZFXFO2RthDtmDg/7ZjbcO2ZYWTtm0b97Z09g+2e0jvtpdv87akbOO2t0HftsfQq7cMUYe3I1ont1Bvu7dxQ0e3mBCLt6FdT7ekn+O3r5Eft7IK97e75G+3zYLPt+fAw7gluY+4PCeLuG79g7h5yMO4lYVjuJuVn7ieSlu4tFSXuLVX77jcUhe47npvuQcNr7kWsI+5jqAnuarvV7nAfYO6Eijvuh+Yi7oxrae6Nhyjuk7577pfS8u6eMwfur0Qg7rNdM+664c7uvRrs7r22uO7DOgruxxIe7s3dEO7oOszu8tIz7vVrUe78F2fu/xhS7wW6pO8Ou7fvE+ym7xUMue8VKkTvFs/a7xqH1u8kj+LvNNh/7zcBI+84bPbvOhJw7z8XWu9CurHvRGj770gBJu9L/R7vTVTR71zRuu9l3hzva3Cd73N2UO904XvveDDb74IB4e+Jo6jvifJO74rnqO+O2VzvjzGj75ce4++tT57vsp0J77tcY+/Bh1jvxSP378a31u/JqqPvzLHJ786M0e/bj3/v25SE7+vfx+/s1xjv8hRr7/wMAO/+e+Lv/oS28AH5ZfAGQHjwDERb8Bj6ofAbRdPwIbVz8DUn1PA7ConwSZ588GzaRfBt9gnwbh3h8HRNYPB8eh3wgq8n8Ip6GfCVF7jwm0tp8KDK4vCuaAvwtWIM8LVlivDRZQfw1hcj8NgmUPDiZQDw6k2I8PTz4fD2uGrw9zK88PjpAvD5gtHw+i3t8P1Zb/D+1+Tw/6308QHEEfEP8ZPxFDCJ8Rdo7vEZaorxGhB/8RyIX/EepljxIL538SZYsPEuXrDxLvdj8TpB9PE9+fLxQS+A8UGOA/FOUCzxU6bw8VSHFfFeUYrxX64D8WLYLfFmbHfxabsK8W7LpfF93jvxiG0w8Y8FfvGPnYvxkZh78ZVXhPGYoCnxmXb08a9j7PGwEBLxsR+L8bWpHfG2mczxuoRx8cnt1fHKbszx3aIv8d3s0PHmQALx6DWg8esIxPHsJqnx8LiP8gsl6PIQT5XyGKo08iMMRvIuVRzyMnur8jL+vfI4kD3yPiZ78l290/JpGS7yagO+8nef6/J3sJzyg9uB8oR7G/KNyLvykI+R8pYI2PKXHMfyn7Xl8qDSlfKlS+Hyqljh8quN7fK1Yzryt7CP8sPjyfLJvZ/y2DNI8tp7AvLdCuDy4thE8uUt6PLqTVTy+Tt78v9GBfMFCnvzFGf28xX1NfMg6u3zIVek8yTZU/MryknzOLW780EOdPNBUNTzUNeA81WFfvNW4kfzXVWz8115LvN76TvzfXW+84cfkvOT5vfzlngQ86XdMvOqhe/zsftn87LBlPOzyR3zujgb88PUQPPGLeTz1KWk89gEevPed/3z5zHf8+tiO/QLbx30EOBG9BMc2PQUPtP0Fd9C9BYWi/QcGcX0IB/n9CSgCPQqG/n0NV099DY77vQ8xbr0RdEW9Edm8vRLaQP0T+BS9FQtufReXVT0YnKK9HkBSvSLoNz0mNWt9J9XffSksDL0rArF9KwtaPSyjnb0tzGA9MFvz/TMboL0zJX19M1MMPTSSWz01Igp9NZ5PPTYm6P04LGv9OkU6PTt4DT07nQV9PfokvT8JDv0/Cgm9P0/ZPUEqc/1BPir9RTYvvUbFS71Ixig9Shsz/UsBl31LK599S0bCfU5oif1QVJg9UoWT/VX6Sn1WKwK9Vq9oPVdwc71XjKC9WJvZPVtY1H1hLlm9Yz15/WNgrb1kp1B9Zc+6fWdPSz1qChC9a8TefXZ56X12iVw9ekdc/YBHVz2BvQB9g+70PYQIjz2EsQj9i9SIPYxr0z2OOJ49jj//fZFf2P2VG+Y9ljzXPZh6H32ZFP89nhowfZ6GIP2ezBJ9pCKAfaUp1D2lvWR9pv1Ufamso72p2UU9rRU2/a7+N72vYyQ9r5sqfbHy8L2z7CR9tLevfbhhXn29Kt59vlhM/cDOzH3Ezl39yLyD/crh5T3MSfX9zLf2/c7qjv3Y1Kc92Vm/fd2la73eMup933uGPd+lMP3hyum94h1qfeK1Dn3lCkA95VrJ/eW+o/3mHeb95xJ9vepF733qeJH98O8HffFfaD3xds/986AxPfXsGb34EYQ9+D18ffm/fH35+xP9/h95ff+2j34DQyh+BkMS/gcOUr4HrFp+CN9ifgkjhL4KpxW+C8msfg2P5L4UX0O+F1U+vhi8Wf4acr1+G/Ia/hyyq34ct/w+HVNn/iBhr34kNSU+JJWgviXhQz4oGSP+KEp6PihfpX4qy16+L1pZ/jB2HD4w4sh+NkNPPjlra74729h+PEX6fj9X6f4/XOO+QIwAPkECbn5Dsss+Q+MMPkUId75FT1G+Rk43Pkajub5G/MK+R2PafkgskX5JOX5+SXUIPkwa6/5NRpK+TcHdflCijn5Qw2t+U+KlPlTVZP5U6vV+Vc6WfldIcD5ZaSh+YV5JPmGacz5kiSW+ZPNn/mWUjn5n5Ed+aEan/mjv1D5p8bf+amw/vmsFLb5uhBn+b/QGPm/2CT5xCEM+cVPBfnIOQj5yJcR+dX5ivns4iH5+RQG+flwPfn7T8f6IT+/+ipP1PorAz76LKUJ+jduOPpCES/6VFXo+mHgU/ppd8n6bUg8+ngdZ/p8eB/6gSqz+pAgJ/qb65n6oKH6+qTbofqlkqz6tc0P+sTfOvrGcwn6xup4+sjxo/rJnU/6zNuf+t2wBfrfpOb64evj+ud0WPrqUkL67GcM+vHaL/r4lR76+9GG+v3zEPr+weX7Cokp+wyhgPsQVJT7EUiE+xcCn/sZRhD7Hn9o+yYo6/snGTr7Lg1f+0Pbn/tLzeL7Tdoc+2Wmuft3peX7eSJZ+3yln/uOfJL7maxX+5/I4/ui0mv7qfHJ+7JqYvuyu1L7tsaw+75+lfvET0D7xYgU+8eEOvvIcNH7yPrm+8kyD/vOZvn722HA++AUNfvhbPT759e6++wXy/v0y2P7+oY9/BE6S/wZMY38J4mi/D1HgPxSFZ/8VzHG/FjRc/xp8Wb8a3cA/G7devxwf8D8gLIv/IJFl/yOl/X8lVoZ/J9wSvypl8T8vDM7/MU3iPzH0Pv8yW2M/Mp5rfzK6Wr8zOSP/NHu5PzZV8D87CrZ/Paqwf0MbQj9FeXc/Rbp/v0c9eL9HXkA/R4DR/0lylT9KSKQ/SsKY/06KaD9Svdy/UyWXP1M73r9UIT3/V2kgP12G279eCbu/ZKBX/2ZWbj9ow+b/bJ6EP24ff39u0zk/cLMrP3MpDH9zMpn/dkfuv3aDEb92g1w/eIBw/3mItn96YS5/fHntv34vFj+Al3B/gRqQP4Fp+j+B2Hh/gm8Lv4Oihn+D+s8/hBWbv4fsg/+JPwj/ijxDf4t+Zv+QZiK/ky5u/5QDy3+UPmf/lMIC/5cQaL+Xh4D/mBdPP5jOf3+ZI/E/mtLUP5uCM/+cdEl/oZVgf6Hl1b+j9Gx/pMo0v6Uhlj+lt05/ph2bv6n9lf+qAtx/qvr2v6wUeT+vygi/s7y0f7Y4DX+3R0R/ulHBP7sJ2n+8zQ5/vsvzP7+brv/AZpX/waczv8b9qX/Hri9/zOo8f85ws3/SavK/0uWPP9MMxT/UgfH/1ZOOv9ZahP/YoEJ/3AOAv9w9MP/la7m/5kHqP+chJL/nkMz/6G/lf+ofC//qq+9/7R2HP+3ovf/uvWP/7tcof+/8or/1AAv/98JaP/fTMv/34xm/+lFH//sRcj//4DS";
|
|
21
|
+
/** Nombre d'empreintes — sert de contrôle au décodage. */
|
|
22
|
+
const COMMON_PASSWORD_HASH_COUNT = 9999;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { COMMON_PASSWORD_HASHES_BASE64, COMMON_PASSWORD_HASH_COUNT };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region nodefony/src/password/passwordHash.ts
|
|
3
|
+
/**
|
|
4
|
+
* Empreinte tronquée d'un mot de passe, pour la seule comparaison à une liste.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ Ce n'est PAS un hachage de stockage : SHA-1 tronqué n'est ni lent ni salé,
|
|
7
|
+
* et ne doit jamais toucher un mot de passe persisté — c'est le rôle de
|
|
8
|
+
* `IPasswordEncoder` (bcrypt/argon). Ici, l'enjeu est inverse : comparer 40 Ko
|
|
9
|
+
* d'empreintes sans embarquer un seul mot de passe en clair.
|
|
10
|
+
*
|
|
11
|
+
* La règle vit ici plutôt que dans le générateur ET dans la politique, parce que
|
|
12
|
+
* deux copies d'une même troncature divergeraient en silence : la liste
|
|
13
|
+
* deviendrait muette, et le contrôle passerait pour vert.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Les 4 premiers octets du SHA-1, en entier non signé.
|
|
17
|
+
*
|
|
18
|
+
* @param plain - mot de passe candidat.
|
|
19
|
+
* @returns l'empreinte tronquée, comparable aux entrées de la liste.
|
|
20
|
+
*/
|
|
21
|
+
function truncatedPasswordHash(plain) {
|
|
22
|
+
return createHash("sha1").update(plain, "utf8").digest().readUInt32BE(0);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { truncatedPasswordHash };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { truncatedPasswordHash } from "./passwordHash.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
//#region nodefony/src/password/passwordPolicy.ts
|
|
4
|
+
/**
|
|
5
|
+
* **Politique de mot de passe par défaut** — refuse ce qui est manifestement
|
|
6
|
+
* faible, et DIT laquelle de ses règles a mordu.
|
|
7
|
+
*
|
|
8
|
+
* ## Pourquoi des règles AVANT une liste
|
|
9
|
+
*
|
|
10
|
+
* Une liste, même de dix mille entrées, ne voit pas `aaaaaaaaaa`, `azertyuiop`
|
|
11
|
+
* ni un mot de passe égal au nom du compte. Les règles algorithmiques coûtent
|
|
12
|
+
* zéro octet, attrapent ces familles entières, et s'appliquent donc d'abord. La
|
|
13
|
+
* liste ne sert qu'à ce qu'elles ratent — un mot réel et courant, assez long
|
|
14
|
+
* pour passer la longueur minimale (`password123`, `iloveyou2`).
|
|
15
|
+
*
|
|
16
|
+
* ## Pourquoi la liste est LAZY
|
|
17
|
+
*
|
|
18
|
+
* Les empreintes pèsent 40 Ko de tas décodé, et 53 Ko de source. Le module qui
|
|
19
|
+
* les porte est donc chargé par un `import()` DYNAMIQUE, au premier contrôle
|
|
20
|
+
* réellement atteint — lors d'une création ou d'un changement de mot de passe,
|
|
21
|
+
* jamais au démarrage, jamais dans le chemin d'une requête. Une application qui
|
|
22
|
+
* ne crée aucun compte ne paie ni le décodage, ni même la lecture du fichier.
|
|
23
|
+
*
|
|
24
|
+
* ## Ce que cette politique ne fait PAS
|
|
25
|
+
*
|
|
26
|
+
* Elle ne juge ni la composition (majuscules, chiffres, caractères spéciaux) ni
|
|
27
|
+
* la rotation : le NIST (SP 800-63B §5.1.1.2) a retiré ces exigences, qui
|
|
28
|
+
* poussent aux mots de passe prévisibles (`Password1!`) sans rien ajouter à
|
|
29
|
+
* l'entropie réelle. Ce qui est mesuré ici, c'est la PRÉVISIBILITÉ.
|
|
30
|
+
*
|
|
31
|
+
* Une application durcit en composant sa propre implémentation
|
|
32
|
+
* d'`IPasswordBlocklist` ; elle ne peut pas désactiver ce contrôle en silence —
|
|
33
|
+
* le service en pose une par défaut, et la retirer est un geste explicite.
|
|
34
|
+
*/
|
|
35
|
+
/** Défauts de la politique — source unique, relue par les tests. */
|
|
36
|
+
const DEFAULT_PASSWORD_POLICY = {
|
|
37
|
+
minLength: 10,
|
|
38
|
+
blocklist: [],
|
|
39
|
+
blocklistFile: null,
|
|
40
|
+
checkCommonPasswords: true
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Suites de touches et de caractères d'où sortent les mots de passe « au clavier ».
|
|
44
|
+
*
|
|
45
|
+
* Les trois dispositions courantes y figurent : un `azerty` français et un
|
|
46
|
+
* `qwerty` anglais donnent des suites DIFFÉRENTES, et ne garder que l'une ferait
|
|
47
|
+
* un contrôle qui mord chez l'un et pas chez l'autre.
|
|
48
|
+
*/
|
|
49
|
+
const KEYBOARD_RUNS = [
|
|
50
|
+
"abcdefghijklmnopqrstuvwxyz",
|
|
51
|
+
"0123456789",
|
|
52
|
+
"azertyuiop",
|
|
53
|
+
"qwertyuiop",
|
|
54
|
+
"qwertzuiop",
|
|
55
|
+
"asdfghjklm",
|
|
56
|
+
"qsdfghjklm",
|
|
57
|
+
"wxcvbn",
|
|
58
|
+
"zxcvbnm",
|
|
59
|
+
"yxcvbnm"
|
|
60
|
+
];
|
|
61
|
+
/** Longueur minimale d'une suite pour que la coïncidence cesse d'en être une. */
|
|
62
|
+
const MIN_RUN_LENGTH = 4;
|
|
63
|
+
/** Vrai si `value` contient une tranche d'au moins 4 touches consécutives. */
|
|
64
|
+
function containsKeyboardRun(value) {
|
|
65
|
+
const lower = value.toLowerCase();
|
|
66
|
+
for (const run of KEYBOARD_RUNS) {
|
|
67
|
+
const reversed = [...run].reverse().join("");
|
|
68
|
+
for (const sequence of [run, reversed]) for (let start = 0; start + MIN_RUN_LENGTH <= sequence.length; start += 1) if (lower.includes(sequence.slice(start, start + MIN_RUN_LENGTH))) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Vrai si `value` n'est qu'un motif court répété (`aaaa`, `abab`, `123123`).
|
|
74
|
+
*
|
|
75
|
+
* On s'arrête à un motif de 4 caractères : au-delà, « répéter » n'est plus un
|
|
76
|
+
* indice de faiblesse (`correct-horse-correct-horse` est long et imprévisible).
|
|
77
|
+
*/
|
|
78
|
+
function isRepeatedPattern(value) {
|
|
79
|
+
const lower = value.toLowerCase();
|
|
80
|
+
for (let size = 1; size <= 4; size += 1) {
|
|
81
|
+
if (lower.length < size * 2 || lower.length % size !== 0) continue;
|
|
82
|
+
if (lower.slice(0, size).repeat(lower.length / size) === lower) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/** Partie « nom » d'un identifiant : `marie.dupont@exemple.fr` → `marie.dupont`. */
|
|
87
|
+
function localPart(identifier) {
|
|
88
|
+
const at = identifier.indexOf("@");
|
|
89
|
+
return at > 0 ? identifier.slice(0, at) : identifier;
|
|
90
|
+
}
|
|
91
|
+
/** Longueur en deçà de laquelle une inclusion n'est plus significative. */
|
|
92
|
+
const MIN_IDENTIFIER_FRAGMENT = 4;
|
|
93
|
+
/**
|
|
94
|
+
* Politique par défaut : six règles, de la moins chère à la plus chère.
|
|
95
|
+
*
|
|
96
|
+
* Implémente {@link IPasswordBlocklist} — donc remplaçable par l'application,
|
|
97
|
+
* et composable (une implémentation maison peut l'appeler avant sa propre source).
|
|
98
|
+
*/
|
|
99
|
+
var PasswordPolicy = class {
|
|
100
|
+
options;
|
|
101
|
+
/** Empreintes décodées — `null` tant qu'aucun contrôle n'en a eu besoin. */
|
|
102
|
+
#commonHashes = null;
|
|
103
|
+
/** Valeurs refusées en propre, repliées en minuscules — lazy, comme le reste. */
|
|
104
|
+
#denied = null;
|
|
105
|
+
/**
|
|
106
|
+
* @param options - réglages ; chaque clé absente prend le défaut sain.
|
|
107
|
+
*/
|
|
108
|
+
constructor(options = {}) {
|
|
109
|
+
this.options = {
|
|
110
|
+
...DEFAULT_PASSWORD_POLICY,
|
|
111
|
+
...options
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* La règle enfreinte, ou `null` si le mot de passe est accepté.
|
|
116
|
+
*
|
|
117
|
+
* Le texte rendu nomme la règle SANS jamais citer le mot de passe : il finit
|
|
118
|
+
* dans un message d'erreur, donc potentiellement dans un journal.
|
|
119
|
+
*
|
|
120
|
+
* @param plain - mot de passe candidat.
|
|
121
|
+
* @param subject - ce qu'on sait du compte (son identifiant).
|
|
122
|
+
* @returns la règle enfreinte, en français, ou `null`.
|
|
123
|
+
*/
|
|
124
|
+
async violation(plain, subject = {}) {
|
|
125
|
+
if (plain.length < this.options.minLength) return `trop court — ${this.options.minLength} caractères au minimum`;
|
|
126
|
+
const identifier = subject.identifier;
|
|
127
|
+
if (identifier != null && identifier.length > 0) {
|
|
128
|
+
const lower = plain.toLowerCase();
|
|
129
|
+
const name = localPart(identifier).toLowerCase();
|
|
130
|
+
if (lower === identifier.toLowerCase() || name.length >= MIN_IDENTIFIER_FRAGMENT && lower.includes(name)) return "contient l'identifiant du compte";
|
|
131
|
+
}
|
|
132
|
+
if (isRepeatedPattern(plain)) return "répète un même motif de bout en bout";
|
|
133
|
+
if (containsKeyboardRun(plain)) return "contient une suite de touches ou de chiffres";
|
|
134
|
+
if (this.#deniedValues().has(plain.toLowerCase())) return "figure dans la liste interdite de cette application";
|
|
135
|
+
if (this.options.checkCommonPasswords && await this.#isCommon(plain)) return "figure parmi les mots de passe les plus courants";
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Le mot de passe doit-il être refusé ?
|
|
140
|
+
*
|
|
141
|
+
* Même règle, même code que {@link violation} — une seule implémentation, deux
|
|
142
|
+
* portes : le contrat ne demande qu'un booléen, l'appelant qui veut expliquer
|
|
143
|
+
* prend l'autre.
|
|
144
|
+
*
|
|
145
|
+
* @param plain - mot de passe candidat.
|
|
146
|
+
* @param subject - ce qu'on sait du compte.
|
|
147
|
+
*/
|
|
148
|
+
async isBlocked(plain, subject = {}) {
|
|
149
|
+
return await this.violation(plain, subject) !== null;
|
|
150
|
+
}
|
|
151
|
+
/** Valeurs refusées en propre — config + fichier, lus une seule fois. */
|
|
152
|
+
#deniedValues() {
|
|
153
|
+
if (this.#denied !== null) return this.#denied;
|
|
154
|
+
const denied = /* @__PURE__ */ new Set();
|
|
155
|
+
for (const value of this.options.blocklist) if (value.length > 0) denied.add(value.toLowerCase());
|
|
156
|
+
const file = this.options.blocklistFile;
|
|
157
|
+
if (file != null && file.length > 0) for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
158
|
+
const value = line.replace(/\r$/, "");
|
|
159
|
+
if (value.length > 0) denied.add(value.toLowerCase());
|
|
160
|
+
}
|
|
161
|
+
this.#denied = denied;
|
|
162
|
+
return denied;
|
|
163
|
+
}
|
|
164
|
+
/** Recherche binaire dans les empreintes triées (chargées au premier appel). */
|
|
165
|
+
async #isCommon(plain) {
|
|
166
|
+
if (this.#commonHashes === null) {
|
|
167
|
+
const { COMMON_PASSWORD_HASHES_BASE64, COMMON_PASSWORD_HASH_COUNT } = await import("./commonPasswordHashes.js");
|
|
168
|
+
const bytes = Buffer.from(COMMON_PASSWORD_HASHES_BASE64, "base64");
|
|
169
|
+
const hashes = new Uint32Array(COMMON_PASSWORD_HASH_COUNT);
|
|
170
|
+
for (let index = 0; index < hashes.length; index += 1) hashes[index] = bytes.readUInt32BE(index * 4);
|
|
171
|
+
this.#commonHashes = hashes;
|
|
172
|
+
}
|
|
173
|
+
const needle = truncatedPasswordHash(plain);
|
|
174
|
+
const hashes = this.#commonHashes;
|
|
175
|
+
let low = 0;
|
|
176
|
+
let high = hashes.length - 1;
|
|
177
|
+
while (low <= high) {
|
|
178
|
+
const middle = low + high >>> 1;
|
|
179
|
+
const value = hashes[middle];
|
|
180
|
+
if (value === needle) return true;
|
|
181
|
+
if (value < needle) low = middle + 1;
|
|
182
|
+
else high = middle - 1;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
//#endregion
|
|
188
|
+
export { DEFAULT_PASSWORD_POLICY, PasswordPolicy };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region nodefony/src/password/seedFailure.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rédige le message d'un semis raté : QUEL compte, QUELLE règle, QUEL geste.
|
|
4
|
+
*
|
|
5
|
+
* Les trois sont indissociables. Un message qui nomme la règle sans le compte
|
|
6
|
+
* laisse chercher lequel ; un message qui nomme le compte sans le geste laisse
|
|
7
|
+
* l'exploitant devant un constat. Et la dernière phrase dit ce que le lecteur se
|
|
8
|
+
* demande immédiatement : l'application tourne-t-elle encore ?
|
|
9
|
+
*
|
|
10
|
+
* @param cause - ce que la création du compte a levé.
|
|
11
|
+
* @param context - ce que l'appelant est seul à savoir.
|
|
12
|
+
* @returns le message à journaliser, sans saut de ligne (il part en `ERROR`).
|
|
13
|
+
*/
|
|
14
|
+
function describeSeedFailure(cause, context) {
|
|
15
|
+
const { identifier, envVar, fromEnv, admin = false } = context;
|
|
16
|
+
return `Le compte "${identifier}" n'a PAS été semé : ${seedFailureReason(cause)}. ${fromEnv ? `Corrige ${envVar}` : `Ce mot de passe est le défaut écrit dans le code de l'application — pose ${envVar} (\`.env.local\`, gestionnaire de secrets)`}, ou crée le compte à la main : \`npx nodefony security:user:add ${identifier}${admin ? " --admin" : ""}\`. Le démarrage continue — l'application tourne, sans ce compte.`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* La cause, en clair — la règle de la politique quand il y en a une.
|
|
20
|
+
*
|
|
21
|
+
* La règle se lit sur la PROPRIÉTÉ `violation`, jamais par `instanceof` : une
|
|
22
|
+
* application qui se retrouve avec deux copies de `@nodefony/user` (hissage npm,
|
|
23
|
+
* lien local, monorepo) verrait le test de classe échouer et perdrait la seule
|
|
24
|
+
* information utile — au pire endroit, celui où l'on explique un échec.
|
|
25
|
+
*
|
|
26
|
+
* @param cause - ce qui a été levé.
|
|
27
|
+
* @returns la règle enfreinte, ou le message de l'erreur.
|
|
28
|
+
*/
|
|
29
|
+
function seedFailureReason(cause) {
|
|
30
|
+
const violation = cause?.violation;
|
|
31
|
+
if (typeof violation === "string" && violation.length > 0) return `mot de passe refusé (${violation})`;
|
|
32
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { describeSeedFailure };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @remarks P5.5–5.9 livrés (contracts, base users, `UserService` + encoders,
|
|
16
16
|
* adapters Drizzle/Mongoose). `IRole`/`IPermission` différés à P6.8.
|
|
17
17
|
*/
|
|
18
|
-
export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, IPasswordBlocklist, IPasswordEncoder, IPasswordVerifier, IUserProvider, IUserRepository, IUserListQuery, IOAuthProfile, IOAuthProvisionPolicy, IOAuthUserProvisioner, } from "./nodefony/contracts/index.js";
|
|
18
|
+
export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, IPasswordBlocklist, IPasswordSubjectHint, IPasswordEncoder, IPasswordVerifier, IUserProvider, IUserRepository, IUserListQuery, IOAuthProfile, IOAuthProvisionPolicy, IOAuthUserProvisioner, } from "./nodefony/contracts/index.js";
|
|
19
19
|
export { BaseUser } from "./nodefony/src/BaseUser.js";
|
|
20
20
|
export type { IBaseUserOptions } from "./nodefony/src/BaseUser.js";
|
|
21
21
|
export { AnonymousUser, anonymousUser, ROLE_ANONYMOUS, } from "./nodefony/src/AnonymousUser.js";
|
|
@@ -32,6 +32,11 @@ export type { IUserColumn, IUserRow, UserColumnType, UserColumnOrigin, } from ".
|
|
|
32
32
|
export { registerUserStore, listUserStores, } from "./nodefony/src/userStoreRegistry.js";
|
|
33
33
|
export { UserService } from "./nodefony/service/UserService.js";
|
|
34
34
|
export type { ICreateUserInput, AuthFailureReason, } from "./nodefony/service/UserService.js";
|
|
35
|
+
export { PasswordPolicy, DEFAULT_PASSWORD_POLICY, } from "./nodefony/src/password/passwordPolicy.js";
|
|
36
|
+
export type { IPasswordPolicyOptions } from "./nodefony/src/password/passwordPolicy.js";
|
|
37
|
+
export { truncatedPasswordHash } from "./nodefony/src/password/passwordHash.js";
|
|
38
|
+
export { describeSeedFailure } from "./nodefony/src/password/seedFailure.js";
|
|
39
|
+
export type { ISeedFailureContext } from "./nodefony/src/password/seedFailure.js";
|
|
35
40
|
export { UserNotFoundError } from "./nodefony/errors/UserNotFoundError.js";
|
|
36
41
|
export { WeakPasswordError } from "./nodefony/errors/WeakPasswordError.js";
|
|
37
42
|
export { createUserAdminApi, registerUserAdminApi, toUserSummary, USER_REVOKED_EVENT, } from "./nodefony/src/admin/UserAdminApi.js";
|
|
@@ -4,17 +4,37 @@
|
|
|
4
4
|
* au login (le hash stocké ne permet plus de juger le clair, et refuser un
|
|
5
5
|
* login existant verrouillerait l'utilisateur).
|
|
6
6
|
*
|
|
7
|
-
* Le
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Le framework en pose une PAR DÉFAUT (`PasswordPolicy` : longueur, identifiant
|
|
8
|
+
* répété, suites de touches, mots de passe les plus courants) — une application
|
|
9
|
+
* la remplace pour durcir ou pour brancher sa propre source (fichier
|
|
10
|
+
* d'exploitation, API k-anonymity type HaveIBeenPwned), jamais pour la
|
|
11
|
+
* désactiver par oubli. Le point de branchement est `UserService.passwordBlocklist`.
|
|
11
12
|
*/
|
|
12
13
|
export interface IPasswordBlocklist {
|
|
13
14
|
/**
|
|
14
15
|
* Le mot de passe en clair est-il connu-compromis / interdit ?
|
|
15
16
|
*
|
|
16
17
|
* @param plain - mot de passe candidat (jamais journalisé par l'implémentation).
|
|
18
|
+
* @param subject - ce qu'on sait du compte visé, quand l'appelant le sait : un
|
|
19
|
+
* mot de passe ne doit pas répéter l'identifiant. Optionnel — une
|
|
20
|
+
* implémentation qui n'en a pas besoin l'ignore.
|
|
17
21
|
* @returns `true` si le mot de passe doit être refusé.
|
|
18
22
|
*/
|
|
19
|
-
isBlocked(plain: string): Promise<boolean>;
|
|
23
|
+
isBlocked(plain: string, subject?: IPasswordSubjectHint): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* La règle enfreinte, en français, ou `null` si le mot de passe est accepté.
|
|
26
|
+
*
|
|
27
|
+
* Optionnel, et c'est voulu : le contrat minimal reste un booléen. Quand une
|
|
28
|
+
* implémentation le fournit, le refus peut NOMMER sa cause — un « mot de passe
|
|
29
|
+
* refusé » sans motif envoie l'utilisateur deviner.
|
|
30
|
+
*
|
|
31
|
+
* @param plain - mot de passe candidat.
|
|
32
|
+
* @param subject - ce qu'on sait du compte visé.
|
|
33
|
+
*/
|
|
34
|
+
violation?(plain: string, subject?: IPasswordSubjectHint): Promise<string | null>;
|
|
35
|
+
}
|
|
36
|
+
/** Ce qu'un appelant peut dire du compte visé — tout est optionnel. */
|
|
37
|
+
export interface IPasswordSubjectHint {
|
|
38
|
+
/** Identifiant du compte (courriel, login). */
|
|
39
|
+
identifier?: string;
|
|
20
40
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, } from "./IUser.js";
|
|
2
|
-
export type { IPasswordBlocklist } from "./IPasswordBlocklist.js";
|
|
2
|
+
export type { IPasswordBlocklist, IPasswordSubjectHint, } from "./IPasswordBlocklist.js";
|
|
3
3
|
export type { IPasswordEncoder } from "./IPasswordEncoder.js";
|
|
4
4
|
export type { IPasswordVerifier } from "./IPasswordVerifier.js";
|
|
5
5
|
export type { IUserProvider } from "./IUserProvider.js";
|