@anonympins/fingerprint 0.6.3 → 0.6.4

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/src/js/library.js CHANGED
@@ -1551,23 +1551,72 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
1551
1551
  * @param {Array<object>} config.trafficData - Données de trafic collectées.
1552
1552
  * @returns {function(object): number[]} Une fonction de fitness qui prend une configuration complète et retourne les scores [taux de faux positifs, taux de faux négatifs].
1553
1553
  */
1554
- Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) => {
1555
- // Un "individu" est un objet de configuration complet
1556
- // { thresholds: { low, medium, high }, weights: { historyScore, ... }, patterns: { velocityThreshold, ... } }
1554
+ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
1555
+ const trafficData = context.trafficData || [];
1556
+ const currentConfig = context.currentConfig || null;
1557
+
1558
+ // Ancres immuables (Baseline Anchors) pour forcer le calibrage d'échelle de suspicion
1559
+ const STATIC_ANCHORS = [
1560
+ {
1561
+ type: 'request_passed',
1562
+ weight: 15.0,
1563
+ vector: {
1564
+ historyScore: 0, rotationScore: 0, headerAnomalyScore: 0, requestPatternScore: 0,
1565
+ inconsistencyScore: 0, behaviorScore: 0, honeypotScore: 0, botScore: 0,
1566
+ crossLayerInconsistencyScore: 0, timeInconsistencyScore: 0, tlsSpoofingScore: 0,
1567
+ clickVarianceScore: 0, clientHintsInconsistencyScore: 0, subnetScore: 0,
1568
+ ipReputationScore: 0, botnetClusterScore: 0, tcpAnomalyScore: 0,
1569
+ quicAnomalyScore: 0, renderingAnomalyScore: 0, threatIntelScore: 0
1570
+ }
1571
+ },
1572
+ {
1573
+ type: 'request_blocked',
1574
+ weight: 15.0,
1575
+ vector: {
1576
+ historyScore: 100, rotationScore: 100, headerAnomalyScore: 80, requestPatternScore: 100,
1577
+ inconsistencyScore: 100, behaviorScore: 0, honeypotScore: 100, botScore: 100,
1578
+ crossLayerInconsistencyScore: 80, timeInconsistencyScore: 100, tlsSpoofingScore: 100,
1579
+ clickVarianceScore: 100, clientHintsInconsistencyScore: 90, subnetScore: 100,
1580
+ ipReputationScore: 100, botnetClusterScore: 100, tcpAnomalyScore: 100,
1581
+ quicAnomalyScore: 100, renderingAnomalyScore: 100, threatIntelScore: 100
1582
+ }
1583
+ },
1584
+ {
1585
+ type: 'request_passed', // Profil humain sain avec du bruit (doit rester sous le seuil d'alerte, ex: < 20)
1586
+ weight: 10.0,
1587
+ vector: {
1588
+ historyScore: 15, rotationScore: 10, headerAnomalyScore: 20, requestPatternScore: 15,
1589
+ inconsistencyScore: 10, behaviorScore: 15, honeypotScore: 0, botScore: 0,
1590
+ crossLayerInconsistencyScore: 10, timeInconsistencyScore: 10, tlsSpoofingScore: 10,
1591
+ clickVarianceScore: 0, clientHintsInconsistencyScore: 10, subnetScore: 5,
1592
+ ipReputationScore: 5, botnetClusterScore: 5, tcpAnomalyScore: 10,
1593
+ quicAnomalyScore: 10, renderingAnomalyScore: 5, threatIntelScore: 0
1594
+ }
1595
+ },
1596
+ {
1597
+ type: 'challenge_issued', // Profil robot furtif moyen (doit être challengé, ex: > 35)
1598
+ weight: 10.0,
1599
+ vector: {
1600
+ historyScore: 20, rotationScore: 20, headerAnomalyScore: 20, requestPatternScore: 40,
1601
+ inconsistencyScore: 30, behaviorScore: 30, honeypotScore: 0, botScore: 0,
1602
+ crossLayerInconsistencyScore: 20, timeInconsistencyScore: 20, tlsSpoofingScore: 30,
1603
+ clickVarianceScore: 20, clientHintsInconsistencyScore: 20, subnetScore: 15,
1604
+ ipReputationScore: 15, botnetClusterScore: 20, tcpAnomalyScore: 30,
1605
+ quicAnomalyScore: 20, renderingAnomalyScore: 20, threatIntelScore: 0
1606
+ }
1607
+ }
1608
+ ];
1557
1609
 
1558
1610
  return function fullConfigFitness(config) {
1559
- let falsePositives = 0; // Humains légitimes challengés ou bloqués
1560
- let falseNegatives = 0; // Bots qui sont passés
1611
+ let falsePositives = 0;
1612
+ let falseNegatives = 0;
1561
1613
  let totalHumans = 0;
1562
1614
  let totalBots = 0;
1563
- let totalChallenges = 0;
1564
- let totalChallengeCost = 0; // NOUVEAU: Coût cumulé des challenges
1565
1615
 
1566
- // Simule le calcul du score pour chaque point de données avec la configuration testée
1616
+ let maxHumanScore = 0;
1617
+ let minBotScore = 100;
1618
+
1567
1619
  const calculateScore = (log) => {
1568
- // Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
1569
- // en utilisant les poids et les paramètres de `config`.
1570
- // Pour cet exemple, nous utilisons une version simplifiée.
1571
1620
  let score = 0;
1572
1621
  for (const key in config.weights) {
1573
1622
  score += (log.vector?.[key] || 0) * config.weights[key];
@@ -1575,57 +1624,78 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
1575
1624
  return score;
1576
1625
  };
1577
1626
 
1578
- // Pondération de la confiance : un challenge résolu est un signal humain plus fort.
1579
1627
  const confidenceWeights = {
1580
- request_passed: 0.7,
1581
- challenge_issued: 1.0,
1582
- request_blocked: 1.0,
1583
- challenge_solved: 1.5, // Signal "humain" de très haute confiance
1584
- trap_triggered: 2.0, // Signal "bot" de confiance absolue
1628
+ request_passed: 0.7,
1629
+ challenge_issued: 1.0,
1630
+ request_blocked: 1.0,
1631
+ challenge_solved: 1.5,
1632
+ trap_triggered: 2.0,
1585
1633
  };
1586
1634
 
1635
+ // 1. Évaluation sur les données de trafic réelles
1587
1636
  for (const log of trafficData) {
1588
- const confidence = confidenceWeights[log.type] || 1.0;
1589
-
1590
- // On se base sur le comportement observé pour déterminer la nature "réelle" de la requête
1591
- const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
1637
+ const weight = log.weight || 1.0;
1638
+ const confidence = (confidenceWeights[log.type] || 1.0) * weight;
1639
+ const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked' || log.type === 'trap_triggered';
1592
1640
  const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
1593
1641
 
1642
+ const score = calculateScore(log);
1643
+
1594
1644
  if (isLikelyBot) {
1595
- totalBots++;
1596
- const score = calculateScore(log);
1597
- // Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
1645
+ totalBots += confidence;
1646
+ minBotScore = Math.min(minBotScore, score);
1598
1647
  if (score < config.thresholds.low) {
1599
1648
  falseNegatives += confidence;
1600
- } else {
1601
- totalChallenges += confidence; // Un bot correctement challengé
1602
- // Calculer le coût de ce challenge (proportionnel à la difficulté)
1603
- const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1604
- totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1605
1649
  }
1606
1650
  } else if (isLikelyHuman) {
1607
- totalHumans++;
1608
- const score = calculateScore(log);
1609
- // Faux positif : un humain qui a été challengé inutilement
1651
+ totalHumans += confidence;
1652
+ maxHumanScore = Math.max(maxHumanScore, score);
1610
1653
  if (score >= config.thresholds.low) {
1611
1654
  falsePositives += confidence;
1612
- totalChallenges += confidence; // Un humain incorrectement challengé
1613
- const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1614
- totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ // 2. Évaluation sur les ancres immuables pour fixer l'échelle
1660
+ for (const anchor of STATIC_ANCHORS) {
1661
+ const score = calculateScore(anchor);
1662
+ const weight = anchor.weight;
1663
+
1664
+ if (anchor.type === 'request_blocked' || anchor.type === 'challenge_issued') {
1665
+ totalBots += weight;
1666
+ minBotScore = Math.min(minBotScore, score);
1667
+ if (score < config.thresholds.low) {
1668
+ falseNegatives += weight * 10; // Pénalité punitive forte
1669
+ }
1670
+ } else {
1671
+ totalHumans += weight;
1672
+ maxHumanScore = Math.max(maxHumanScore, score);
1673
+ if (score >= config.thresholds.low) {
1674
+ falsePositives += weight * 10; // Pénalité punitive forte
1615
1675
  }
1616
1676
  }
1617
1677
  }
1618
1678
 
1619
1679
  const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
1620
1680
  const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
1621
- // NOUVEAU: Taux de challenge global
1622
- const challengeRate = (totalHumans + totalBots) > 0 ? totalChallenges / (totalHumans + totalBots) : 0;
1623
- // NOUVEAU: Coût moyen par challenge émis
1624
- const averageChallengeCost = totalChallenges > 0 ? totalChallengeCost / totalChallenges : 0;
1625
-
1626
- // L'algorithme doit minimiser ces deux objectifs
1627
- // On a maintenant 4 objectifs à minimiser !
1628
- return [falsePositiveRate, falseNegativeRate, challengeRate, averageChallengeCost];
1681
+
1682
+ // 3. Pénalité de dérive d'échelle L2 (Régularisation par rapport au point d'origine)
1683
+ let regularizationPenalty = 0;
1684
+ if (currentConfig && currentConfig.weights) {
1685
+ for (const key in config.weights) {
1686
+ const originalVal = currentConfig.weights[key] || 0;
1687
+ regularizationPenalty += Math.pow(config.weights[key] - originalVal, 2);
1688
+ }
1689
+ }
1690
+
1691
+ // 4. Maximisation de la marge de séparation (SVM-like Margin Loss)
1692
+ const marginOverlap = Math.max(0, maxHumanScore - minBotScore);
1693
+ const marginPenalty = marginOverlap / 100;
1694
+
1695
+ const obj1 = falsePositiveRate + (regularizationPenalty * 0.05);
1696
+ const obj2 = falseNegativeRate + marginPenalty;
1697
+
1698
+ return [obj1, obj2];
1629
1699
  };
1630
1700
  };
1631
1701
 
@@ -154,6 +154,19 @@ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
154
154
  triggerFallback();
155
155
  }
156
156
  },
157
+ async clear() {
158
+ localStore.clear();
159
+ for (const timeout of localTimeouts.values()) {
160
+ clearTimeout(timeout);
161
+ }
162
+ localTimeouts.clear();
163
+ if (isDown) return;
164
+ try {
165
+ await collection.deleteMany({});
166
+ } catch (e) {
167
+ triggerFallback();
168
+ }
169
+ },
157
170
  async init() {
158
171
  try {
159
172
  // Automates index configuration
@@ -111,5 +111,18 @@ export function createRedisStore(redisClient) {
111
111
  triggerFallback();
112
112
  }
113
113
  },
114
+ async clear() {
115
+ localStore.clear();
116
+ for (const timeout of localTimeouts.values()) {
117
+ clearTimeout(timeout);
118
+ }
119
+ localTimeouts.clear();
120
+ if (isDown) return;
121
+ try {
122
+ await redisClient.flushdb();
123
+ } catch (e) {
124
+ triggerFallback();
125
+ }
126
+ }
114
127
  };
115
128
  }
@@ -1,78 +1,81 @@
1
- /**
2
- * @file Creates a store adapter for Knex.js.
3
- * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
- * It handles serialization of complex objects and TTL for automatic data expiration.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a Knex.js client.
9
- *
10
- * **Note:** You must create the table yourself before using the store.
11
- * The table should have at least the following columns:
12
- * - `key` (string, primary key)
13
- * - `value` (text or json/jsonb)
14
- * - `expiresAt` (datetime or timestamp with time zone)
15
- *
16
- * Example schema for PostgreSQL:
17
- * ```sql
18
- * CREATE TABLE your_table_name (
19
- * "key" VARCHAR(255) PRIMARY KEY,
20
- * "value" TEXT NOT NULL,
21
- * "expiresAt" TIMESTAMPTZ
22
- * );
23
- * ```
24
- *
25
- * @param {import('knex').Knex} knex - An instance of the Knex client.
26
- * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
- */
29
- export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
- // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
- const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
- const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
-
34
- return {
35
- async get(key) {
36
- const row = await knex(tableName).where('key', key).first();
37
- if (!row) return null;
38
-
39
- // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
- await this.delete(key); // Clean up expired key.
42
- return null;
43
- }
44
-
45
- try {
46
- return JSON.parse(row.value, reviver);
47
- } catch (e) {
48
- // In case of malformed JSON, treat it as a miss.
49
- return null;
50
- }
51
- },
52
-
53
- async set(key, value, ttl) {
54
- const stringValue = JSON.stringify(value, replacer);
55
- const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
-
57
- // Use native upsert capabilities of Knex for different SQL dialects.
58
- await knex(tableName)
59
- .insert({ key, value: stringValue, expiresAt })
60
- .onConflict('key')
61
- .merge();
62
- },
63
-
64
- async has(key) {
65
- const row = await knex(tableName).where('key', key).first('key');
66
- if (!row) return false;
67
- // Also check for expiration here.
68
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
- return false;
70
- }
71
- return true;
72
- },
73
-
74
- async delete(key) {
75
- await knex(tableName).where('key', key).del();
76
- },
77
- };
1
+ /**
2
+ * @file Creates a store adapter for Knex.js.
3
+ * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
+ * It handles serialization of complex objects and TTL for automatic data expiration.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a Knex.js client.
9
+ *
10
+ * **Note:** You must create the table yourself before using the store.
11
+ * The table should have at least the following columns:
12
+ * - `key` (string, primary key)
13
+ * - `value` (text or json/jsonb)
14
+ * - `expiresAt` (datetime or timestamp with time zone)
15
+ *
16
+ * Example schema for PostgreSQL:
17
+ * ```sql
18
+ * CREATE TABLE your_table_name (
19
+ * "key" VARCHAR(255) PRIMARY KEY,
20
+ * "value" TEXT NOT NULL,
21
+ * "expiresAt" TIMESTAMPTZ
22
+ * );
23
+ * ```
24
+ *
25
+ * @param {import('knex').Knex} knex - An instance of the Knex client.
26
+ * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
+ */
29
+ export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
+ // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
+ const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
+ const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
+
34
+ return {
35
+ async get(key) {
36
+ const row = await knex(tableName).where('key', key).first();
37
+ if (!row) return null;
38
+
39
+ // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
+ await this.delete(key); // Clean up expired key.
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(row.value, reviver);
47
+ } catch (e) {
48
+ // In case of malformed JSON, treat it as a miss.
49
+ return null;
50
+ }
51
+ },
52
+
53
+ async set(key, value, ttl) {
54
+ const stringValue = JSON.stringify(value, replacer);
55
+ const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
+
57
+ // Use native upsert capabilities of Knex for different SQL dialects.
58
+ await knex(tableName)
59
+ .insert({ key, value: stringValue, expiresAt })
60
+ .onConflict('key')
61
+ .merge();
62
+ },
63
+
64
+ async has(key) {
65
+ const row = await knex(tableName).where('key', key).first('key');
66
+ if (!row) return false;
67
+ // Also check for expiration here.
68
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
+ return false;
70
+ }
71
+ return true;
72
+ },
73
+
74
+ async delete(key) {
75
+ await knex(tableName).where('key', key).del();
76
+ },
77
+ async clear() {
78
+ await knex(tableName).del();
79
+ }
80
+ };
78
81
  }
@@ -92,8 +92,7 @@ describe('FingerprintEngine Challenge Validation', () => {
92
92
 
93
93
  // The client solves the challenge
94
94
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
95
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-A:`;
96
- const baseBlock = new TextEncoder().encode(messageBase);
95
+ const baseBlock = new Uint8Array(challengeContext.baseBlock);
97
96
 
98
97
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
99
98
  // The memory challenge seed does not include the fingerprint.
@@ -153,7 +152,7 @@ describe('FingerprintEngine Challenge Validation', () => {
153
152
  // --- 2. Second request: Solution is submitted from "Machine B" ---
154
153
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
155
154
  // The solver uses the fingerprint of the machine it's running on.
156
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:`;
155
+ const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:127.0.0.1::`;
157
156
  const baseBlock = new TextEncoder().encode(messageBase);
158
157
 
159
158
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
@@ -739,7 +739,7 @@ describe('Fingerprint & PoW Security Suite', () => {
739
739
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
740
740
 
741
741
  // The client constructs the base block and solves the challenge
742
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
742
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
743
743
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
744
744
  const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
745
745
 
@@ -821,7 +821,7 @@ describe('Fingerprint & PoW Security Suite', () => {
821
821
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
822
822
 
823
823
  // The server stores the challenge context. We need to ensure the `baseBlock` is present for verification.
824
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
824
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
825
825
  await inMemoryStore.set(`secret:${nonce}`, {
826
826
  clientSecret: clientSecret,
827
827
  cpuTarget: cpuTargetHex,
@@ -633,14 +633,14 @@ class ChallengeUtils
633
633
  /**
634
634
  * Crée le bloc de données de base pour le challenge CPU.
635
635
  */
636
- public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
636
+ public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
637
637
  {
638
638
  $parts = explode('|', $fingerprint);
639
639
  $filteredParts = array_filter($parts);
640
640
  sort($filteredParts);
641
641
  $sortedFingerprint = implode('|', $filteredParts);
642
642
 
643
- return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
643
+ return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
644
644
  }
645
645
 
646
646
  /**
@@ -58,6 +58,7 @@ class SecurityProfiles
58
58
  'decayFactor' => 0.9,
59
59
  'inactivityReset' => 5000,
60
60
  ],
61
+ 'filterWhitelist' => 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
61
62
  'wasm' => true,
62
63
  ],
63
64
 
@@ -108,6 +109,7 @@ class SecurityProfiles
108
109
  ],
109
110
  'challengeNewDevices' => true, // Challenge all new devices
110
111
  'wasm' => true,
112
+ 'filterWhitelist' => true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
111
113
  ],
112
114
 
113
115
  /**
@@ -155,6 +157,7 @@ class SecurityProfiles
155
157
  ],
156
158
  // This would be a callable in PHP, but for now, we represent its intent.
157
159
  'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
160
+ 'filterWhitelist' => 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
158
161
  'wasm' => true,
159
162
  ],
160
163
 
@@ -204,6 +207,7 @@ class SecurityProfiles
204
207
  'decayFactor' => 0.92,
205
208
  'inactivityReset' => 10000,
206
209
  ],
210
+ 'filterWhitelist' => 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
207
211
  'wasm' => true,
208
212
  ],
209
213
 
@@ -257,6 +261,7 @@ class SecurityProfiles
257
261
  // This would be a callable in PHP, but for now, we represent its intent.
258
262
  'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
259
263
  'wasm' => true,
264
+ 'filterWhitelist' => true, // Tolérance zéro pour le scraping / scalping distribué
260
265
  ],
261
266
  ];
262
267