rp_python_sdk 1.4.1__tar.gz → 1.5.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.
Files changed (36) hide show
  1. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/PKG-INFO +160 -1
  2. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/README.md +157 -0
  3. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/pyproject.toml +5 -1
  4. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/pushed_authorisation_request.py +41 -31
  5. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/retrieve_tokens.py +46 -46
  6. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/user_info.py +22 -6
  7. rp_python_sdk-1.5.1/rp_python_sdk/endpoints/util/client_assertion.py +28 -0
  8. rp_python_sdk-1.5.1/rp_python_sdk/endpoints/util/version_info.py +2 -0
  9. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/relying_party_client_sdk.py +58 -2
  10. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/sdk_config.py +2 -0
  11. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/__init__.py +21 -0
  12. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/async_telemetry_sender.py +36 -0
  13. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/sync_telemetry_sender.py +21 -0
  14. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_config.py +27 -0
  15. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_context.py +69 -0
  16. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_dispatcher.py +120 -0
  17. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_event.py +45 -0
  18. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_event_factory.py +107 -0
  19. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_sender.py +24 -0
  20. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_session.py +55 -0
  21. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_token_provider.py +92 -0
  22. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_transport.py +79 -0
  23. rp_python_sdk-1.5.1/rp_python_sdk/telemetry/telemetry_url_resolver.py +64 -0
  24. rp_python_sdk-1.4.1/rp_python_sdk/endpoints/util/version_info.py +0 -2
  25. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/__init__.py +0 -0
  26. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/cache/__init__.py +0 -0
  27. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/cache/http_cache.py +0 -0
  28. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/get_participants.py +0 -0
  29. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/util/fapi.py +0 -0
  30. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/util/mapping.py +0 -0
  31. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/util/system_info.py +0 -0
  32. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
  33. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/filters/participant_filters.py +0 -0
  34. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/model.py +0 -0
  35. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  36. {rp_python_sdk-1.4.1 → rp_python_sdk-1.5.1}/rp_python_sdk/setup_logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rp_python_sdk
3
- Version: 1.4.1
3
+ Version: 1.5.1
4
4
  Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
5
5
  License: Proprietary
6
6
  Author: Erik Pragt
@@ -11,7 +11,9 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Programming Language :: Python :: 3.15
14
15
  Requires-Dist: authlib (>=1.7.2,<2.0.0)
16
+ Requires-Dist: certifi (>=2026.4.22,<2027.0.0)
15
17
  Requires-Dist: cffi (>=2.0.0,<3.0.0)
16
18
  Requires-Dist: cryptography (>=50.0.0)
17
19
  Requires-Dist: joserfc (>=1.6.8,<2.0.0)
@@ -414,6 +416,155 @@ The required function parameters are:
414
416
  The method will return a `Tokenset` that contains the access_token and id_token with the requested claims along
415
417
  with a unique `x-fapi-interaction-id` that was used for this request to the authorisation server.
416
418
 
419
+ ## Telemetry
420
+
421
+ The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
422
+ centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
423
+
424
+ > **Note:** Telemetry is temporarily **disabled by default** while a bugfix is in progress. Please do not enable telemetry until the fix is released.
425
+
426
+ When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
427
+ threads, so there is no overhead.
428
+
429
+ ### Delivery mode and budget
430
+
431
+ The `delivery_mode` field selects how events are delivered:
432
+
433
+ - `TelemetryDeliveryMode.ASYNC` (default): fire-and-forget. Events are placed on a bounded in-memory queue
434
+ that is drained by daemon worker threads, so the SDK operation returns without waiting for any network I/O.
435
+ - `TelemetryDeliveryMode.SYNC`: events are delivered inline, before the SDK operation returns. Use this for
436
+ hosts that may freeze or terminate before background work completes (for example AWS Lambda), where a
437
+ fire-and-forget event could be lost. Inline delivery is bounded by `sync_send_budget_seconds` (default `1.0`).
438
+
439
+ ```python
440
+ from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig, TelemetryDeliveryMode
441
+
442
+ CustomConfig(
443
+ telemetry_config=TelemetryConfig(
444
+ delivery_mode=TelemetryDeliveryMode.SYNC,
445
+ sync_send_budget_seconds=1.0,
446
+ )
447
+ )
448
+ ```
449
+
450
+ Note on the SYNC budget: `sync_send_budget_seconds` bounds the **per-phase inactivity** (the connect and read
451
+ timeouts) of the underlying HTTP calls, not the total wall-clock latency of delivery. A degraded telemetry
452
+ endpoint that stays marginally responsive can therefore add more than the configured budget to your SDK call.
453
+ The budget is applied as a remaining-time budget across the token refresh and the event POST, so no single
454
+ phase is given the full budget afresh.
455
+
456
+ `TelemetryConfig` fields:
457
+
458
+ | Field | Default | Description |
459
+ | --- | --- | --- |
460
+ | `enabled` | `False` | Master on/off switch for telemetry. |
461
+ | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
462
+ | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
463
+ | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
464
+ | `sync_send_budget_seconds` | `1.0` | SYNC-only per-phase inactivity budget in seconds. Must be greater than 0. |
465
+ | `queue_capacity` | `200` | ASYNC-only bound on the in-memory event queue. Events are dropped when the queue is full. Must be greater than 0. |
466
+ | `max_concurrent_sends` | `3` | ASYNC-only number of daemon worker threads draining the queue. Must be greater than 0. |
467
+
468
+ Under `SYNC` the queue is bypassed entirely, so `queue_capacity` and `max_concurrent_sends` are inert.
469
+
470
+ ### What is sent
471
+
472
+ One event per operation, tagged `par_request`, `token_request` or `userinfo_request`. Every event carries the same
473
+ top-level envelope; the `data` object varies by operation. Optional fields are omitted entirely when absent rather
474
+ than sent as `null`.
475
+
476
+ #### Top-level fields
477
+
478
+ | Field | Type | Description |
479
+ |-------------------------|---------|--------------------------------------------------------------------------------------|
480
+ | `sourceService` | string | The SDK that emitted the event; always `rp-python-sdk` |
481
+ | `userAgent` | string | The SDK user-agent string, including SDK name and version |
482
+ | `sdkVersion` | string | SDK version, for diagnosing version-specific issues |
483
+ | `authorisationServerId` | string | The identity provider the request was sent to |
484
+ | `clientId` | string | Your client id, as configured on the SDK |
485
+ | `timestamp` | string | When the operation completed (RFC 3339 / ISO-8601, UTC, `Z`-suffixed) |
486
+ | `xFapiInteractionId` | string | The same interaction id returned to you, for correlation |
487
+ | `durationMs` | number | How long the operation took, in milliseconds |
488
+ | `success` | boolean | Whether the operation succeeded |
489
+ | `type` | string | The operation: `par_request`, `token_request` or `userinfo_request` |
490
+ | `schemaVersion` | string | Telemetry schema version (currently `1.0`) |
491
+ | `data` | object | Per-operation detail (see below) |
492
+
493
+ #### `data` by operation
494
+
495
+ `errorDetails` is present only on failure; `expires_in` / `request_uri` and `transaction_id` are present only on
496
+ success.
497
+
498
+ **`par_request`**
499
+
500
+ | Field | Type | Description |
501
+ |------------------------------|---------|-------------------------------------------------------------------|
502
+ | `state` | string | The OAuth `state` sent on the request |
503
+ | `request.purpose` | string | The requested purpose |
504
+ | `request.body.claims` | object | The requested claims structure (claim *names* only, never values) |
505
+ | `response.statusCode` | number | HTTP status code of the PAR response |
506
+ | `response.errorDetails` | string | Error detail from the response, on failure |
507
+ | `response.body.expires_in` | number | `expires_in` from the PAR response, on success |
508
+ | `response.body.request_uri` | string | `request_uri` from the PAR response, on success |
509
+
510
+ **`token_request`**
511
+
512
+ | Field | Type | Description |
513
+ |---------------------------|---------|-----------------------------------------------------------------|
514
+ | `state` | string | The OAuth `state` associated with the token exchange |
515
+ | `request.grant_type` | string | The grant type used (e.g. `authorization_code`) |
516
+ | `response.statusCode` | number | HTTP status code of the token response |
517
+ | `response.errorDetails` | string | Error detail from the response, on failure |
518
+ | `response.transaction_id` | string | The `txn` transaction identifier from the token response, on success |
519
+
520
+ **`userinfo_request`**
521
+
522
+ | Field | Type | Description |
523
+ |---------------------------|---------|---------------------------------------------|
524
+ | `response.statusCode` | number | HTTP status code of the userinfo response |
525
+ | `response.errorDetails` | string | Error detail from the response, on failure |
526
+
527
+ UserInfo events carry no `state` and no `request` object.
528
+
529
+ The following are **never** collected:
530
+
531
+ - no access, refresh or ID tokens
532
+ - no authorization codes
533
+ - no client credentials or client assertions
534
+ - no nonces
535
+ - no code verifiers or code challenges
536
+ - no request-object JWTs
537
+ - no full request or response bodies
538
+ - no claim values (beyond the transaction id)
539
+
540
+ Events are authenticated with the SDK's existing transport certificate (mutual TLS) and a signed client assertion —
541
+ the same credentials the SDK already uses for the ConnectID Registry — so no extra configuration or credentials are
542
+ needed.
543
+
544
+ ### Releasing telemetry resources (`close()` and context-manager use)
545
+
546
+ `RelyingPartyClientSdk` exposes a `close()` method and supports the context-manager protocol. Closing signals
547
+ the background dispatcher to stop, discards any queued events, and joins the worker threads within a bounded
548
+ grace period, releasing the transport session. `close()` is idempotent and never raises.
549
+
550
+ Prefer the context manager so resources are released automatically:
551
+
552
+ ```python
553
+ with RelyingPartyClientSdk(config) as sdk:
554
+ participants = sdk.get_participants()
555
+ # telemetry resources are released on exit
556
+ ```
557
+
558
+ Or call `close()` explicitly when you manage the lifecycle yourself:
559
+
560
+ ```python
561
+ sdk = RelyingPartyClientSdk(config)
562
+ try:
563
+ participants = sdk.get_participants()
564
+ finally:
565
+ sdk.close()
566
+ ```
567
+
417
568
  ## Conformance test
418
569
 
419
570
  An automated conformance test has been added. To execute the conformance test, a token is required,
@@ -451,6 +602,14 @@ poetry publish
451
602
 
452
603
  ## Release Notes
453
604
 
605
+ ### 1.5.1 (Sep 25, 2026)
606
+ - **Telemetry disabled by default**: Telemetry is temporarily disabled while a bugfix is in progress. Please do not enable telemetry until the fix is released.
607
+
608
+ ### 1.5.0 (Sep 22, 2026)
609
+ - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
610
+ - Supports ASYNC (default) and SYNC delivery modes
611
+ - Configurable via `TelemetryConfig` on `CustomConfig` — disable with `TelemetryConfig(enabled=False)`
612
+
454
613
  ### 1.4.1 (Aug 4, 2026)
455
614
  - Security: require cryptography 50.0.0 or newer to address CVE-2026-69247 and CVE-2026-69249
456
615
  - Removed the cryptography major-version ceiling so consumers can adopt future security releases without waiting for an SDK release
@@ -393,6 +393,155 @@ The required function parameters are:
393
393
  The method will return a `Tokenset` that contains the access_token and id_token with the requested claims along
394
394
  with a unique `x-fapi-interaction-id` that was used for this request to the authorisation server.
395
395
 
396
+ ## Telemetry
397
+
398
+ The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
399
+ centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
400
+
401
+ > **Note:** Telemetry is temporarily **disabled by default** while a bugfix is in progress. Please do not enable telemetry until the fix is released.
402
+
403
+ When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
404
+ threads, so there is no overhead.
405
+
406
+ ### Delivery mode and budget
407
+
408
+ The `delivery_mode` field selects how events are delivered:
409
+
410
+ - `TelemetryDeliveryMode.ASYNC` (default): fire-and-forget. Events are placed on a bounded in-memory queue
411
+ that is drained by daemon worker threads, so the SDK operation returns without waiting for any network I/O.
412
+ - `TelemetryDeliveryMode.SYNC`: events are delivered inline, before the SDK operation returns. Use this for
413
+ hosts that may freeze or terminate before background work completes (for example AWS Lambda), where a
414
+ fire-and-forget event could be lost. Inline delivery is bounded by `sync_send_budget_seconds` (default `1.0`).
415
+
416
+ ```python
417
+ from rp_python_sdk.telemetry.telemetry_config import TelemetryConfig, TelemetryDeliveryMode
418
+
419
+ CustomConfig(
420
+ telemetry_config=TelemetryConfig(
421
+ delivery_mode=TelemetryDeliveryMode.SYNC,
422
+ sync_send_budget_seconds=1.0,
423
+ )
424
+ )
425
+ ```
426
+
427
+ Note on the SYNC budget: `sync_send_budget_seconds` bounds the **per-phase inactivity** (the connect and read
428
+ timeouts) of the underlying HTTP calls, not the total wall-clock latency of delivery. A degraded telemetry
429
+ endpoint that stays marginally responsive can therefore add more than the configured budget to your SDK call.
430
+ The budget is applied as a remaining-time budget across the token refresh and the event POST, so no single
431
+ phase is given the full budget afresh.
432
+
433
+ `TelemetryConfig` fields:
434
+
435
+ | Field | Default | Description |
436
+ | --- | --- | --- |
437
+ | `enabled` | `False` | Master on/off switch for telemetry. |
438
+ | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
439
+ | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
440
+ | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
441
+ | `sync_send_budget_seconds` | `1.0` | SYNC-only per-phase inactivity budget in seconds. Must be greater than 0. |
442
+ | `queue_capacity` | `200` | ASYNC-only bound on the in-memory event queue. Events are dropped when the queue is full. Must be greater than 0. |
443
+ | `max_concurrent_sends` | `3` | ASYNC-only number of daemon worker threads draining the queue. Must be greater than 0. |
444
+
445
+ Under `SYNC` the queue is bypassed entirely, so `queue_capacity` and `max_concurrent_sends` are inert.
446
+
447
+ ### What is sent
448
+
449
+ One event per operation, tagged `par_request`, `token_request` or `userinfo_request`. Every event carries the same
450
+ top-level envelope; the `data` object varies by operation. Optional fields are omitted entirely when absent rather
451
+ than sent as `null`.
452
+
453
+ #### Top-level fields
454
+
455
+ | Field | Type | Description |
456
+ |-------------------------|---------|--------------------------------------------------------------------------------------|
457
+ | `sourceService` | string | The SDK that emitted the event; always `rp-python-sdk` |
458
+ | `userAgent` | string | The SDK user-agent string, including SDK name and version |
459
+ | `sdkVersion` | string | SDK version, for diagnosing version-specific issues |
460
+ | `authorisationServerId` | string | The identity provider the request was sent to |
461
+ | `clientId` | string | Your client id, as configured on the SDK |
462
+ | `timestamp` | string | When the operation completed (RFC 3339 / ISO-8601, UTC, `Z`-suffixed) |
463
+ | `xFapiInteractionId` | string | The same interaction id returned to you, for correlation |
464
+ | `durationMs` | number | How long the operation took, in milliseconds |
465
+ | `success` | boolean | Whether the operation succeeded |
466
+ | `type` | string | The operation: `par_request`, `token_request` or `userinfo_request` |
467
+ | `schemaVersion` | string | Telemetry schema version (currently `1.0`) |
468
+ | `data` | object | Per-operation detail (see below) |
469
+
470
+ #### `data` by operation
471
+
472
+ `errorDetails` is present only on failure; `expires_in` / `request_uri` and `transaction_id` are present only on
473
+ success.
474
+
475
+ **`par_request`**
476
+
477
+ | Field | Type | Description |
478
+ |------------------------------|---------|-------------------------------------------------------------------|
479
+ | `state` | string | The OAuth `state` sent on the request |
480
+ | `request.purpose` | string | The requested purpose |
481
+ | `request.body.claims` | object | The requested claims structure (claim *names* only, never values) |
482
+ | `response.statusCode` | number | HTTP status code of the PAR response |
483
+ | `response.errorDetails` | string | Error detail from the response, on failure |
484
+ | `response.body.expires_in` | number | `expires_in` from the PAR response, on success |
485
+ | `response.body.request_uri` | string | `request_uri` from the PAR response, on success |
486
+
487
+ **`token_request`**
488
+
489
+ | Field | Type | Description |
490
+ |---------------------------|---------|-----------------------------------------------------------------|
491
+ | `state` | string | The OAuth `state` associated with the token exchange |
492
+ | `request.grant_type` | string | The grant type used (e.g. `authorization_code`) |
493
+ | `response.statusCode` | number | HTTP status code of the token response |
494
+ | `response.errorDetails` | string | Error detail from the response, on failure |
495
+ | `response.transaction_id` | string | The `txn` transaction identifier from the token response, on success |
496
+
497
+ **`userinfo_request`**
498
+
499
+ | Field | Type | Description |
500
+ |---------------------------|---------|---------------------------------------------|
501
+ | `response.statusCode` | number | HTTP status code of the userinfo response |
502
+ | `response.errorDetails` | string | Error detail from the response, on failure |
503
+
504
+ UserInfo events carry no `state` and no `request` object.
505
+
506
+ The following are **never** collected:
507
+
508
+ - no access, refresh or ID tokens
509
+ - no authorization codes
510
+ - no client credentials or client assertions
511
+ - no nonces
512
+ - no code verifiers or code challenges
513
+ - no request-object JWTs
514
+ - no full request or response bodies
515
+ - no claim values (beyond the transaction id)
516
+
517
+ Events are authenticated with the SDK's existing transport certificate (mutual TLS) and a signed client assertion —
518
+ the same credentials the SDK already uses for the ConnectID Registry — so no extra configuration or credentials are
519
+ needed.
520
+
521
+ ### Releasing telemetry resources (`close()` and context-manager use)
522
+
523
+ `RelyingPartyClientSdk` exposes a `close()` method and supports the context-manager protocol. Closing signals
524
+ the background dispatcher to stop, discards any queued events, and joins the worker threads within a bounded
525
+ grace period, releasing the transport session. `close()` is idempotent and never raises.
526
+
527
+ Prefer the context manager so resources are released automatically:
528
+
529
+ ```python
530
+ with RelyingPartyClientSdk(config) as sdk:
531
+ participants = sdk.get_participants()
532
+ # telemetry resources are released on exit
533
+ ```
534
+
535
+ Or call `close()` explicitly when you manage the lifecycle yourself:
536
+
537
+ ```python
538
+ sdk = RelyingPartyClientSdk(config)
539
+ try:
540
+ participants = sdk.get_participants()
541
+ finally:
542
+ sdk.close()
543
+ ```
544
+
396
545
  ## Conformance test
397
546
 
398
547
  An automated conformance test has been added. To execute the conformance test, a token is required,
@@ -430,6 +579,14 @@ poetry publish
430
579
 
431
580
  ## Release Notes
432
581
 
582
+ ### 1.5.1 (Sep 25, 2026)
583
+ - **Telemetry disabled by default**: Telemetry is temporarily disabled while a bugfix is in progress. Please do not enable telemetry until the fix is released.
584
+
585
+ ### 1.5.0 (Sep 22, 2026)
586
+ - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
587
+ - Supports ASYNC (default) and SYNC delivery modes
588
+ - Configurable via `TelemetryConfig` on `CustomConfig` — disable with `TelemetryConfig(enabled=False)`
589
+
433
590
  ### 1.4.1 (Aug 4, 2026)
434
591
  - Security: require cryptography 50.0.0 or newer to address CVE-2026-69247 and CVE-2026-69249
435
592
  - Removed the cryptography major-version ceiling so consumers can adopt future security releases without waiting for an SDK release
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.4.1"
3
+ version = "1.5.1"
4
4
  description = "Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem"
5
5
  authors = ["Erik Pragt <erik.pragt@connectid.com.au>"]
6
6
  license = "Proprietary"
@@ -14,11 +14,15 @@ cryptography = ">=50.0.0"
14
14
  joserfc = "^1.6.8"
15
15
  cffi = "^2.0.0"
16
16
  urllib3 = "^2.7.0"
17
+ # Already present transitively via requests; declared explicitly because the telemetry TLS
18
+ # context imports it directly to trust the public roots that sign the Registry endpoints.
19
+ certifi = "^2026.4.22"
17
20
 
18
21
  [tool.poetry.group.test.dependencies]
19
22
  pytest = "^9.0.0"
20
23
  httpx = "^0.28.1"
21
24
  toml = "^0.10.2"
25
+ jsonschema = { extras = ["format-nongpl"], version = "^4.25.1" }
22
26
 
23
27
  [tool.poetry.group.dev.dependencies]
24
28
  black = "^26.5.1"
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import time
3
- import uuid
4
3
  from typing import Any
5
4
  from urllib.parse import quote
6
5
 
@@ -11,12 +10,16 @@ from authlib.oauth2.rfc7636 import create_s256_code_challenge
11
10
 
12
11
  from rp_python_sdk.endpoints.get_participants import get_auth_server_details, \
13
12
  get_issuer_metadata
13
+ from rp_python_sdk.endpoints.util.client_assertion import create_client_assertion
14
14
  from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id, log_x_fapi_interaction_id_matches
15
15
  from rp_python_sdk.model import PARResponse
16
16
  from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
17
17
  from rp_python_sdk.sdk_config import SdkConfig
18
18
  from rp_python_sdk.setup_logger import logger
19
19
  from rp_python_sdk.cache.http_cache import HttpCache
20
+ from rp_python_sdk.telemetry.telemetry_context import ParTelemetryContext
21
+ from rp_python_sdk.telemetry.telemetry_event_factory import create_par_request_event
22
+ from rp_python_sdk.telemetry.telemetry_sender import NoopTelemetrySender, TelemetrySender
20
23
 
21
24
  extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
22
25
  "beneficiary_account_au_payid", "beneficiary_account_international"}
@@ -27,7 +30,8 @@ def send_pushed_authorisation_request(config: SdkConfig,
27
30
  essential_claims: set[str],
28
31
  voluntary_claims: set[str],
29
32
  purpose: str,
30
- cache: HttpCache) -> PARResponse:
33
+ cache: HttpCache,
34
+ telemetry_sender: TelemetrySender = NoopTelemetrySender.instance()) -> PARResponse:
31
35
  _check_valid_purpose(purpose)
32
36
 
33
37
  checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
@@ -39,7 +43,7 @@ def send_pushed_authorisation_request(config: SdkConfig,
39
43
  auth_server_url = issuer_metadata.issuer
40
44
  x_fapi_interaction_id = create_x_fapi_interaction_id()
41
45
 
42
- logger.info(
46
+ logger.debug(
43
47
  f"Sending PAR to auth server: {authorisation_server_id} - {authorisation_server.customer_friendly_name}, "
44
48
  f"essential claims requested: {checked_essential_claims}, voluntary claims requested: {voluntary_claims}, "
45
49
  f"x-fapi-interaction-id: {x_fapi_interaction_id}, purpose: {purpose}")
@@ -57,7 +61,6 @@ def send_pushed_authorisation_request(config: SdkConfig,
57
61
  state = generate_token()
58
62
  nonce = generate_token()
59
63
  scope = "openid"
60
- jti = str(uuid.uuid4())
61
64
 
62
65
  code_verifier = generate_token(48)
63
66
  code_challenge = create_s256_code_challenge(code_verifier)
@@ -84,22 +87,12 @@ def send_pushed_authorisation_request(config: SdkConfig,
84
87
  jws = JsonWebSignature()
85
88
  request_jwt = jws.serialize_compact(header, json.dumps(request_payload), config.signing_key)
86
89
 
87
- client_assertion_payload = {
88
- 'sub': client_id,
89
- 'aud': auth_server_url,
90
- 'iss': client_id,
91
- 'exp': exp,
92
- 'jti': jti,
93
- 'iat': iat,
94
- }
95
-
96
- client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload),
97
- config.signing_key)
90
+ client_assertion_jwt = create_client_assertion(config.signing_key, config.signing_kid, client_id, auth_server_url)
98
91
 
99
92
  # Data payload
100
93
  data = {
101
94
  "request": request_jwt.decode("utf-8"),
102
- "client_assertion": client_assertion_jwt.decode("utf-8"),
95
+ "client_assertion": client_assertion_jwt,
103
96
  "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
104
97
  }
105
98
 
@@ -109,25 +102,42 @@ def send_pushed_authorisation_request(config: SdkConfig,
109
102
  "x-fapi-interaction-id": x_fapi_interaction_id
110
103
  }
111
104
 
112
- # Making the POST request
113
- response = requests.post(par_endpoint, data=data, headers=headers,
114
- cert=(config.transport_pem, config.transport_key),
115
- verify=config.ca_pem)
116
-
117
- log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
118
- x_fapi_interaction_id, response)
119
-
120
- if not response.ok:
121
- raise RelyingPartyClientSdkException(f"Response code for PAR to {authorisation_server_id} not successful, "
122
- f"was: {response.status_code} x-fapi-interaction-id: {x_fapi_interaction_id}")
123
-
124
- request_uri = response.json()['request_uri']
125
- logger.debug(f"PAR was sent, request_uri='{request_uri}'")
105
+ context = ParTelemetryContext()
106
+ http_status_code = None
107
+ try:
108
+ # Making the POST request
109
+ response = requests.post(par_endpoint, data=data, headers=headers,
110
+ cert=(config.transport_pem, config.transport_key),
111
+ verify=config.ca_pem)
112
+ http_status_code = response.status_code
113
+
114
+ log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
115
+ x_fapi_interaction_id, response)
116
+
117
+ if not response.ok:
118
+ raise RelyingPartyClientSdkException(
119
+ f"Response code for PAR to {authorisation_server_id} not successful, "
120
+ f"was: {response.status_code} x-fapi-interaction-id: {x_fapi_interaction_id}, "
121
+ f"response body: {response.text}")
122
+
123
+ body = response.json()
124
+ raw_expires_in = body.get("expires_in")
125
+ expires_in = raw_expires_in if isinstance(raw_expires_in, int) else None
126
+ request_uri = body['request_uri']
127
+ logger.debug(f"PAR was sent, request_uri='{request_uri}'")
128
+
129
+ context.record_success(http_status_code, expires_in, request_uri)
130
+ except Exception as exception:
131
+ context.record_failure(http_status_code, str(exception))
132
+ raise
133
+ finally:
134
+ event = create_par_request_event(config, x_fapi_interaction_id, authorisation_server_id,
135
+ state, purpose, claim_request, context)
136
+ telemetry_sender.send(event)
126
137
 
127
138
  return PARResponse(_construct_auth_url(auth_endpoint, client_id, request_uri),
128
139
  code_verifier, state, nonce, x_fapi_interaction_id)
129
140
 
130
-
131
141
  def _check_valid_purpose(purpose: str) -> None:
132
142
  if len(purpose) < 3 or len(purpose) > 300:
133
143
  raise RelyingPartyClientSdkException("Purpose must be between 3 and 300 characters")