vortex-python-sdk 0.0.2__tar.gz → 0.0.3__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.

Potentially problematic release.


This version of vortex-python-sdk might be problematic. Click here for more details.

@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.3] - 2025-01-31
11
+
12
+ ### Fixed
13
+ - **CRITICAL FIX**: JWT generation now matches Node.js SDK implementation exactly
14
+ - Improved JWT signing algorithm to match Node.js SDK
15
+ - Added `iat` (issued at) and `expires` timestamp fields to JWT
16
+ - Added `attributes` field support in JwtPayload for custom user attributes
17
+ - Fixed base64url encoding
18
+ - Added comprehensive tests verifying JWT output matches Node.js SDK byte-for-byte
19
+
20
+ ### Breaking Changes
21
+ - JWT structure changed significantly - tokens from 0.0.2 are incompatible with 0.0.3
22
+ - JWTs now include `iat` and `expires` fields for proper token lifecycle management
23
+
10
24
  ## [0.0.2] - 2025-01-31
11
25
 
12
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -55,11 +55,11 @@ pip install vortex-python-sdk
55
55
  ```python
56
56
  from vortex_sdk import Vortex
57
57
 
58
- # Initialize the client
59
- vortex = Vortex(api_key="your-api-key")
58
+ # Initialize the client with your Vortex API key
59
+ vortex = Vortex(api_key="your-vortex-api-key")
60
60
 
61
61
  # Or with custom base URL
62
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
62
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
63
63
  ```
64
64
 
65
65
  ### JWT Generation
@@ -17,11 +17,11 @@ pip install vortex-python-sdk
17
17
  ```python
18
18
  from vortex_sdk import Vortex
19
19
 
20
- # Initialize the client
21
- vortex = Vortex(api_key="your-api-key")
20
+ # Initialize the client with your Vortex API key
21
+ vortex = Vortex(api_key="your-vortex-api-key")
22
22
 
23
23
  # Or with custom base URL
24
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
24
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
25
25
  ```
26
26
 
27
27
  ### JWT Generation
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex-python-sdk"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "Vortex Python SDK for invitation management and JWT generation"
9
9
  authors = [{name = "TeamVortexSoftware", email = "support@vortexsoftware.com"}]
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -55,11 +55,11 @@ pip install vortex-python-sdk
55
55
  ```python
56
56
  from vortex_sdk import Vortex
57
57
 
58
- # Initialize the client
59
- vortex = Vortex(api_key="your-api-key")
58
+ # Initialize the client with your Vortex API key
59
+ vortex = Vortex(api_key="your-vortex-api-key")
60
60
 
61
61
  # Or with custom base URL
62
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
62
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
63
63
  ```
64
64
 
65
65
  ### JWT Generation
@@ -18,7 +18,7 @@ from .types import (
18
18
  VortexApiError
19
19
  )
20
20
 
21
- __version__ = "0.0.2"
21
+ __version__ = "0.0.3"
22
22
  __author__ = "TeamVortexSoftware"
23
23
  __email__ = "support@vortexsoftware.com"
24
24
 
@@ -1,4 +1,4 @@
1
- from typing import Dict, List, Optional, Union, Literal
1
+ from typing import Dict, List, Optional, Union, Literal, Any
2
2
  from pydantic import BaseModel, Field
3
3
 
4
4
 
@@ -58,6 +58,7 @@ class JwtPayload(BaseModel):
58
58
  identifiers: List[IdentifierInput]
59
59
  groups: Optional[List[GroupInput]] = None
60
60
  role: Optional[str] = None
61
+ attributes: Optional[Dict[str, Any]] = None
61
62
 
62
63
 
63
64
  class InvitationTarget(BaseModel):
@@ -2,6 +2,8 @@ import json
2
2
  import hmac
3
3
  import hashlib
4
4
  import base64
5
+ import time
6
+ import uuid
5
7
  from typing import Dict, List, Optional, Union, Literal
6
8
  from urllib.parse import urlencode
7
9
  import httpx
@@ -32,58 +34,104 @@ class Vortex:
32
34
 
33
35
  def generate_jwt(self, payload: Union[JwtPayload, Dict]) -> str:
34
36
  """
35
- Generate a JWT token for the given payload
37
+ Generate a JWT token for the given payload matching Node.js SDK implementation
36
38
 
37
39
  Args:
38
40
  payload: JWT payload containing user_id, identifiers, groups, and role
39
41
 
40
42
  Returns:
41
43
  JWT token string
44
+
45
+ Raises:
46
+ ValueError: If API key format is invalid
42
47
  """
43
48
  if isinstance(payload, dict):
44
49
  payload = JwtPayload(**payload)
45
50
 
46
- # JWT Header
51
+ # Parse API key (format: VRTX.base64url(uuid).key)
52
+ parts = self.api_key.split('.')
53
+ if len(parts) != 3:
54
+ raise ValueError('Invalid API key format. Expected: VRTX.{encodedId}.{key}')
55
+
56
+ prefix, encoded_id, key = parts
57
+
58
+ if prefix != 'VRTX':
59
+ raise ValueError('Invalid API key prefix. Expected: VRTX')
60
+
61
+ # Decode UUID from base64url
62
+ # Add padding if needed
63
+ padding = 4 - len(encoded_id) % 4
64
+ if padding != 4:
65
+ encoded_id_padded = encoded_id + ('=' * padding)
66
+ else:
67
+ encoded_id_padded = encoded_id
68
+
69
+ try:
70
+ uuid_bytes = base64.urlsafe_b64decode(encoded_id_padded)
71
+ kid = str(uuid.UUID(bytes=uuid_bytes))
72
+ except Exception as e:
73
+ raise ValueError(f'Invalid UUID in API key: {e}')
74
+
75
+ # Generate timestamps
76
+ iat = int(time.time())
77
+ expires = iat + 3600
78
+
79
+ # Step 1: Derive signing key from API key + UUID
80
+ signing_key = hmac.new(
81
+ key.encode(),
82
+ kid.encode(),
83
+ hashlib.sha256
84
+ ).digest()
85
+
86
+ # Step 2: Build header + payload
47
87
  header = {
48
- "alg": "HS256",
49
- "typ": "JWT"
88
+ 'iat': iat,
89
+ 'alg': 'HS256',
90
+ 'typ': 'JWT',
91
+ 'kid': kid,
50
92
  }
51
93
 
52
- # JWT Payload - serialize identifiers and groups to dicts
53
- jwt_payload = {
54
- "userId": payload.user_id,
55
- "identifiers": [{"type": id.type, "value": id.value} for id in payload.identifiers],
56
- }
94
+ # Serialize identifiers
95
+ identifiers_list = [{"type": id.type, "value": id.value} for id in payload.identifiers]
57
96
 
97
+ # Serialize groups
98
+ groups_list = None
58
99
  if payload.groups is not None:
59
- # Serialize groups, using model_dump to handle camelCase conversion
60
- jwt_payload["groups"] = [
100
+ groups_list = [
61
101
  {k: v for k, v in group.model_dump(by_alias=True, exclude_none=True).items()}
62
102
  for group in payload.groups
63
103
  ]
64
- if payload.role is not None:
65
- jwt_payload["role"] = payload.role
66
104
 
67
- # Encode header and payload
68
- header_encoded = base64.urlsafe_b64encode(
69
- json.dumps(header, separators=(',', ':')).encode()
70
- ).decode().rstrip('=')
105
+ jwt_payload = {
106
+ 'userId': payload.user_id,
107
+ 'groups': groups_list,
108
+ 'role': payload.role,
109
+ 'expires': expires,
110
+ 'identifiers': identifiers_list,
111
+ }
112
+
113
+ # Add attributes if provided
114
+ if hasattr(payload, 'attributes') and payload.attributes:
115
+ jwt_payload['attributes'] = payload.attributes
116
+
117
+ # Step 3: Base64URL encode (without padding)
118
+ header_json = json.dumps(header, separators=(',', ':'))
119
+ payload_json = json.dumps(jwt_payload, separators=(',', ':'))
71
120
 
72
- payload_encoded = base64.urlsafe_b64encode(
73
- json.dumps(jwt_payload, separators=(',', ':')).encode()
74
- ).decode().rstrip('=')
121
+ header_b64 = base64.urlsafe_b64encode(header_json.encode()).decode().rstrip('=')
122
+ payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip('=')
75
123
 
76
- # Create signature
77
- message = f"{header_encoded}.{payload_encoded}"
124
+ # Step 4: Sign
125
+ to_sign = f'{header_b64}.{payload_b64}'
78
126
  signature = hmac.new(
79
- self.api_key.encode(),
80
- message.encode(),
127
+ signing_key,
128
+ to_sign.encode(),
81
129
  hashlib.sha256
82
130
  ).digest()
83
131
 
84
- signature_encoded = base64.urlsafe_b64encode(signature).decode().rstrip('=')
132
+ signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')
85
133
 
86
- return f"{message}.{signature_encoded}"
134
+ return f'{to_sign}.{signature_b64}'
87
135
 
88
136
  async def _vortex_api_request(
89
137
  self,