hippius 0.2.30__tar.gz → 0.2.31__tar.gz
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.
- {hippius-0.2.30 → hippius-0.2.31}/PKG-INFO +1 -1
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/__init__.py +1 -1
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/client.py +5 -3
- hippius-0.2.31/hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql +34 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/ipfs.py +14 -11
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/key_storage.py +44 -79
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/substrate.py +0 -3
- {hippius-0.2.30 → hippius-0.2.31}/pyproject.toml +1 -1
- {hippius-0.2.30 → hippius-0.2.31}/README.md +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/cli.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/cli_assets.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/cli_handlers.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/cli_parser.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/cli_rich.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/config.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/db/README.md +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/db/env.db.template +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/db/migrations/20241201000001_create_key_storage_tables.sql +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/db/setup_database.sh +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/db_utils.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/errors.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/ipfs_core.py +0 -0
- {hippius-0.2.30 → hippius-0.2.31}/hippius_sdk/utils.py +0 -0
@@ -26,7 +26,7 @@ from hippius_sdk.config import (
|
|
26
26
|
from hippius_sdk.ipfs import IPFSClient, S3PublishResult, S3DownloadResult
|
27
27
|
from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
|
28
28
|
|
29
|
-
__version__ = "0.2.
|
29
|
+
__version__ = "0.2.31"
|
30
30
|
__all__ = [
|
31
31
|
"HippiusClient",
|
32
32
|
"IPFSClient",
|
@@ -516,6 +516,7 @@ class HippiusClient:
|
|
516
516
|
file_path: str,
|
517
517
|
encrypt: bool,
|
518
518
|
seed_phrase: str,
|
519
|
+
subaccount_id: str,
|
519
520
|
store_node: str = "http://localhost:5001",
|
520
521
|
pin_node: str = "https://store.hippius.network",
|
521
522
|
substrate_url: str = "wss://rpc.hippius.network",
|
@@ -548,6 +549,7 @@ class HippiusClient:
|
|
548
549
|
file_path,
|
549
550
|
encrypt,
|
550
551
|
seed_phrase,
|
552
|
+
subaccount_id,
|
551
553
|
store_node,
|
552
554
|
pin_node,
|
553
555
|
substrate_url,
|
@@ -557,7 +559,7 @@ class HippiusClient:
|
|
557
559
|
self,
|
558
560
|
cid: str,
|
559
561
|
output_path: str,
|
560
|
-
|
562
|
+
subaccount_id: str,
|
561
563
|
auto_decrypt: bool = True,
|
562
564
|
download_node: str = "http://localhost:5001",
|
563
565
|
) -> S3DownloadResult:
|
@@ -574,7 +576,7 @@ class HippiusClient:
|
|
574
576
|
Args:
|
575
577
|
cid: Content Identifier (CID) of the file to download
|
576
578
|
output_path: Path where the downloaded file will be saved
|
577
|
-
|
579
|
+
subaccount_id: The subaccount id as api key
|
578
580
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
579
581
|
download_node: IPFS node URL for download (default: local node)
|
580
582
|
|
@@ -587,5 +589,5 @@ class HippiusClient:
|
|
587
589
|
ValueError: If decryption fails
|
588
590
|
"""
|
589
591
|
return await self.ipfs_client.s3_download(
|
590
|
-
cid, output_path,
|
592
|
+
cid, output_path, subaccount_id, auto_decrypt, download_node
|
591
593
|
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
-- migrate:up
|
2
|
+
|
3
|
+
-- For security reasons, completely drop all existing tables and data
|
4
|
+
-- This removes any stored seed phrases from the database
|
5
|
+
DROP TABLE IF EXISTS encryption_keys CASCADE;
|
6
|
+
DROP TABLE IF EXISTS seed_phrases CASCADE;
|
7
|
+
|
8
|
+
-- Create new simplified schema using only subaccount_id
|
9
|
+
-- No seed phrases are stored in the database anymore
|
10
|
+
CREATE TABLE encryption_keys (
|
11
|
+
id SERIAL PRIMARY KEY,
|
12
|
+
subaccount_id VARCHAR(255) NOT NULL,
|
13
|
+
encryption_key_b64 TEXT NOT NULL,
|
14
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
15
|
+
);
|
16
|
+
|
17
|
+
-- Index for efficient lookups of latest encryption key per subaccount
|
18
|
+
CREATE INDEX idx_encryption_keys_subaccount_created
|
19
|
+
ON encryption_keys(subaccount_id, created_at DESC);
|
20
|
+
|
21
|
+
-- Comments for documentation
|
22
|
+
COMMENT ON TABLE encryption_keys IS 'Stores versioned encryption keys per subaccount ID (never deleted, always use most recent)';
|
23
|
+
COMMENT ON COLUMN encryption_keys.subaccount_id IS 'Subaccount identifier for key association';
|
24
|
+
COMMENT ON COLUMN encryption_keys.encryption_key_b64 IS 'Base64 encoded encryption key';
|
25
|
+
|
26
|
+
-- migrate:down
|
27
|
+
|
28
|
+
-- Drop new table
|
29
|
+
DROP INDEX IF EXISTS idx_encryption_keys_subaccount_created;
|
30
|
+
DROP TABLE IF EXISTS encryption_keys;
|
31
|
+
|
32
|
+
-- Note: We do NOT recreate the old seed_phrases table in the down migration
|
33
|
+
-- This is intentional for security - once we've removed seed phrases from the DB,
|
34
|
+
-- we don't want to accidentally restore them
|
@@ -21,8 +21,8 @@ from hippius_sdk.config import get_config_value, get_encryption_key
|
|
21
21
|
from hippius_sdk.errors import HippiusIPFSError, HippiusSubstrateError
|
22
22
|
from hippius_sdk.ipfs_core import AsyncIPFSClient
|
23
23
|
from hippius_sdk.key_storage import (
|
24
|
-
|
25
|
-
|
24
|
+
generate_and_store_key_for_subaccount,
|
25
|
+
get_key_for_subaccount,
|
26
26
|
is_key_storage_enabled,
|
27
27
|
)
|
28
28
|
from hippius_sdk.substrate import FileInput, SubstrateClient
|
@@ -1939,6 +1939,7 @@ class IPFSClient:
|
|
1939
1939
|
file_path: str,
|
1940
1940
|
encrypt: bool,
|
1941
1941
|
seed_phrase: str,
|
1942
|
+
subaccount_id: str,
|
1942
1943
|
store_node: str = "http://localhost:5001",
|
1943
1944
|
pin_node: str = "https://store.hippius.network",
|
1944
1945
|
substrate_url: str = "wss://rpc.hippius.network",
|
@@ -1994,17 +1995,19 @@ class IPFSClient:
|
|
1994
1995
|
|
1995
1996
|
if key_storage_available:
|
1996
1997
|
# Try to get existing key for this seed phrase
|
1997
|
-
existing_key_b64 = await
|
1998
|
+
existing_key_b64 = await get_key_for_subaccount(subaccount_id)
|
1998
1999
|
|
1999
2000
|
if existing_key_b64:
|
2000
2001
|
# Use existing key
|
2001
|
-
logger.debug("Using existing encryption key for
|
2002
|
+
logger.debug("Using existing encryption key for subaccount")
|
2002
2003
|
encryption_key_bytes = base64.b64decode(existing_key_b64)
|
2003
2004
|
encryption_key_used = existing_key_b64
|
2004
2005
|
else:
|
2005
|
-
# Generate and store new key for this
|
2006
|
-
logger.info("Generating new encryption key for
|
2007
|
-
new_key_b64 = await
|
2006
|
+
# Generate and store new key for this subaccount
|
2007
|
+
logger.info("Generating new encryption key for subaccount")
|
2008
|
+
new_key_b64 = await generate_and_store_key_for_subaccount(
|
2009
|
+
subaccount_id
|
2010
|
+
)
|
2008
2011
|
encryption_key_bytes = base64.b64decode(new_key_b64)
|
2009
2012
|
encryption_key_used = new_key_b64
|
2010
2013
|
|
@@ -2120,7 +2123,7 @@ class IPFSClient:
|
|
2120
2123
|
self,
|
2121
2124
|
cid: str,
|
2122
2125
|
output_path: str,
|
2123
|
-
|
2126
|
+
subaccount_id: str,
|
2124
2127
|
auto_decrypt: bool = True,
|
2125
2128
|
download_node: str = "http://localhost:5001",
|
2126
2129
|
) -> S3DownloadResult:
|
@@ -2137,7 +2140,7 @@ class IPFSClient:
|
|
2137
2140
|
Args:
|
2138
2141
|
cid: Content Identifier (CID) of the file to download
|
2139
2142
|
output_path: Path where the downloaded file will be saved
|
2140
|
-
|
2143
|
+
subaccount_id: The subaccount id as api key
|
2141
2144
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
2142
2145
|
download_node: IPFS node URL for download (default: local node)
|
2143
2146
|
|
@@ -2218,11 +2221,11 @@ class IPFSClient:
|
|
2218
2221
|
if key_storage_available:
|
2219
2222
|
# Try to get the encryption key for this seed phrase
|
2220
2223
|
try:
|
2221
|
-
existing_key_b64 = await
|
2224
|
+
existing_key_b64 = await get_key_for_subaccount(subaccount_id)
|
2222
2225
|
|
2223
2226
|
if existing_key_b64:
|
2224
2227
|
logger.debug(
|
2225
|
-
"Found encryption key for
|
2228
|
+
"Found encryption key for subaccount, attempting decryption"
|
2226
2229
|
)
|
2227
2230
|
decryption_attempted = True
|
2228
2231
|
encryption_key_used = existing_key_b64
|
@@ -1,15 +1,12 @@
|
|
1
1
|
"""
|
2
|
-
Key storage module for managing encryption keys per
|
2
|
+
Key storage module for managing encryption keys per subaccount ID.
|
3
3
|
|
4
4
|
This module provides PostgreSQL-backed storage for:
|
5
|
-
1.
|
6
|
-
2. Encryption keys associated with each seed phrase (versioned, never deleted)
|
5
|
+
1. Encryption keys associated with each subaccount ID (versioned, never deleted)
|
7
6
|
"""
|
8
7
|
|
9
8
|
import base64
|
10
9
|
import hashlib
|
11
|
-
import os
|
12
|
-
from datetime import datetime
|
13
10
|
from typing import Optional
|
14
11
|
|
15
12
|
from hippius_sdk.config import get_config_value
|
@@ -30,7 +27,7 @@ class KeyStorageError(Exception):
|
|
30
27
|
|
31
28
|
|
32
29
|
class KeyStorage:
|
33
|
-
"""PostgreSQL-backed key storage for
|
30
|
+
"""PostgreSQL-backed key storage for subaccount encryption keys."""
|
34
31
|
|
35
32
|
def __init__(self, database_url: Optional[str] = None):
|
36
33
|
"""
|
@@ -62,34 +59,23 @@ class KeyStorage:
|
|
62
59
|
|
63
60
|
async def _ensure_tables_exist(self):
|
64
61
|
"""Create tables if they don't exist."""
|
65
|
-
create_seed_phrases_table = """
|
66
|
-
CREATE TABLE IF NOT EXISTS seed_phrases (
|
67
|
-
id SERIAL PRIMARY KEY,
|
68
|
-
seed_hash VARCHAR(64) UNIQUE NOT NULL,
|
69
|
-
seed_phrase_b64 TEXT NOT NULL,
|
70
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
71
|
-
);
|
72
|
-
"""
|
73
|
-
|
74
62
|
create_encryption_keys_table = """
|
75
63
|
CREATE TABLE IF NOT EXISTS encryption_keys (
|
76
64
|
id SERIAL PRIMARY KEY,
|
77
|
-
|
65
|
+
subaccount_id VARCHAR(255) NOT NULL,
|
78
66
|
encryption_key_b64 TEXT NOT NULL,
|
79
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
80
|
-
FOREIGN KEY (seed_hash) REFERENCES seed_phrases(seed_hash)
|
67
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
81
68
|
);
|
82
69
|
"""
|
83
70
|
|
84
71
|
create_index = """
|
85
|
-
CREATE INDEX IF NOT EXISTS
|
86
|
-
ON encryption_keys(
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_encryption_keys_subaccount_created
|
73
|
+
ON encryption_keys(subaccount_id, created_at DESC);
|
87
74
|
"""
|
88
75
|
|
89
76
|
try:
|
90
77
|
conn = await self._get_connection()
|
91
78
|
try:
|
92
|
-
await conn.execute(create_seed_phrases_table)
|
93
79
|
await conn.execute(create_encryption_keys_table)
|
94
80
|
await conn.execute(create_index)
|
95
81
|
finally:
|
@@ -97,59 +83,37 @@ class KeyStorage:
|
|
97
83
|
except Exception as e:
|
98
84
|
raise KeyStorageError(f"Failed to create tables: {e}")
|
99
85
|
|
100
|
-
def
|
101
|
-
"""Create a SHA-256 hash of the
|
102
|
-
return hashlib.sha256(
|
103
|
-
|
104
|
-
async def _ensure_seed_phrase_exists(self, seed_phrase: str) -> str:
|
105
|
-
"""Ensure seed phrase exists in database and return its hash."""
|
106
|
-
seed_hash = self._hash_seed_phrase(seed_phrase)
|
107
|
-
seed_phrase_b64 = base64.b64encode(seed_phrase.encode("utf-8")).decode("utf-8")
|
108
|
-
|
109
|
-
try:
|
110
|
-
conn = await self._get_connection()
|
111
|
-
try:
|
112
|
-
# Try to insert, ignore if already exists
|
113
|
-
await conn.execute(
|
114
|
-
"""
|
115
|
-
INSERT INTO seed_phrases (seed_hash, seed_phrase_b64)
|
116
|
-
VALUES ($1, $2)
|
117
|
-
ON CONFLICT (seed_hash) DO NOTHING
|
118
|
-
""",
|
119
|
-
seed_hash,
|
120
|
-
seed_phrase_b64,
|
121
|
-
)
|
122
|
-
finally:
|
123
|
-
await conn.close()
|
124
|
-
return seed_hash
|
125
|
-
except Exception as e:
|
126
|
-
raise KeyStorageError(f"Failed to store seed phrase: {e}")
|
86
|
+
def _hash_subaccount_id(self, subaccount_id: str) -> str:
|
87
|
+
"""Create a SHA-256 hash of the subaccount ID for indexing."""
|
88
|
+
return hashlib.sha256(subaccount_id.encode("utf-8")).hexdigest()
|
127
89
|
|
128
|
-
async def
|
90
|
+
async def set_key_for_subaccount(
|
91
|
+
self, subaccount_id: str, encryption_key_b64: str
|
92
|
+
) -> None:
|
129
93
|
"""
|
130
|
-
Store a new encryption key for a
|
94
|
+
Store a new encryption key for a subaccount.
|
131
95
|
|
132
96
|
Creates a new row (doesn't update existing ones) to maintain key history.
|
133
97
|
|
134
98
|
Args:
|
135
|
-
|
99
|
+
subaccount_id: The subaccount identifier
|
136
100
|
encryption_key_b64: Base64-encoded encryption key
|
137
101
|
|
138
102
|
Raises:
|
139
103
|
KeyStorageError: If storage fails
|
140
104
|
"""
|
141
105
|
await self._ensure_tables_exist()
|
142
|
-
|
106
|
+
subaccount_hash = self._hash_subaccount_id(subaccount_id)
|
143
107
|
|
144
108
|
try:
|
145
109
|
conn = await self._get_connection()
|
146
110
|
try:
|
147
111
|
await conn.execute(
|
148
112
|
"""
|
149
|
-
INSERT INTO encryption_keys (
|
113
|
+
INSERT INTO encryption_keys (subaccount_id, encryption_key_b64)
|
150
114
|
VALUES ($1, $2)
|
151
115
|
""",
|
152
|
-
|
116
|
+
subaccount_hash,
|
153
117
|
encryption_key_b64,
|
154
118
|
)
|
155
119
|
finally:
|
@@ -157,12 +121,12 @@ class KeyStorage:
|
|
157
121
|
except Exception as e:
|
158
122
|
raise KeyStorageError(f"Failed to store encryption key: {e}")
|
159
123
|
|
160
|
-
async def
|
124
|
+
async def get_key_for_subaccount(self, subaccount_id: str) -> Optional[str]:
|
161
125
|
"""
|
162
|
-
Get the most recent encryption key for a
|
126
|
+
Get the most recent encryption key for a subaccount.
|
163
127
|
|
164
128
|
Args:
|
165
|
-
|
129
|
+
subaccount_id: The subaccount identifier
|
166
130
|
|
167
131
|
Returns:
|
168
132
|
Base64-encoded encryption key or None if not found
|
@@ -171,7 +135,7 @@ class KeyStorage:
|
|
171
135
|
KeyStorageError: If database operation fails
|
172
136
|
"""
|
173
137
|
await self._ensure_tables_exist()
|
174
|
-
|
138
|
+
subaccount_hash = self._hash_subaccount_id(subaccount_id)
|
175
139
|
|
176
140
|
try:
|
177
141
|
conn = await self._get_connection()
|
@@ -180,11 +144,11 @@ class KeyStorage:
|
|
180
144
|
"""
|
181
145
|
SELECT encryption_key_b64
|
182
146
|
FROM encryption_keys
|
183
|
-
WHERE
|
147
|
+
WHERE subaccount_id = $1
|
184
148
|
ORDER BY created_at DESC
|
185
149
|
LIMIT 1
|
186
150
|
""",
|
187
|
-
|
151
|
+
subaccount_hash,
|
188
152
|
)
|
189
153
|
|
190
154
|
return result["encryption_key_b64"] if result else None
|
@@ -193,12 +157,12 @@ class KeyStorage:
|
|
193
157
|
except Exception as e:
|
194
158
|
raise KeyStorageError(f"Failed to retrieve encryption key: {e}")
|
195
159
|
|
196
|
-
async def
|
160
|
+
async def generate_and_store_key_for_subaccount(self, subaccount_id: str) -> str:
|
197
161
|
"""
|
198
|
-
Generate a new encryption key and store it for the
|
162
|
+
Generate a new encryption key and store it for the subaccount.
|
199
163
|
|
200
164
|
Args:
|
201
|
-
|
165
|
+
subaccount_id: The subaccount identifier
|
202
166
|
|
203
167
|
Returns:
|
204
168
|
Base64-encoded encryption key that was generated and stored
|
@@ -206,17 +170,14 @@ class KeyStorage:
|
|
206
170
|
Raises:
|
207
171
|
KeyStorageError: If generation or storage fails
|
208
172
|
"""
|
209
|
-
# Generate a new encryption key
|
210
173
|
try:
|
211
174
|
import nacl.secret
|
212
175
|
import nacl.utils
|
213
176
|
|
214
|
-
# Generate a random key
|
215
177
|
key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
|
216
178
|
key_b64 = base64.b64encode(key).decode("utf-8")
|
217
179
|
|
218
|
-
|
219
|
-
await self.set_key_for_seed(seed_phrase, key_b64)
|
180
|
+
await self.set_key_for_subaccount(subaccount_id, key_b64)
|
220
181
|
|
221
182
|
return key_b64
|
222
183
|
except ImportError:
|
@@ -261,38 +222,42 @@ def get_default_storage() -> KeyStorage:
|
|
261
222
|
return _default_storage
|
262
223
|
|
263
224
|
|
264
|
-
async def
|
225
|
+
async def get_key_for_subaccount(subaccount_id: str) -> Optional[str]:
|
265
226
|
"""
|
266
|
-
Get the most recent encryption key for a
|
227
|
+
Get the most recent encryption key for a subaccount.
|
267
228
|
|
268
229
|
Args:
|
269
|
-
|
230
|
+
subaccount_id: The subaccount identifier
|
270
231
|
|
271
232
|
Returns:
|
272
233
|
Base64-encoded encryption key or None if not found
|
273
234
|
"""
|
274
|
-
return await get_default_storage().
|
235
|
+
return await get_default_storage().get_key_for_subaccount(subaccount_id)
|
275
236
|
|
276
237
|
|
277
|
-
async def
|
238
|
+
async def set_key_for_subaccount(subaccount_id: str, encryption_key_b64: str) -> None:
|
278
239
|
"""
|
279
|
-
Store a new encryption key for a
|
240
|
+
Store a new encryption key for a subaccount.
|
280
241
|
|
281
242
|
Args:
|
282
|
-
|
243
|
+
subaccount_id: The subaccount identifier
|
283
244
|
encryption_key_b64: Base64-encoded encryption key
|
284
245
|
"""
|
285
|
-
return await get_default_storage().
|
246
|
+
return await get_default_storage().set_key_for_subaccount(
|
247
|
+
subaccount_id, encryption_key_b64
|
248
|
+
)
|
286
249
|
|
287
250
|
|
288
|
-
async def
|
251
|
+
async def generate_and_store_key_for_subaccount(subaccount_id: str) -> str:
|
289
252
|
"""
|
290
|
-
Generate a new encryption key and store it for the
|
253
|
+
Generate a new encryption key and store it for the subaccount.
|
291
254
|
|
292
255
|
Args:
|
293
|
-
|
256
|
+
subaccount_id: The subaccount identifier
|
294
257
|
|
295
258
|
Returns:
|
296
259
|
Base64-encoded encryption key that was generated and stored
|
297
260
|
"""
|
298
|
-
return await get_default_storage().
|
261
|
+
return await get_default_storage().generate_and_store_key_for_subaccount(
|
262
|
+
subaccount_id
|
263
|
+
)
|
@@ -1270,7 +1270,6 @@ class SubstrateClient:
|
|
1270
1270
|
return result.value
|
1271
1271
|
return None
|
1272
1272
|
|
1273
|
-
|
1274
1273
|
def is_main_account(self, account_id: str, seed_phrase: str) -> bool:
|
1275
1274
|
sub_account = self.query_sub_account(account_id, seed_phrase=seed_phrase)
|
1276
1275
|
return sub_account is None
|
@@ -1288,8 +1287,6 @@ class SubstrateClient:
|
|
1288
1287
|
# Return the u128 value (converted to int for Python compatibility)
|
1289
1288
|
return int(result.value) if result and result.value is not None else 0
|
1290
1289
|
|
1291
|
-
|
1292
|
-
|
1293
1290
|
def get_account_roles(self, account_id: str, seed_phrase) -> int:
|
1294
1291
|
if not self._substrate:
|
1295
1292
|
self.connect(seed_phrase)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|