orionis 0.545.0__py3-none-any.whl → 0.547.0__py3-none-any.whl

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.
Files changed (162) hide show
  1. orionis/console/base/scheduler_event_listener.py +0 -17
  2. orionis/console/contracts/schedule_event_listener.py +0 -18
  3. orionis/console/request/cli_request.py +2 -2
  4. orionis/foundation/config/app/entities/app.py +3 -2
  5. orionis/foundation/config/app/enums/ciphers.py +5 -19
  6. orionis/foundation/config/session/entities/session.py +2 -2
  7. orionis/foundation/providers/cli_request_provider.py +44 -0
  8. orionis/metadata/framework.py +1 -1
  9. orionis/services/encrypter/encrypter.py +115 -0
  10. orionis/services/environment/dynamic/caster.py +35 -27
  11. orionis/services/environment/key/key_generator.py +32 -11
  12. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/METADATA +1 -1
  13. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/RECORD +17 -160
  14. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/top_level.txt +0 -1
  15. tests/container/__init__.py +0 -0
  16. tests/container/context/__init__.py +0 -0
  17. tests/container/context/test_manager.py +0 -38
  18. tests/container/context/test_scope.py +0 -32
  19. tests/container/core/__init__.py +0 -0
  20. tests/container/core/test_advanced_async.py +0 -234
  21. tests/container/core/test_async_optimizations.py +0 -268
  22. tests/container/core/test_container.py +0 -453
  23. tests/container/core/test_singleton.py +0 -122
  24. tests/container/core/test_thread_safety.py +0 -90
  25. tests/container/entities/__init__.py +0 -0
  26. tests/container/entities/test_binding.py +0 -242
  27. tests/container/enums/__init__.py +0 -0
  28. tests/container/enums/test_lifetimes.py +0 -97
  29. tests/container/facades/__init__.py +0 -0
  30. tests/container/facades/test_facade.py +0 -78
  31. tests/container/mocks/__init__.py +0 -0
  32. tests/container/mocks/mock_advanced_async.py +0 -332
  33. tests/container/mocks/mock_async_optimizations.py +0 -407
  34. tests/container/mocks/mock_auto_resolution.py +0 -192
  35. tests/container/mocks/mock_complex_classes.py +0 -792
  36. tests/container/mocks/mock_simple_classes.py +0 -98
  37. tests/container/providers/__init__.py +0 -0
  38. tests/container/providers/test_providers.py +0 -55
  39. tests/container/validators/__init__.py +0 -0
  40. tests/container/validators/test_implements.py +0 -186
  41. tests/container/validators/test_is_abstract_class.py +0 -147
  42. tests/container/validators/test_is_callable.py +0 -102
  43. tests/container/validators/test_is_concrete_class.py +0 -160
  44. tests/container/validators/test_is_instance.py +0 -150
  45. tests/container/validators/test_is_not_subclass.py +0 -49
  46. tests/container/validators/test_is_subclass.py +0 -178
  47. tests/container/validators/test_is_valid_alias.py +0 -147
  48. tests/container/validators/test_lifetime.py +0 -106
  49. tests/example/__init__.py +0 -0
  50. tests/example/test_example.py +0 -725
  51. tests/foundation/__init__.py +0 -0
  52. tests/foundation/config/__init__.py +0 -0
  53. tests/foundation/config/app/__init__.py +0 -0
  54. tests/foundation/config/app/test_foundation_config_app.py +0 -262
  55. tests/foundation/config/auth/__init__.py +0 -0
  56. tests/foundation/config/auth/test_foundation_config_auth.py +0 -29
  57. tests/foundation/config/cache/__init__.py +0 -0
  58. tests/foundation/config/cache/test_foundation_config_cache.py +0 -143
  59. tests/foundation/config/cache/test_foundation_config_cache_file.py +0 -126
  60. tests/foundation/config/cache/test_foundation_config_cache_stores.py +0 -156
  61. tests/foundation/config/cors/__init__.py +0 -0
  62. tests/foundation/config/cors/test_foundation_config_cors.py +0 -190
  63. tests/foundation/config/database/__init__.py +0 -0
  64. tests/foundation/config/database/test_foundation_config_database.py +0 -158
  65. tests/foundation/config/database/test_foundation_config_database_connections.py +0 -203
  66. tests/foundation/config/database/test_foundation_config_database_mysql.py +0 -354
  67. tests/foundation/config/database/test_foundation_config_database_oracle.py +0 -288
  68. tests/foundation/config/database/test_foundation_config_database_pgsql.py +0 -257
  69. tests/foundation/config/database/test_foundation_config_database_sqlite.py +0 -207
  70. tests/foundation/config/filesystems/__init__.py +0 -0
  71. tests/foundation/config/filesystems/test_foundation_config_filesystems.py +0 -160
  72. tests/foundation/config/filesystems/test_foundation_config_filesystems_aws.py +0 -189
  73. tests/foundation/config/filesystems/test_foundation_config_filesystems_disks.py +0 -184
  74. tests/foundation/config/filesystems/test_foundation_config_filesystems_local.py +0 -143
  75. tests/foundation/config/filesystems/test_foundation_config_filesystems_public.py +0 -184
  76. tests/foundation/config/logging/__init__.py +0 -0
  77. tests/foundation/config/logging/test_foundation_config_logging.py +0 -112
  78. tests/foundation/config/logging/test_foundation_config_logging_channels.py +0 -246
  79. tests/foundation/config/logging/test_foundation_config_logging_chunked.py +0 -217
  80. tests/foundation/config/logging/test_foundation_config_logging_daily.py +0 -220
  81. tests/foundation/config/logging/test_foundation_config_logging_hourly.py +0 -196
  82. tests/foundation/config/logging/test_foundation_config_logging_monthly.py +0 -214
  83. tests/foundation/config/logging/test_foundation_config_logging_stack.py +0 -178
  84. tests/foundation/config/logging/test_foundation_config_logging_weekly.py +0 -224
  85. tests/foundation/config/mail/__init__.py +0 -0
  86. tests/foundation/config/mail/test_foundation_config_mail.py +0 -145
  87. tests/foundation/config/mail/test_foundation_config_mail_file.py +0 -97
  88. tests/foundation/config/mail/test_foundation_config_mail_mailers.py +0 -106
  89. tests/foundation/config/mail/test_foundation_config_mail_smtp.py +0 -146
  90. tests/foundation/config/queue/__init__.py +0 -0
  91. tests/foundation/config/queue/test_foundation_config_queue.py +0 -88
  92. tests/foundation/config/queue/test_foundation_config_queue_brokers.py +0 -72
  93. tests/foundation/config/queue/test_foundation_config_queue_database.py +0 -134
  94. tests/foundation/config/root/__init__.py +0 -0
  95. tests/foundation/config/root/test_foundation_config_root_paths.py +0 -112
  96. tests/foundation/config/session/__init__.py +0 -0
  97. tests/foundation/config/session/test_foundation_config_session.py +0 -213
  98. tests/foundation/config/startup/__init__.py +0 -0
  99. tests/foundation/config/startup/test_foundation_config_startup.py +0 -202
  100. tests/foundation/config/testing/__init__.py +0 -0
  101. tests/foundation/config/testing/test_foundation_config_testing.py +0 -235
  102. tests/metadata/__init__.py +0 -0
  103. tests/metadata/test_metadata_framework.py +0 -140
  104. tests/metadata/test_metadata_package.py +0 -139
  105. tests/services/__init__.py +0 -0
  106. tests/services/asynchrony/__init__.py +0 -0
  107. tests/services/asynchrony/test_services_asynchrony_coroutine.py +0 -85
  108. tests/services/environment/__init__.py +0 -0
  109. tests/services/environment/test_services_environment.py +0 -226
  110. tests/services/introspection/__init__.py +0 -0
  111. tests/services/introspection/dependencies/__init__.py +0 -0
  112. tests/services/introspection/dependencies/mocks/__init__.py +0 -0
  113. tests/services/introspection/dependencies/mocks/mock_user.py +0 -30
  114. tests/services/introspection/dependencies/mocks/mock_user_controller.py +0 -27
  115. tests/services/introspection/dependencies/mocks/mock_users_permissions.py +0 -41
  116. tests/services/introspection/dependencies/test_reflect_dependencies.py +0 -261
  117. tests/services/introspection/reflection/__init__.py +0 -0
  118. tests/services/introspection/reflection/mock/__init__.py +0 -0
  119. tests/services/introspection/reflection/mock/fake_reflect_instance.py +0 -1115
  120. tests/services/introspection/reflection/test_reflection_abstract.py +0 -1011
  121. tests/services/introspection/reflection/test_reflection_callable.py +0 -206
  122. tests/services/introspection/reflection/test_reflection_concrete.py +0 -952
  123. tests/services/introspection/reflection/test_reflection_instance.py +0 -1233
  124. tests/services/introspection/reflection/test_reflection_module.py +0 -567
  125. tests/services/introspection/test_reflection.py +0 -462
  126. tests/services/log/__init__.py +0 -0
  127. tests/services/log/test_log.py +0 -97
  128. tests/services/system/__init__.py +0 -0
  129. tests/services/system/test_services_system_imports.py +0 -204
  130. tests/services/system/test_services_system_workers.py +0 -131
  131. tests/support/__init__.py +0 -0
  132. tests/support/entities/__init__.py +0 -0
  133. tests/support/entities/mock_dataclass.py +0 -40
  134. tests/support/entities/test_base.py +0 -64
  135. tests/support/patterns/__init__.py +0 -0
  136. tests/support/patterns/singleton/__init__.py +0 -0
  137. tests/support/patterns/singleton/test_patterns_singleton.py +0 -39
  138. tests/support/standard/__init__.py +0 -0
  139. tests/support/standard/test_services_std.py +0 -226
  140. tests/support/wrapper/__init__.py +0 -0
  141. tests/support/wrapper/test_services_wrapper_docdict.py +0 -202
  142. tests/testing/__init__.py +0 -0
  143. tests/testing/cases/__init__.py +0 -0
  144. tests/testing/cases/test_testing_asynchronous.py +0 -63
  145. tests/testing/cases/test_testing_synchronous.py +0 -57
  146. tests/testing/entities/__init__.py +0 -0
  147. tests/testing/entities/test_testing_result.py +0 -146
  148. tests/testing/enums/__init__.py +0 -0
  149. tests/testing/enums/test_testing_status.py +0 -63
  150. tests/testing/output/__init__.py +0 -0
  151. tests/testing/output/test_testing_dumper.py +0 -29
  152. tests/testing/output/test_testing_printer.py +0 -42
  153. tests/testing/records/__init__.py +0 -0
  154. tests/testing/records/test_testing_records.py +0 -171
  155. tests/testing/test_testing_unit.py +0 -164
  156. tests/testing/validators/__init__.py +0 -0
  157. tests/testing/validators/test_testing_validators.py +0 -392
  158. tests/testing/view/__init__.py +0 -0
  159. tests/testing/view/test_render.py +0 -30
  160. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/WHEEL +0 -0
  161. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/licenses/LICENCE +0 -0
  162. {orionis-0.545.0.dist-info → orionis-0.547.0.dist-info}/zip-safe +0 -0
@@ -44,23 +44,6 @@ class BaseScheduleEventListener(IScheduleEventListener):
44
44
  """
45
45
  pass # Placeholder for post-job execution logic
46
46
 
47
- async def onSuccess(self, event: EventJob, schedule):
48
- """
49
- Called when a job is successfully executed.
50
-
51
- Parameters
52
- ----------
53
- event : EventJob
54
- The successful job execution event containing details about the job.
55
- schedule : ISchedule
56
- The associated schedule instance managing the job.
57
-
58
- Returns
59
- -------
60
- None
61
- """
62
- pass # Placeholder for logic to handle successful job execution
63
-
64
47
  async def onFailure(self, event: EventJob, schedule):
65
48
  """
66
49
  Called when a job execution fails.
@@ -43,24 +43,6 @@ class IScheduleEventListener(ABC):
43
43
  """
44
44
  pass
45
45
 
46
- @abstractmethod
47
- async def onSuccess(self, event: EventJob, schedule):
48
- """
49
- Called when a job is successfully executed.
50
-
51
- Parameters
52
- ----------
53
- event : EventJob
54
- The successful job execution event containing details about the job.
55
- schedule : ISchedule
56
- The associated schedule instance managing the job.
57
-
58
- Returns
59
- -------
60
- None
61
- """
62
- pass
63
-
64
46
  @abstractmethod
65
47
  async def onFailure(self, event: EventJob, schedule):
66
48
  """
@@ -100,8 +100,8 @@ class CLIRequest(ICLIRequest):
100
100
 
101
101
  def __init__(
102
102
  self,
103
- command: str,
104
- args: dict
103
+ command: str = "__unknown__",
104
+ args: dict = {}
105
105
  ):
106
106
  """
107
107
  Initialize a CLI request object with command line arguments.
@@ -237,8 +237,9 @@ class App(BaseEntity):
237
237
  # Validate `key` attribute
238
238
  if self.key is None:
239
239
  self.key = SecureKeyGenerator.generate()
240
- if not isinstance(self.key, str):
241
- raise OrionisIntegrityException("The 'key' attribute must be a string.")
240
+ Env.set('APP_KEY', self.key)
241
+ if not isinstance(self.key, (bytes, str)) or not self.key.strip():
242
+ raise OrionisIntegrityException("The 'key' attribute must be a non-empty string.")
242
243
 
243
244
  # Validate `maintenance` attribute
244
245
  if not isinstance(self.maintenance, str) or not self.name.strip() or not self.maintenance.startswith('/'):
@@ -8,27 +8,13 @@ class Cipher(Enum):
8
8
  commonly used for encryption and decryption operations.
9
9
 
10
10
  Members:
11
- AES_128_CBC: AES with 128-bit key in Cipher Block Chaining (CBC) mode.
12
- AES_192_CBC: AES with 192-bit key in CBC mode.
13
- AES_256_CBC: AES with 256-bit key in CBC mode.
14
- AES_128_GCM: AES with 128-bit key in Galois/Counter Mode (GCM).
15
- AES_256_GCM: AES with 256-bit key in GCM.
16
- AES_CTR: AES in Counter (CTR) mode.
17
- AES_CFB: AES in Cipher Feedback (CFB) mode.
18
- AES_CFB8: AES in CFB mode with 8-bit feedback.
19
- AES_CFB128: AES in CFB mode with 128-bit feedback.
20
- AES_OFB: AES in Output Feedback (OFB) mode.
21
- AES_ECB: AES in Electronic Codebook (ECB) mode.
11
+ - AES_128_CBC: AES with a 128-bit key in Cipher Block Chaining (CBC) mode.
12
+ - AES_256_CBC: AES with a 256-bit key in Cipher Block Chaining (CBC) mode.
13
+ - AES_128_GCM: AES with a 128-bit key in Galois/Counter Mode (GCM).
14
+ - AES_256_GCM: AES with a 256-bit key in Galois/Counter Mode (GCM).
22
15
  """
23
16
 
24
17
  AES_128_CBC = "AES-128-CBC"
25
- AES_192_CBC = "AES-192-CBC"
26
18
  AES_256_CBC = "AES-256-CBC"
27
19
  AES_128_GCM = "AES-128-GCM"
28
- AES_256_GCM = "AES-256-GCM"
29
- AES_CTR = "AES-CTR"
30
- AES_CFB = "AES-CFB"
31
- AES_CFB8 = "AES-CFB8"
32
- AES_CFB128 = "AES-CFB128"
33
- AES_OFB = "AES-OFB"
34
- AES_ECB = "AES-ECB"
20
+ AES_256_GCM = "AES-256-GCM"
@@ -94,8 +94,8 @@ class Session(BaseEntity):
94
94
  # Validate secret_key
95
95
  if self.secret_key is None:
96
96
  self.secret_key = SecretKey.random()
97
- if not isinstance(self.secret_key, str) or not self.secret_key.strip():
98
- raise OrionisIntegrityException("Session secret_key must be a non-empty string")
97
+ if not isinstance(self.secret_key, (bytes, str)) or not self.secret_key.strip():
98
+ raise OrionisIntegrityException("secret_key must be a non-empty string")
99
99
 
100
100
  # Validate session_cookie
101
101
  if not isinstance(self.session_cookie, str) or not self.session_cookie.strip():
@@ -0,0 +1,44 @@
1
+ from orionis.console.contracts.cli_request import ICLIRequest
2
+ from orionis.console.request.cli_request import CLIRequest
3
+ from orionis.container.providers.service_provider import ServiceProvider
4
+
5
+ class CLRequestProvider(ServiceProvider):
6
+ """
7
+ Service provider for registering CLI request services in the Orionis framework.
8
+
9
+ This provider handles the registration and binding of CLI request interfaces
10
+ to their concrete implementations within the application container.
11
+ """
12
+
13
+ def register(self) -> None:
14
+ """
15
+ Register CLI request services in the application container.
16
+
17
+ Binds the ICLIRequest interface to the CLIRequest implementation as a
18
+ transient service, making it available for dependency injection throughout
19
+ the application with the specified alias.
20
+
21
+ Returns
22
+ -------
23
+ None
24
+ This method does not return any value.
25
+ """
26
+ # Register CLIRequest as a transient service bound to ICLIRequest interface
27
+ # Transient services create a new instance each time they are resolved
28
+ self.app.transient(ICLIRequest, CLIRequest, alias="x-orionis.console.request.cli_request")
29
+
30
+ def boot(self) -> None:
31
+ """
32
+ Perform any necessary bootstrapping after service registration.
33
+
34
+ This method is called after all services have been registered and can be
35
+ used to perform additional setup or configuration tasks. Currently, no
36
+ bootstrapping logic is required for CLI request services.
37
+
38
+ Returns
39
+ -------
40
+ None
41
+ This method does not return any value.
42
+ """
43
+ # No bootstrapping logic required for CLI request services
44
+ pass
@@ -5,7 +5,7 @@
5
5
  NAME = "orionis"
6
6
 
7
7
  # Current version of the framework
8
- VERSION = "0.545.0"
8
+ VERSION = "0.547.0"
9
9
 
10
10
  # Full name of the author or maintainer of the project
11
11
  AUTHOR = "Raul Mauricio Uñate Castro"
@@ -0,0 +1,115 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from typing import Optional
5
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
6
+ from cryptography.hazmat.backends import default_backend
7
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
8
+
9
+
10
+ class Encrypter:
11
+
12
+ SUPPORTED_CIPHERS = ["AES-128-CBC", "AES-256-CBC", "AES-128-GCM", "AES-256-GCM"]
13
+
14
+ def __init__(self, app_key: str, cipher: str = "AES-256-CBC"):
15
+ # Decodificar APP_KEY de Laravel
16
+ if app_key.startswith("base64:"):
17
+ key_bytes = base64.b64decode(app_key[7:])
18
+ else:
19
+ key_bytes = app_key.encode()
20
+
21
+ self.key = key_bytes
22
+ self.cipher = cipher
23
+
24
+ # Validar cipher
25
+ if cipher not in self.SUPPORTED_CIPHERS:
26
+ raise ValueError(f"Cipher '{cipher}' no soportado. Usa uno de: {self.SUPPORTED_CIPHERS}")
27
+
28
+ # Validar longitud de clave según el cipher
29
+ key_len = len(self.key)
30
+ if cipher.startswith("AES-128") and key_len != 16:
31
+ raise ValueError("La clave debe ser de 16 bytes para AES-128")
32
+ if cipher.startswith("AES-256") and key_len != 32:
33
+ raise ValueError("La clave debe ser de 32 bytes para AES-256")
34
+
35
+ def encrypt(self, plaintext: str) -> str:
36
+ data = plaintext.encode()
37
+
38
+ if "GCM" in self.cipher:
39
+ return self._encrypt_gcm(data)
40
+ else:
41
+ return self._encrypt_cbc(data)
42
+
43
+ def decrypt(self, payload: str) -> str:
44
+ # Decodificar base64 -> JSON
45
+ decoded = base64.b64decode(payload).decode()
46
+ data = json.loads(decoded)
47
+
48
+ cipher = data.get("cipher")
49
+ iv = base64.b64decode(data["iv"])
50
+ value = base64.b64decode(data["value"])
51
+ tag = base64.b64decode(data["tag"]) if data.get("tag") else None
52
+
53
+ if cipher != self.cipher:
54
+ raise ValueError(f"El cipher del payload '{cipher}' no coincide con el configurado '{self.cipher}'")
55
+
56
+ if "GCM" in cipher:
57
+ return self._decrypt_gcm(value, iv, tag).decode()
58
+ else:
59
+ return self._decrypt_cbc(value, iv).decode()
60
+
61
+ # -------------------
62
+ # Métodos privados CBC
63
+ # -------------------
64
+ def _encrypt_cbc(self, data: bytes) -> str:
65
+ iv = os.urandom(16)
66
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=default_backend())
67
+ encryptor = cipher.encryptor()
68
+
69
+ # PKCS7 padding
70
+ pad_len = 16 - (len(data) % 16)
71
+ data += bytes([pad_len]) * pad_len
72
+
73
+ ct = encryptor.update(data) + encryptor.finalize()
74
+
75
+ payload = {
76
+ "iv": base64.b64encode(iv).decode(),
77
+ "value": base64.b64encode(ct).decode(),
78
+ "tag": None,
79
+ "cipher": self.cipher,
80
+ }
81
+
82
+ return base64.b64encode(json.dumps(payload).encode()).decode()
83
+
84
+ def _decrypt_cbc(self, ct: bytes, iv: bytes) -> bytes:
85
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=default_backend())
86
+ decryptor = cipher.decryptor()
87
+ data = decryptor.update(ct) + decryptor.finalize()
88
+
89
+ # Unpad PKCS7
90
+ pad_len = data[-1]
91
+ return data[:-pad_len]
92
+
93
+ # -------------------
94
+ # Métodos privados GCM
95
+ # -------------------
96
+ def _encrypt_gcm(self, data: bytes) -> str:
97
+ iv = os.urandom(12)
98
+ aesgcm = AESGCM(self.key)
99
+ ct = aesgcm.encrypt(iv, data, None)
100
+
101
+ # separar ciphertext y tag (últimos 16 bytes)
102
+ value, tag = ct[:-16], ct[-16:]
103
+
104
+ payload = {
105
+ "iv": base64.b64encode(iv).decode(),
106
+ "value": base64.b64encode(value).decode(),
107
+ "tag": base64.b64encode(tag).decode(),
108
+ "cipher": self.cipher,
109
+ }
110
+
111
+ return base64.b64encode(json.dumps(payload).encode()).decode()
112
+
113
+ def _decrypt_gcm(self, value: bytes, iv: bytes, tag: Optional[bytes]) -> bytes:
114
+ aesgcm = AESGCM(self.key)
115
+ return aesgcm.decrypt(iv, value + tag, None)
@@ -255,64 +255,72 @@ class EnvironmentCaster(IEnvironmentCaster):
255
255
  """
256
256
  Convert the internal value to a Base64 encoded string with the type hint prefix.
257
257
 
258
+ - If the value is already valid base64, leave it as-is.
259
+ - Otherwise, encode it to base64.
260
+
258
261
  Returns
259
262
  -------
260
263
  str
261
- A string in the format "<type_hint>:<base64_value>", where <base64_value> is the Base64
262
- encoded representation of the internal value.
263
-
264
- Raises
265
- ------
266
- OrionisEnvironmentValueError
267
- If the internal value is not a string or bytes.
264
+ A string in the format "<type_hint>:<base64_value>".
268
265
  """
269
-
270
- # Import the base64 module for encoding
271
266
  import base64
272
267
 
273
- # Ensure the internal value is a string or bytes before encoding
274
268
  if not isinstance(self.__value_raw, (str, bytes)):
275
269
  raise OrionisEnvironmentValueError(
276
270
  f"Value must be a string or bytes to convert to Base64, got {type(self.__value_raw).__name__} instead."
277
271
  )
278
272
 
279
- # Encode the value in Base64
280
- encoded_value = base64.b64encode(str(self.__value_raw).encode()).decode()
273
+ # Normalizar a str
274
+ if isinstance(self.__value_raw, bytes):
275
+ candidate = self.__value_raw.decode("utf-8", errors="ignore")
276
+ else:
277
+ candidate = self.__value_raw
278
+
279
+ try:
280
+ # Validar si ya es base64 válido
281
+ base64.b64decode(candidate, validate=True)
282
+ encoded_value = candidate
283
+ except Exception:
284
+ # No era base64, entonces convertirlo
285
+ raw_bytes = (
286
+ self.__value_raw.encode() if isinstance(self.__value_raw, str) else self.__value_raw
287
+ )
288
+ encoded_value = base64.b64encode(raw_bytes).decode("utf-8")
281
289
 
282
- # Return the formatted string with type hint and Base64 encoded value
283
290
  return f"{self.__type_hint}:{encoded_value}"
284
291
 
285
- def __parseBase64(self) -> str:
292
+ def __parseBase64(self) -> bytes:
286
293
  """
287
294
  Decode the internal raw value from Base64 encoding.
288
295
 
289
- Decodes the Base64-encoded string stored in the internal raw value and returns the decoded result as a string.
290
- If decoding fails, raises an OrionisEnvironmentValueException.
291
-
292
296
  Returns
293
297
  -------
294
- str
295
- The decoded string from the Base64-encoded internal value.
298
+ bytes
299
+ The decoded raw bytes from the Base64-encoded internal value.
296
300
 
297
301
  Raises
298
302
  ------
299
303
  OrionisEnvironmentValueException
300
304
  If the internal value cannot be decoded from Base64.
301
305
  """
302
-
303
- # Import the base64 module for decoding
304
306
  import base64
305
307
 
306
308
  try:
309
+ raw_value = self.__value_raw
310
+ if isinstance(raw_value, bytes):
311
+ raw_value = raw_value.decode("utf-8", errors="ignore")
307
312
 
308
- # Decode the Base64 encoded value and return as string
309
- decoded_value = base64.b64decode(self.__value_raw).decode()
310
- return decoded_value
313
+ decoded = base64.b64decode(raw_value, validate=True)
311
314
 
312
- except Exception as e:
315
+ try:
316
+ return decoded.decode("utf-8")
317
+ except UnicodeDecodeError:
318
+ return decoded
313
319
 
314
- # Raise a custom exception if decoding fails
315
- raise OrionisEnvironmentValueException(f"Cannot decode Base64 value '{self.__value_raw}': {str(e)}")
320
+ except Exception as e:
321
+ raise OrionisEnvironmentValueException(
322
+ f"Cannot decode Base64 value '{self.__value_raw}': {str(e)}"
323
+ )
316
324
 
317
325
  def __parsePath(self):
318
326
  """
@@ -1,26 +1,47 @@
1
1
  import os
2
+ import base64
2
3
 
3
4
  class SecureKeyGenerator:
4
5
  """
5
- Static utility class for generating cryptographically secure random keys.
6
+ Utility class for generating Laravel-compatible APP_KEY values.
6
7
 
7
- This class provides a static method to generate a secure random key,
8
- encoded as a hexadecimal string, suitable for use in security-sensitive
9
- applications such as cryptographic secrets or tokens.
8
+ Laravel expects keys in Base64 format, prefixed with 'base64:'.
9
+ Supported ciphers: AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM.
10
10
  """
11
11
 
12
+ KEY_SIZES = {
13
+ "AES-128-CBC": 16,
14
+ "AES-256-CBC": 32,
15
+ "AES-128-GCM": 16,
16
+ "AES-256-GCM": 32,
17
+ }
18
+
12
19
  @staticmethod
13
- def generate() -> str:
20
+ def generate(cipher: str = "AES-256-CBC") -> str:
14
21
  """
15
- Generate a cryptographically secure random key encoded in hexadecimal.
22
+ Generate a Laravel-compatible APP_KEY.
23
+
24
+ Parameters
25
+ ----------
26
+ cipher : str
27
+ The cipher algorithm. Options: AES-128-CBC, AES-256-CBC,
28
+ AES-128-GCM, AES-256-GCM. Default is AES-256-CBC.
16
29
 
17
30
  Returns
18
31
  -------
19
32
  str
20
- A 64-character hexadecimal string representing a 32-byte
21
- cryptographically secure random key.
33
+ A string formatted like Laravel's APP_KEY (e.g., base64:xxxx).
22
34
  """
35
+ if cipher not in SecureKeyGenerator.KEY_SIZES:
36
+ raise ValueError(
37
+ f"Cipher '{cipher}' no soportado. "
38
+ f"Opciones: {', '.join(SecureKeyGenerator.KEY_SIZES.keys())}"
39
+ )
40
+
41
+ key_length = SecureKeyGenerator.KEY_SIZES[cipher]
42
+
43
+ # Generate secure random bytes
44
+ key = os.urandom(key_length)
23
45
 
24
- # Generate 32 random bytes using a cryptographically secure RNG
25
- # Encode the bytes as a hexadecimal string and return
26
- return os.urandom(32).hex()
46
+ # Encode in Base64 and prepend 'base64:'
47
+ return "base64:" + base64.b64encode(key).decode("utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionis
3
- Version: 0.545.0
3
+ Version: 0.547.0
4
4
  Summary: Orionis Framework – Elegant, Fast, and Powerful.
5
5
  Home-page: https://github.com/orionis-framework/framework
6
6
  Author: Raul Mauricio Uñate Castro