mevault 0.1.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.
- mevault/__init__.py +53 -0
- mevault/client.py +343 -0
- mevault/exceptions.py +34 -0
- mevault/protocol.py +81 -0
- mevault/types.py +32 -0
- mevault-0.1.0.dist-info/METADATA +104 -0
- mevault-0.1.0.dist-info/RECORD +8 -0
- mevault-0.1.0.dist-info/WHEEL +4 -0
mevault/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mevault — Python SDK for requesting secrets from the local MeVault broker.
|
|
3
|
+
|
|
4
|
+
Quickstart
|
|
5
|
+
----------
|
|
6
|
+
|
|
7
|
+
Synchronous::
|
|
8
|
+
|
|
9
|
+
from mevault import get_secret, get_optional_secret
|
|
10
|
+
from mevault.exceptions import VaultLocked, SecretNotFound
|
|
11
|
+
|
|
12
|
+
db_password = get_secret("DB_PASSWORD")
|
|
13
|
+
api_key = get_optional_secret("OPTIONAL_API_KEY", default="")
|
|
14
|
+
|
|
15
|
+
Asynchronous::
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from mevault import async_get_secret
|
|
19
|
+
|
|
20
|
+
async def main():
|
|
21
|
+
token = await async_get_secret("AUTH_TOKEN")
|
|
22
|
+
|
|
23
|
+
asyncio.run(main())
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .client import async_get_secret, get_optional_secret, get_secret
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
AccessDenied,
|
|
29
|
+
MeVaultError,
|
|
30
|
+
MeVaultUnavailable,
|
|
31
|
+
ProtocolError,
|
|
32
|
+
SecretNotFound,
|
|
33
|
+
SessionExpired,
|
|
34
|
+
VaultLocked,
|
|
35
|
+
)
|
|
36
|
+
from .types import SecretValue
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Functions
|
|
40
|
+
"get_secret",
|
|
41
|
+
"get_optional_secret",
|
|
42
|
+
"async_get_secret",
|
|
43
|
+
# Exceptions
|
|
44
|
+
"MeVaultError",
|
|
45
|
+
"MeVaultUnavailable",
|
|
46
|
+
"VaultLocked",
|
|
47
|
+
"SecretNotFound",
|
|
48
|
+
"AccessDenied",
|
|
49
|
+
"ProtocolError",
|
|
50
|
+
"SessionExpired",
|
|
51
|
+
# Types
|
|
52
|
+
"SecretValue",
|
|
53
|
+
]
|
mevault/client.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MeVault client — synchronous and asyncio interfaces.
|
|
3
|
+
|
|
4
|
+
Design constraints
|
|
5
|
+
------------------
|
|
6
|
+
- Named-pipe transport only (no subprocess, no env-var injection).
|
|
7
|
+
- No vault password accepted or stored.
|
|
8
|
+
- Secret values are NEVER logged; only error codes are logged.
|
|
9
|
+
- Hard 10-second timeout on every operation.
|
|
10
|
+
- Hard 1 MiB cap on response size.
|
|
11
|
+
- One connection per request; connection closed immediately after response.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AccessDenied,
|
|
22
|
+
MeVaultUnavailable,
|
|
23
|
+
ProtocolError,
|
|
24
|
+
SecretNotFound,
|
|
25
|
+
SessionExpired,
|
|
26
|
+
VaultLocked,
|
|
27
|
+
)
|
|
28
|
+
from .protocol import (
|
|
29
|
+
KNOWN_ERROR_CODES,
|
|
30
|
+
MAX_RESPONSE_BYTES,
|
|
31
|
+
PIPE_NAME,
|
|
32
|
+
PROTOCOL_VERSION,
|
|
33
|
+
BrokerResponse,
|
|
34
|
+
GetSecretRequest,
|
|
35
|
+
)
|
|
36
|
+
from .types import SecretValue
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Default timeout for all I/O operations (seconds).
|
|
41
|
+
_TIMEOUT: float = 10.0
|
|
42
|
+
|
|
43
|
+
# Windows error code: pipe server is busy (all instances occupied).
|
|
44
|
+
_ERROR_PIPE_BUSY: int = 231
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Internal helpers
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _map_error(error_code: Optional[str], message: Optional[str]) -> Exception:
|
|
53
|
+
"""Convert a broker error code into the appropriate SDK exception."""
|
|
54
|
+
detail = message or error_code or "unknown error"
|
|
55
|
+
mapping = {
|
|
56
|
+
"vault_locked": VaultLocked,
|
|
57
|
+
"session_expired": SessionExpired,
|
|
58
|
+
"access_denied": AccessDenied,
|
|
59
|
+
"secret_not_found": SecretNotFound,
|
|
60
|
+
}
|
|
61
|
+
exc_class = mapping.get(error_code or "", MeVaultUnavailable) # type: ignore[arg-type]
|
|
62
|
+
# Log only the error code, never the message (which might contain secret names).
|
|
63
|
+
logger.debug("broker returned error code: %s", error_code)
|
|
64
|
+
return exc_class(detail)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_response(raw_bytes: bytes) -> BrokerResponse:
|
|
68
|
+
"""
|
|
69
|
+
Parse a raw response line from the broker.
|
|
70
|
+
|
|
71
|
+
Raises ProtocolError on any malformed or oversized payload.
|
|
72
|
+
"""
|
|
73
|
+
if len(raw_bytes) > MAX_RESPONSE_BYTES:
|
|
74
|
+
raise ProtocolError(
|
|
75
|
+
f"Response exceeded maximum allowed size of {MAX_RESPONSE_BYTES} bytes"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(raw_bytes.decode("utf-8"))
|
|
80
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
81
|
+
raise ProtocolError(f"Malformed JSON response from broker: {exc}") from exc
|
|
82
|
+
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise ProtocolError("Broker response was not a JSON object")
|
|
85
|
+
|
|
86
|
+
ok = data.get("ok")
|
|
87
|
+
if not isinstance(ok, bool):
|
|
88
|
+
raise ProtocolError("Broker response missing boolean 'ok' field")
|
|
89
|
+
|
|
90
|
+
# Validate protocol version if the broker echoes it.
|
|
91
|
+
version = data.get("version")
|
|
92
|
+
if version is not None and version != PROTOCOL_VERSION:
|
|
93
|
+
raise ProtocolError(
|
|
94
|
+
f"Protocol version mismatch: expected {PROTOCOL_VERSION}, got {version}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return BrokerResponse(
|
|
98
|
+
ok=ok,
|
|
99
|
+
value=data.get("value"),
|
|
100
|
+
error=data.get("error"),
|
|
101
|
+
message=data.get("message"),
|
|
102
|
+
version=version,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_request(name: str) -> bytes:
|
|
107
|
+
"""Serialise a GetSecretRequest to a newline-terminated JSON byte string."""
|
|
108
|
+
req = GetSecretRequest(name=name)
|
|
109
|
+
return (json.dumps(req.to_dict()) + "\n").encode("utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Platform guard — MeVault only runs on Windows
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
if sys.platform != "win32":
|
|
117
|
+
def get_secret(name: str) -> str:
|
|
118
|
+
"""Raise MeVaultUnavailable: MeVault only runs on Windows."""
|
|
119
|
+
raise MeVaultUnavailable("MeVault is only supported on Windows")
|
|
120
|
+
|
|
121
|
+
def get_optional_secret(name: str, default: Optional[str] = None) -> Optional[str]:
|
|
122
|
+
"""Return *default*: MeVault only runs on Windows."""
|
|
123
|
+
return default
|
|
124
|
+
|
|
125
|
+
async def async_get_secret(name: str) -> str:
|
|
126
|
+
"""Raise MeVaultUnavailable: MeVault only runs on Windows."""
|
|
127
|
+
raise MeVaultUnavailable("MeVault is only supported on Windows")
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Windows-only transport
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
import ctypes
|
|
135
|
+
import ctypes.wintypes
|
|
136
|
+
|
|
137
|
+
def _wait_and_open(pipe_name: str, timeout_ms: int = 10000): # type: ignore[return]
|
|
138
|
+
"""
|
|
139
|
+
Open a Windows named pipe, waiting if all server instances are busy.
|
|
140
|
+
|
|
141
|
+
Uses WaitNamedPipeW from kernel32 so the caller blocks instead of
|
|
142
|
+
busy-looping when the broker is serving another connection.
|
|
143
|
+
|
|
144
|
+
Returns a raw binary file object (unbuffered) on success.
|
|
145
|
+
Raises MeVaultUnavailable if the pipe cannot be opened within *timeout_ms*.
|
|
146
|
+
"""
|
|
147
|
+
deadline_ms = timeout_ms
|
|
148
|
+
while True:
|
|
149
|
+
try:
|
|
150
|
+
# Named pipes opened via open() in binary+unbuffered mode work
|
|
151
|
+
# correctly on Windows for byte-stream pipes.
|
|
152
|
+
return open(pipe_name, "r+b", buffering=0) # noqa: WPS515
|
|
153
|
+
except OSError as exc:
|
|
154
|
+
winerr = getattr(exc, "winerror", None)
|
|
155
|
+
if winerr == _ERROR_PIPE_BUSY:
|
|
156
|
+
# All pipe instances are in use; ask the kernel to wait.
|
|
157
|
+
wait_ms = min(deadline_ms, 1000)
|
|
158
|
+
ok = ctypes.windll.kernel32.WaitNamedPipeW(pipe_name, wait_ms)
|
|
159
|
+
if not ok:
|
|
160
|
+
raise MeVaultUnavailable(
|
|
161
|
+
f"MeVault broker pipe busy after {timeout_ms} ms"
|
|
162
|
+
) from exc
|
|
163
|
+
deadline_ms -= wait_ms
|
|
164
|
+
if deadline_ms <= 0:
|
|
165
|
+
raise MeVaultUnavailable(
|
|
166
|
+
"MeVault broker pipe wait timed out"
|
|
167
|
+
) from exc
|
|
168
|
+
elif exc.errno == 2 or isinstance(exc, FileNotFoundError):
|
|
169
|
+
raise MeVaultUnavailable(
|
|
170
|
+
"MeVault broker is not running (pipe not found)"
|
|
171
|
+
) from exc
|
|
172
|
+
elif isinstance(exc, PermissionError):
|
|
173
|
+
raise MeVaultUnavailable(
|
|
174
|
+
"Permission denied when connecting to MeVault broker"
|
|
175
|
+
) from exc
|
|
176
|
+
else:
|
|
177
|
+
raise MeVaultUnavailable(
|
|
178
|
+
f"Cannot open MeVault pipe: {exc}"
|
|
179
|
+
) from exc
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Synchronous implementation (Windows named pipe via open())
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _sync_transact(name: str) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Open the named pipe, send a get_secret request, and return the secret value.
|
|
188
|
+
|
|
189
|
+
The pipe is opened with WaitNamedPipe semantics so the call blocks
|
|
190
|
+
correctly when the broker is momentarily busy serving another client.
|
|
191
|
+
The connection is closed once the response line is read regardless of outcome.
|
|
192
|
+
"""
|
|
193
|
+
request_bytes = _build_request(name)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
pipe = _wait_and_open(PIPE_NAME, timeout_ms=int(_TIMEOUT * 1000))
|
|
197
|
+
except MeVaultUnavailable:
|
|
198
|
+
raise
|
|
199
|
+
except OSError as exc:
|
|
200
|
+
raise MeVaultUnavailable(
|
|
201
|
+
f"Could not connect to MeVault broker: {exc}"
|
|
202
|
+
) from exc
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
pipe.write(request_bytes)
|
|
206
|
+
|
|
207
|
+
# Read response line by line (capped at MAX_RESPONSE_BYTES + 1).
|
|
208
|
+
response_bytes = b""
|
|
209
|
+
while True:
|
|
210
|
+
chunk = pipe.read(4096)
|
|
211
|
+
if not chunk:
|
|
212
|
+
break
|
|
213
|
+
response_bytes += chunk
|
|
214
|
+
if b"\n" in response_bytes:
|
|
215
|
+
break
|
|
216
|
+
if len(response_bytes) > MAX_RESPONSE_BYTES:
|
|
217
|
+
raise ProtocolError(
|
|
218
|
+
f"Response exceeded maximum allowed size of {MAX_RESPONSE_BYTES} bytes"
|
|
219
|
+
)
|
|
220
|
+
except ProtocolError:
|
|
221
|
+
raise
|
|
222
|
+
except OSError as exc:
|
|
223
|
+
raise MeVaultUnavailable(
|
|
224
|
+
f"I/O error while communicating with MeVault broker: {exc}"
|
|
225
|
+
) from exc
|
|
226
|
+
finally:
|
|
227
|
+
try:
|
|
228
|
+
pipe.close()
|
|
229
|
+
except OSError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
if not response_bytes:
|
|
233
|
+
raise ProtocolError("Broker closed the connection without sending a response")
|
|
234
|
+
|
|
235
|
+
# Parse only the first line.
|
|
236
|
+
line = response_bytes.split(b"\n")[0]
|
|
237
|
+
response = _parse_response(line)
|
|
238
|
+
|
|
239
|
+
if response.ok:
|
|
240
|
+
if response.value is None:
|
|
241
|
+
raise ProtocolError("Broker returned ok=true but no 'value' field")
|
|
242
|
+
# Value is intentionally NOT logged here.
|
|
243
|
+
return response.value
|
|
244
|
+
|
|
245
|
+
raise _map_error(response.error, response.message)
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Public synchronous API
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def get_secret(name: str) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Retrieve a secret from the MeVault broker synchronously.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
name:
|
|
258
|
+
The name of the secret to retrieve.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
str
|
|
263
|
+
The secret value as a plain string.
|
|
264
|
+
|
|
265
|
+
Raises
|
|
266
|
+
------
|
|
267
|
+
MeVaultUnavailable
|
|
268
|
+
The broker pipe could not be reached.
|
|
269
|
+
VaultLocked
|
|
270
|
+
The vault is locked.
|
|
271
|
+
SecretNotFound
|
|
272
|
+
No secret with that name exists.
|
|
273
|
+
AccessDenied
|
|
274
|
+
The calling process is not permitted to read this secret.
|
|
275
|
+
SessionExpired
|
|
276
|
+
The broker session has expired.
|
|
277
|
+
ProtocolError
|
|
278
|
+
The broker sent a malformed response.
|
|
279
|
+
"""
|
|
280
|
+
return _sync_transact(name)
|
|
281
|
+
|
|
282
|
+
def get_optional_secret(name: str, default: Optional[str] = None) -> Optional[str]:
|
|
283
|
+
"""
|
|
284
|
+
Retrieve a secret, returning *default* if the secret does not exist.
|
|
285
|
+
|
|
286
|
+
All other errors (VaultLocked, AccessDenied, etc.) are still raised.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
name:
|
|
291
|
+
The name of the secret to retrieve.
|
|
292
|
+
default:
|
|
293
|
+
Value to return when the secret is not found. Defaults to ``None``.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
str or None
|
|
298
|
+
The secret value, or *default* if not found.
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
return get_secret(name)
|
|
302
|
+
except SecretNotFound:
|
|
303
|
+
return default
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Asynchronous implementation — wraps sync call in thread pool
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
async def async_get_secret(name: str) -> str:
|
|
310
|
+
"""
|
|
311
|
+
Retrieve a secret from the MeVault broker asynchronously.
|
|
312
|
+
|
|
313
|
+
Runs the synchronous named-pipe call in a thread-pool executor so the
|
|
314
|
+
event loop is never blocked. This is the correct approach for Windows
|
|
315
|
+
named pipes: ``asyncio.open_connection()`` cannot open a UNC pipe path.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
name:
|
|
320
|
+
The name of the secret to retrieve.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
str
|
|
325
|
+
The secret value as a plain string.
|
|
326
|
+
|
|
327
|
+
Raises
|
|
328
|
+
------
|
|
329
|
+
MeVaultUnavailable
|
|
330
|
+
The broker pipe could not be reached.
|
|
331
|
+
VaultLocked
|
|
332
|
+
The vault is locked.
|
|
333
|
+
SecretNotFound
|
|
334
|
+
No secret with that name exists.
|
|
335
|
+
AccessDenied
|
|
336
|
+
The calling process is not permitted to read this secret.
|
|
337
|
+
SessionExpired
|
|
338
|
+
The broker session has expired.
|
|
339
|
+
ProtocolError
|
|
340
|
+
The broker sent a malformed response.
|
|
341
|
+
"""
|
|
342
|
+
loop = asyncio.get_event_loop()
|
|
343
|
+
return await loop.run_in_executor(None, get_secret, name)
|
mevault/exceptions.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MeVault exception hierarchy.
|
|
3
|
+
|
|
4
|
+
All exceptions raised by the MeVault SDK derive from MeVaultError so callers
|
|
5
|
+
can catch the whole family with a single except clause if they prefer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MeVaultError(Exception):
|
|
10
|
+
"""Base class for all MeVault SDK errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MeVaultUnavailable(MeVaultError):
|
|
14
|
+
"""The MeVault broker pipe could not be reached (not running or not found)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VaultLocked(MeVaultError):
|
|
18
|
+
"""The vault is locked; the user must unlock MeVault before secrets can be read."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SecretNotFound(MeVaultError):
|
|
22
|
+
"""The requested secret does not exist in the vault."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AccessDenied(MeVaultError):
|
|
26
|
+
"""The calling process is not authorised to read this secret."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProtocolError(MeVaultError):
|
|
30
|
+
"""The broker returned an unexpected or malformed response."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SessionExpired(MeVaultError):
|
|
34
|
+
"""The broker session has expired; the user must re-authenticate."""
|
mevault/protocol.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wire-protocol constants and data structures for the MeVault named-pipe IPC.
|
|
3
|
+
|
|
4
|
+
Protocol summary
|
|
5
|
+
----------------
|
|
6
|
+
- One JSON line per message, terminated with ``\\n``.
|
|
7
|
+
- One connection per request; the client closes the connection after reading
|
|
8
|
+
the response.
|
|
9
|
+
- The broker may include an optional ``"version"`` field in the response;
|
|
10
|
+
if present it must match PROTOCOL_VERSION.
|
|
11
|
+
|
|
12
|
+
Request (client → broker)::
|
|
13
|
+
|
|
14
|
+
{"op": "get_secret", "name": "<SECRET_NAME>"}
|
|
15
|
+
|
|
16
|
+
Success response (broker → client)::
|
|
17
|
+
|
|
18
|
+
{"ok": true, "value": "<secret-value>"}
|
|
19
|
+
|
|
20
|
+
Error response (broker → client)::
|
|
21
|
+
|
|
22
|
+
{"ok": false, "error": "<error-code>", "message": "<optional detail>"}
|
|
23
|
+
|
|
24
|
+
Known error codes
|
|
25
|
+
-----------------
|
|
26
|
+
vault_locked, session_expired, access_denied, secret_not_found,
|
|
27
|
+
identity_unknown, grant_invalid, internal_error, invalid_request
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Constants
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
PIPE_NAME: str = r"\\.\pipe\mevault-runtime"
|
|
39
|
+
PROTOCOL_VERSION: int = 1
|
|
40
|
+
MAX_RESPONSE_BYTES: int = 1_048_576 # 1 MiB
|
|
41
|
+
|
|
42
|
+
# All error codes the broker may return.
|
|
43
|
+
KNOWN_ERROR_CODES: frozenset = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"vault_locked",
|
|
46
|
+
"session_expired",
|
|
47
|
+
"access_denied",
|
|
48
|
+
"secret_not_found",
|
|
49
|
+
"identity_unknown",
|
|
50
|
+
"grant_invalid",
|
|
51
|
+
"internal_error",
|
|
52
|
+
"invalid_request",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Request / response dataclasses
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class GetSecretRequest:
|
|
64
|
+
"""Represents a ``get_secret`` request sent to the broker."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
op: str = field(default="get_secret", init=False)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {"op": self.op, "name": self.name}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BrokerResponse:
|
|
75
|
+
"""Parsed broker response."""
|
|
76
|
+
|
|
77
|
+
ok: bool
|
|
78
|
+
value: Optional[str] = None # present when ok is True
|
|
79
|
+
error: Optional[str] = None # error code when ok is False
|
|
80
|
+
message: Optional[str] = None # optional human-readable detail
|
|
81
|
+
version: Optional[int] = None # optional protocol version echo
|
mevault/types.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Value types used by the MeVault SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SecretValue:
|
|
10
|
+
"""
|
|
11
|
+
Wraps a secret string.
|
|
12
|
+
|
|
13
|
+
The repr and str are deliberately redacted so that logging framework
|
|
14
|
+
calls, exception tracebacks, and interactive sessions never accidentally
|
|
15
|
+
expose the underlying value.
|
|
16
|
+
|
|
17
|
+
Retrieve the raw string only when you genuinely need it:
|
|
18
|
+
|
|
19
|
+
value = secret.get()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_value: str
|
|
23
|
+
|
|
24
|
+
def get(self) -> str:
|
|
25
|
+
"""Return the raw secret string."""
|
|
26
|
+
return self._value
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return "SecretValue(<redacted>)"
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return "<redacted>"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mevault
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MeVault Python SDK — request secrets from the local MeVault broker
|
|
5
|
+
Project-URL: Homepage, https://github.com/thecalebyte/mevault-cli
|
|
6
|
+
Project-URL: Documentation, https://github.com/thecalebyte/mevault-cli/blob/main/docs/sdk.md
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/thecalebyte/mevault-cli/issues
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: credentials,mevault,secrets,security
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# MeVault Python SDK
|
|
25
|
+
|
|
26
|
+
Request secrets from the [MeVault](https://github.com/thecalebyte/mevault-cli) broker in Python — no passwords, no env-var leakage, no subprocesses.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.9+
|
|
31
|
+
- Windows (the broker communicates over a Windows named pipe)
|
|
32
|
+
- MeVault broker running and vault unlocked
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install mevault
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from source:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd sdk/python
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Synchronous
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from mevault import get_secret, get_optional_secret
|
|
53
|
+
from mevault.exceptions import VaultLocked, SecretNotFound, MeVaultUnavailable
|
|
54
|
+
|
|
55
|
+
# Raises SecretNotFound if the secret doesn't exist.
|
|
56
|
+
db_password = get_secret("DB_PASSWORD")
|
|
57
|
+
|
|
58
|
+
# Returns None (or a custom default) if the secret doesn't exist.
|
|
59
|
+
api_key = get_optional_secret("OPTIONAL_API_KEY", default="")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Asynchronous
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
from mevault import async_get_secret
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
token = await async_get_secret("AUTH_TOKEN")
|
|
70
|
+
# use token ...
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Error handling
|
|
76
|
+
|
|
77
|
+
| Exception | When raised |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `MeVaultUnavailable` | Broker not running / pipe not found |
|
|
80
|
+
| `VaultLocked` | Vault is locked — user must unlock MeVault |
|
|
81
|
+
| `SecretNotFound` | No secret with that name |
|
|
82
|
+
| `AccessDenied` | Process not permitted to read the secret |
|
|
83
|
+
| `SessionExpired` | Broker session expired — user must re-authenticate |
|
|
84
|
+
| `ProtocolError` | Malformed or oversized broker response |
|
|
85
|
+
|
|
86
|
+
All exceptions derive from `MeVaultError`.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from mevault.exceptions import MeVaultError
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
value = get_secret("MY_SECRET")
|
|
93
|
+
except MeVaultError as exc:
|
|
94
|
+
# Handle any MeVault error generically.
|
|
95
|
+
print(f"MeVault error: {exc}")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Security notes
|
|
99
|
+
|
|
100
|
+
- Secret values are **never** logged or printed by the SDK.
|
|
101
|
+
- `SecretValue.__repr__` and `__str__` return `<redacted>` to prevent accidental exposure in logs or tracebacks.
|
|
102
|
+
- The SDK does **not** accept vault passwords and does **not** spawn subprocesses.
|
|
103
|
+
- The SDK does **not** place secrets in environment variables.
|
|
104
|
+
- Each request uses a fresh pipe connection, closed immediately after the response is received.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mevault/__init__.py,sha256=r7Ntpq9p5pb_He7-UHZ7-A18dVOtYIfYyC9TTpxW0K8,1149
|
|
2
|
+
mevault/client.py,sha256=yiTwBD_v_3EsaExk5XbRkkYyzSdowAjAO4cwjR1Uhvg,12043
|
|
3
|
+
mevault/exceptions.py,sha256=2GslvhceJVU-Ho7s-jJT891fMEJBvEMzX_MMy-_-O7Y,968
|
|
4
|
+
mevault/protocol.py,sha256=ZriFt6bFj0lTaoK8rEm20S8cgOTvX4vjnwYvH-opYMA,2376
|
|
5
|
+
mevault/types.py,sha256=az7yvWqxg-h6aOgxL2hzg8z7nqJePfj5WPyUSSVHiYE,710
|
|
6
|
+
mevault-0.1.0.dist-info/METADATA,sha256=TcUXRkPB_wc70TpfnWWTXf7NWcpS3yKZ3BMEWnTgyek,3166
|
|
7
|
+
mevault-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
mevault-0.1.0.dist-info/RECORD,,
|