extendvcc-cli 0.1.0__tar.gz → 0.1.1__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.
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.github/workflows/release.yml +7 -3
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/CHANGELOG.md +11 -1
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/PKG-INFO +1 -1
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/pyproject.toml +1 -1
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/auth.py +21 -8
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_auth.py +16 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.github/workflows/ci.yml +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/.gitignore +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/AGENTS.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/CLAUDE.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/CONTRIBUTING.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/LICENSE +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/README.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/SECURITY.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/docs/testing-policy.md +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/__init__.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/_exit_codes.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/_jsonl.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/_paths.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/cards.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/cli.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/client.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/imap_otp.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/ledger.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/models.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/src/extendvcc/py.typed +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/fixtures/op_credit_card_template.json +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_cards.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_cli.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_client.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_imap_otp.py +0 -0
- {extendvcc_cli-0.1.0 → extendvcc_cli-0.1.1}/tests/test_ledger.py +0 -0
|
@@ -10,14 +10,15 @@ permissions:
|
|
|
10
10
|
jobs:
|
|
11
11
|
build:
|
|
12
12
|
strategy:
|
|
13
|
+
# One platform failing must not cancel the others; the release attaches
|
|
14
|
+
# whatever built. Intel macOS (macos-13) is intentionally dropped: the
|
|
15
|
+
# runner pool is scarce and being retired, and pip/pipx covers that case.
|
|
16
|
+
fail-fast: false
|
|
13
17
|
matrix:
|
|
14
18
|
include:
|
|
15
19
|
- os: macos-latest
|
|
16
20
|
arch: arm64
|
|
17
21
|
artifact: extendvcc-macos-arm64
|
|
18
|
-
- os: macos-13
|
|
19
|
-
arch: x86_64
|
|
20
|
-
artifact: extendvcc-macos-x86_64
|
|
21
22
|
- os: ubuntu-latest
|
|
22
23
|
arch: x86_64
|
|
23
24
|
artifact: extendvcc-linux-x86_64
|
|
@@ -54,6 +55,9 @@ jobs:
|
|
|
54
55
|
|
|
55
56
|
release:
|
|
56
57
|
needs: [build, test]
|
|
58
|
+
# Run once builds finish even if a platform failed, as long as tests passed,
|
|
59
|
+
# so a single bad/slow binary never blocks the GitHub release.
|
|
60
|
+
if: ${{ !cancelled() && needs.test.result == 'success' }}
|
|
57
61
|
runs-on: ubuntu-latest
|
|
58
62
|
steps:
|
|
59
63
|
- uses: actions/download-artifact@v4
|
|
@@ -10,6 +10,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.1.1] - 2026-06-15
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- First-time login failed at device registration with `Found negative value for salt or password verifier`. The device SRP salt and password verifier are now zero-padded so Cognito never reads them as negative.
|
|
18
|
+
- Authentication errors now include Cognito's error type and message instead of only a status code, so failures report the real reason.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## [0.1.0] - 2026-06-14
|
|
14
23
|
|
|
15
24
|
### Added
|
|
@@ -28,5 +37,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
28
37
|
- **Python API:** public re-exports in `extendvcc.__init__` for programmatic use.
|
|
29
38
|
- **Typed:** `py.typed` marker (PEP 561); type hints throughout.
|
|
30
39
|
|
|
31
|
-
[Unreleased]: https://github.com/4LAU/extendvcc/compare/v0.1.
|
|
40
|
+
[Unreleased]: https://github.com/4LAU/extendvcc/compare/v0.1.1...HEAD
|
|
41
|
+
[0.1.1]: https://github.com/4LAU/extendvcc/compare/v0.1.0...v0.1.1
|
|
32
42
|
[0.1.0]: https://github.com/4LAU/extendvcc/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: extendvcc-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Unofficial CLI and Python client for the Extend virtual card API
|
|
5
5
|
Project-URL: Homepage, https://github.com/4LAU/extendvcc
|
|
6
6
|
Project-URL: Repository, https://github.com/4LAU/extendvcc
|
|
@@ -150,7 +150,20 @@ def _raise_for_status(resp: Any, *, kind: str = "auth", path: str | None = None)
|
|
|
150
150
|
status_code=status_code,
|
|
151
151
|
path=path or "",
|
|
152
152
|
)
|
|
153
|
-
|
|
153
|
+
# Cognito error responses carry {"__type": "...", "message": "..."} — surface
|
|
154
|
+
# it so a 400 distinguishes a wrong/expired code from a real flow bug. The body
|
|
155
|
+
# holds only Cognito's own error type/message, no secrets.
|
|
156
|
+
detail = ""
|
|
157
|
+
try:
|
|
158
|
+
body = _response_json(resp)
|
|
159
|
+
if isinstance(body, dict):
|
|
160
|
+
err_type = str(body.get("__type", "")).rsplit("#", 1)[-1]
|
|
161
|
+
message = body.get("message") or body.get("Message") or ""
|
|
162
|
+
detail = " - ".join(part for part in (err_type, message) if part)
|
|
163
|
+
except Exception:
|
|
164
|
+
detail = ""
|
|
165
|
+
suffix = f" ({detail})" if detail else ""
|
|
166
|
+
raise PayWithExtendAuthError(f"PayWithExtend Cognito request failed with status {status_code}{suffix}")
|
|
154
167
|
|
|
155
168
|
|
|
156
169
|
def _inspect_account_risk(resp: Any, path: str) -> None:
|
|
@@ -284,12 +297,6 @@ def _hex_to_int(value: str) -> int:
|
|
|
284
297
|
return int(value, 16)
|
|
285
298
|
|
|
286
299
|
|
|
287
|
-
def _int_to_bytes(value: int) -> bytes:
|
|
288
|
-
if value == 0:
|
|
289
|
-
return b"\x00"
|
|
290
|
-
return value.to_bytes((value.bit_length() + 7) // 8, "big")
|
|
291
|
-
|
|
292
|
-
|
|
293
300
|
def _hex_to_bytes(value: int | str) -> bytes:
|
|
294
301
|
return bytes.fromhex(_pad_hex(value))
|
|
295
302
|
|
|
@@ -454,11 +461,17 @@ def _generate_device_verifier(
|
|
|
454
461
|
salt: bytes | None = None,
|
|
455
462
|
) -> tuple[str, str]:
|
|
456
463
|
salt_bytes = salt or secrets.token_bytes(16)
|
|
464
|
+
# Cognito decodes Salt and PasswordVerifier as signed big-endian integers, so a
|
|
465
|
+
# leading high bit is read as negative ("Found negative value for salt or password
|
|
466
|
+
# verifier") and ConfirmDevice 400s. Prepend a 0x00 byte when needed via
|
|
467
|
+
# _hex_to_bytes/_pad_hex, and use the SAME sign-fixed salt for both the x-hash and
|
|
468
|
+
# the wire value so they stay consistent.
|
|
469
|
+
salt_bytes = _hex_to_bytes(salt_bytes.hex())
|
|
457
470
|
device_username = f"{device_group_key}{device_key}"
|
|
458
471
|
x_value = _calculate_x(salt_bytes.hex(), device_username, device_password)
|
|
459
472
|
verifier = pow(G, x_value, N)
|
|
460
473
|
return (
|
|
461
|
-
base64.b64encode(
|
|
474
|
+
base64.b64encode(_hex_to_bytes(verifier)).decode("ascii"),
|
|
462
475
|
base64.b64encode(salt_bytes).decode("ascii"),
|
|
463
476
|
)
|
|
464
477
|
|
|
@@ -207,6 +207,22 @@ def test_authenticate_handles_password_otp_and_device_registration(tmp_path, mon
|
|
|
207
207
|
configure_paths()
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
def test_device_verifier_pads_negative_salt_and_verifier() -> None:
|
|
211
|
+
"""Cognito decodes Salt/PasswordVerifier as signed big-endian ints; a high bit must be
|
|
212
|
+
0x00-padded so neither is negative. Regression for the ConfirmDevice 400
|
|
213
|
+
'Found negative value for salt or password verifier'."""
|
|
214
|
+
salt = b"\x80" + b"\x11" * 15 # high bit set -> negative without padding
|
|
215
|
+
verifier_b64, salt_b64 = auth._generate_device_verifier("grp", "devkey", "pw", salt=salt)
|
|
216
|
+
salt_bytes = base64.b64decode(salt_b64)
|
|
217
|
+
verifier_bytes = base64.b64decode(verifier_b64)
|
|
218
|
+
# High bit clear (first byte < 0x80) keeps Cognito's signed decode non-negative.
|
|
219
|
+
assert salt_bytes[0] < 0x80
|
|
220
|
+
assert verifier_bytes[0] < 0x80
|
|
221
|
+
# The sign-fixed salt is the original 16 bytes with a 0x00 prepended, and that same
|
|
222
|
+
# salt is what gets sent (so it matches the value used in the x-hash).
|
|
223
|
+
assert salt_bytes == b"\x00" + salt
|
|
224
|
+
|
|
225
|
+
|
|
210
226
|
def test_authenticate_handles_remembered_device_srp(tmp_path, monkeypatch) -> None:
|
|
211
227
|
configure_paths(state_dir=tmp_path)
|
|
212
228
|
auth.save_session(
|
|
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
|
|
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
|