nui-lambda-shared-utils 1.1.4__tar.gz → 1.2.0__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 (71) hide show
  1. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/CLAUDE.md +2 -0
  2. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/PKG-INFO +28 -1
  3. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/README.md +20 -0
  4. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/README.md +4 -1
  5. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/getting-started/configuration.md +11 -0
  6. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/getting-started/installation.md +7 -0
  7. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/getting-started/quickstart.md +36 -1
  8. nui_lambda_shared_utils-1.2.0/docs/guides/jwt-authentication.md +148 -0
  9. nui_lambda_shared_utils-1.2.0/docs/guides/log-processing.md +156 -0
  10. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/__init__.py +35 -0
  11. nui_lambda_shared_utils-1.2.0/nui_lambda_shared_utils/jwt_auth.py +218 -0
  12. nui_lambda_shared_utils-1.2.0/nui_lambda_shared_utils/log_processors.py +172 -0
  13. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/powertools_helpers.py +5 -1
  14. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_client.py +45 -28
  15. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils.egg-info/SOURCES.txt +6 -0
  16. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/pyproject.toml +6 -0
  17. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/requirements-test.txt +3 -1
  18. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/setup.py +10 -0
  19. nui_lambda_shared_utils-1.2.0/tests/test_jwt_auth.py +276 -0
  20. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_lambda_helpers.py +6 -0
  21. nui_lambda_shared_utils-1.2.0/tests/test_log_processors.py +236 -0
  22. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_slack_client.py +127 -105
  23. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/.editorconfig +0 -0
  24. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/.github/workflows/ci.yml +0 -0
  25. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/.github/workflows/publish.yml +0 -0
  26. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/.github/workflows/test.yml +0 -0
  27. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/.markdownlint-cli2.yaml +0 -0
  28. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/CONTRIBUTING.md +0 -0
  29. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/LICENSE +0 -0
  30. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/MANIFEST.in +0 -0
  31. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/development/testing.md +0 -0
  32. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/cli-tools.md +0 -0
  33. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/elasticsearch-integration.md +0 -0
  34. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/lambda-utilities.md +0 -0
  35. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/powertools-integration.md +0 -0
  36. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/shared-types.md +0 -0
  37. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/docs/guides/slack-integration.md +0 -0
  38. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/mypy.ini +0 -0
  39. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/base_client.py +0 -0
  40. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/cli.py +0 -0
  41. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/cloudwatch_metrics.py +0 -0
  42. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/config.py +0 -0
  43. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/db_client.py +0 -0
  44. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/error_handler.py +0 -0
  45. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/es_client.py +0 -0
  46. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/es_query_builder.py +0 -0
  47. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/lambda_helpers.py +0 -0
  48. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/secrets_helper.py +0 -0
  49. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_formatter.py +0 -0
  50. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_setup/__init__.py +0 -0
  51. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_setup/channel_creator.py +0 -0
  52. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_setup/channel_definitions.py +0 -0
  53. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/slack_setup/setup_helpers.py +0 -0
  54. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/timezone.py +0 -0
  55. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/nui_lambda_shared_utils/utils.py +0 -0
  56. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/pytest.ini +0 -0
  57. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/setup.cfg +0 -0
  58. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/__init__.py +0 -0
  59. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_aws_utils.py +0 -0
  60. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_base_client.py +0 -0
  61. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_cloudwatch_metrics.py +0 -0
  62. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_config.py +0 -0
  63. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_db_client.py +0 -0
  64. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_error_handler.py +0 -0
  65. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_es_client.py +0 -0
  66. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_es_query_builder.py +0 -0
  67. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_powertools_helpers.py +0 -0
  68. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_secrets_helper.py +0 -0
  69. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_slack_formatter.py +0 -0
  70. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_timezone.py +0 -0
  71. {nui_lambda_shared_utils-1.1.4 → nui_lambda_shared_utils-1.2.0}/tests/test_utils.py +0 -0
@@ -37,6 +37,7 @@ This is `nui-lambda-shared-utils`, a Python package providing production-ready u
37
37
  - **Elasticsearch** (`es_client.py`, `es_query_builder.py`) - Query builders and health monitoring
38
38
  - **Database** (`db_client.py`) - Connection pooling with retry logic
39
39
  - **Metrics** (`cloudwatch_metrics.py`) - Batched CloudWatch publishing with decorators
40
+ - **JWT Authentication** (`jwt_auth.py`) - RS256 token validation for API Gateway Lambdas (uses `rsa` package)
40
41
  - **Error Handling** (`error_handler.py`) - Retry patterns with exponential backoff
41
42
  - **Timezone Utils** (`timezone.py`) - Timezone conversion and formatting utilities
42
43
 
@@ -47,6 +48,7 @@ The package uses optional extras to minimize Lambda bundle size:
47
48
  - `elasticsearch` - Elasticsearch client and query builders
48
49
  - `database` - MySQL/PostgreSQL drivers
49
50
  - `slack` - Slack SDK
51
+ - `jwt` - RS256 JWT validation (`rsa` package)
50
52
  - `all` - All integrations
51
53
  - `dev` - Development and testing tools
52
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-lambda-shared-utils
3
- Version: 1.1.4
3
+ Version: 1.2.0
4
4
  Summary: Enterprise-grade utilities for AWS Lambda functions with Slack, Elasticsearch, and monitoring integrations
5
5
  Home-page: https://github.com/nuimarkets/nui-lambda-shared-utils
6
6
  Author: NUI Markets
@@ -41,6 +41,8 @@ Requires-Dist: slack-sdk>=3.19.0; extra == "slack"
41
41
  Provides-Extra: powertools
42
42
  Requires-Dist: aws-lambda-powertools<4.0.0,>=3.6.0; extra == "powertools"
43
43
  Requires-Dist: coloredlogs>=15.0; extra == "powertools"
44
+ Provides-Extra: jwt
45
+ Requires-Dist: rsa>=4.9; extra == "jwt"
44
46
  Provides-Extra: all
45
47
  Requires-Dist: elasticsearch<8.0.0,>=7.17.0; extra == "all"
46
48
  Requires-Dist: pymysql>=1.0.0; extra == "all"
@@ -48,6 +50,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
48
50
  Requires-Dist: slack-sdk>=3.19.0; extra == "all"
49
51
  Requires-Dist: aws-lambda-powertools<4.0.0,>=3.6.0; extra == "all"
50
52
  Requires-Dist: coloredlogs>=15.0; extra == "all"
53
+ Requires-Dist: rsa>=4.9; extra == "all"
51
54
  Provides-Extra: dev
52
55
  Requires-Dist: pytest>=7.0.0; extra == "dev"
53
56
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -56,8 +59,12 @@ Requires-Dist: moto>=4.0.0; extra == "dev"
56
59
  Requires-Dist: black>=22.0.0; extra == "dev"
57
60
  Requires-Dist: mypy>=0.990; extra == "dev"
58
61
  Requires-Dist: boto3-stubs[essential]>=1.20.0; extra == "dev"
62
+ Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
63
+ Requires-Dist: types-pytz>=2021.3.0; extra == "dev"
59
64
  Requires-Dist: twine>=4.0.0; extra == "dev"
60
65
  Requires-Dist: build>=0.8.0; extra == "dev"
66
+ Requires-Dist: rsa>=4.9; extra == "dev"
67
+ Requires-Dist: cryptography>=41.0.0; extra == "dev"
61
68
  Dynamic: author
62
69
  Dynamic: home-page
63
70
  Dynamic: license-file
@@ -99,6 +106,7 @@ Production-ready utilities for AWS Lambda functions with Slack, Elasticsearch, d
99
106
  - **Elasticsearch Operations** - Query builders, index management, and health monitoring
100
107
  - **Database Connections** - Connection pooling, automatic retries, and transaction management
101
108
  - **CloudWatch Metrics** - Batched publishing with custom dimensions
109
+ - **JWT Authentication** - RS256 token validation for API Gateway Lambdas (lightweight, no PyJWT needed)
102
110
  - **Error Handling** - Intelligent retry patterns with exponential backoff
103
111
  - **Timezone Utilities** - Timezone handling and formatting
104
112
  - **Configurable Defaults** - Environment-aware configuration system
@@ -134,6 +142,7 @@ pip install nui-lambda-shared-utils[powertools] # AWS Powertools only
134
142
  pip install nui-lambda-shared-utils[slack] # Slack only
135
143
  pip install nui-lambda-shared-utils[elasticsearch] # Elasticsearch only
136
144
  pip install nui-lambda-shared-utils[database] # Database only
145
+ pip install nui-lambda-shared-utils[jwt] # JWT authentication only
137
146
  ```
138
147
 
139
148
  ### Basic Configuration
@@ -276,6 +285,23 @@ def lambda_handler(event, context):
276
285
 
277
286
  **[→ See full metrics guide](docs/getting-started/quickstart.md#cloudwatch-metrics)**
278
287
 
288
+ ### JWT Authentication
289
+
290
+ ```python
291
+ from nui_lambda_shared_utils import require_auth, AuthenticationError
292
+
293
+ def lambda_handler(event, context):
294
+ try:
295
+ claims = require_auth(event) # Validates Bearer token from Authorization header
296
+ except AuthenticationError as e:
297
+ return {"statusCode": 401, "body": "Unauthorized"}
298
+
299
+ user_id = claims["sub"]
300
+ return {"statusCode": 200, "body": f"Hello {user_id}"}
301
+ ```
302
+
303
+ **[→ See JWT authentication guide](docs/guides/jwt-authentication.md)**
304
+
279
305
  ### Error Handling
280
306
 
281
307
  ```python
@@ -330,6 +356,7 @@ This package requires AWS Secrets Manager for credential storage and IAM permiss
330
356
  - Elasticsearch: `{"host": "...", "username": "...", "password": "..."}`
331
357
  - Database: `{"host": "...", "port": 3306, "username": "...", "password": "...", "database": "..."}`
332
358
  - Slack: `{"bot_token": "xoxb-...", "webhook_url": "..."}`
359
+ - JWT Public Key: `{"TOKEN_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\n..."}`
333
360
 
334
361
  **IAM Permissions** - Lambda execution role needs:
335
362
 
@@ -34,6 +34,7 @@ Production-ready utilities for AWS Lambda functions with Slack, Elasticsearch, d
34
34
  - **Elasticsearch Operations** - Query builders, index management, and health monitoring
35
35
  - **Database Connections** - Connection pooling, automatic retries, and transaction management
36
36
  - **CloudWatch Metrics** - Batched publishing with custom dimensions
37
+ - **JWT Authentication** - RS256 token validation for API Gateway Lambdas (lightweight, no PyJWT needed)
37
38
  - **Error Handling** - Intelligent retry patterns with exponential backoff
38
39
  - **Timezone Utilities** - Timezone handling and formatting
39
40
  - **Configurable Defaults** - Environment-aware configuration system
@@ -69,6 +70,7 @@ pip install nui-lambda-shared-utils[powertools] # AWS Powertools only
69
70
  pip install nui-lambda-shared-utils[slack] # Slack only
70
71
  pip install nui-lambda-shared-utils[elasticsearch] # Elasticsearch only
71
72
  pip install nui-lambda-shared-utils[database] # Database only
73
+ pip install nui-lambda-shared-utils[jwt] # JWT authentication only
72
74
  ```
73
75
 
74
76
  ### Basic Configuration
@@ -211,6 +213,23 @@ def lambda_handler(event, context):
211
213
 
212
214
  **[→ See full metrics guide](docs/getting-started/quickstart.md#cloudwatch-metrics)**
213
215
 
216
+ ### JWT Authentication
217
+
218
+ ```python
219
+ from nui_lambda_shared_utils import require_auth, AuthenticationError
220
+
221
+ def lambda_handler(event, context):
222
+ try:
223
+ claims = require_auth(event) # Validates Bearer token from Authorization header
224
+ except AuthenticationError as e:
225
+ return {"statusCode": 401, "body": "Unauthorized"}
226
+
227
+ user_id = claims["sub"]
228
+ return {"statusCode": 200, "body": f"Hello {user_id}"}
229
+ ```
230
+
231
+ **[→ See JWT authentication guide](docs/guides/jwt-authentication.md)**
232
+
214
233
  ### Error Handling
215
234
 
216
235
  ```python
@@ -265,6 +284,7 @@ This package requires AWS Secrets Manager for credential storage and IAM permiss
265
284
  - Elasticsearch: `{"host": "...", "username": "...", "password": "..."}`
266
285
  - Database: `{"host": "...", "port": 3306, "username": "...", "password": "...", "database": "..."}`
267
286
  - Slack: `{"bot_token": "xoxb-...", "webhook_url": "..."}`
287
+ - JWT Public Key: `{"TOKEN_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\n..."}`
268
288
 
269
289
  **IAM Permissions** - Lambda execution role needs:
270
290
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Welcome to the comprehensive documentation for `nui-lambda-shared-utils`.
4
4
 
5
- **Last Updated**: 2025-01-19
5
+ **Last Updated**: 2026-02-13
6
6
 
7
7
  ## Quick Navigation
8
8
 
@@ -20,6 +20,8 @@ Component-specific guides for major features:
20
20
  - **[Lambda Context Helpers](guides/lambda-utilities.md)** - Environment info extraction for logging and metrics
21
21
  - **[Slack Integration](guides/slack-integration.md)** - Messaging, formatting, and file uploads
22
22
  - **[Elasticsearch Integration](guides/elasticsearch-integration.md)** - Search, bulk indexing, health checks
23
+ - **[JWT Authentication](guides/jwt-authentication.md)** - RS256 token validation for API Gateway Lambdas
24
+ - **[Log Processing](guides/log-processing.md)** - Kinesis log extraction and ES index naming
23
25
  - Database Connections (planned)
24
26
  - Error Handling Patterns (planned)
25
27
  - CloudWatch Metrics (planned)
@@ -122,6 +124,7 @@ When contributing to documentation:
122
124
  - Lambda context helpers guide (guides/lambda-utilities.md)
123
125
  - Slack integration guide (guides/slack-integration.md)
124
126
  - Elasticsearch integration guide (guides/elasticsearch-integration.md)
127
+ - JWT authentication guide (guides/jwt-authentication.md)
125
128
  - Shared types reference (guides/shared-types.md)
126
129
  - CLI tools guide (guides/cli-tools.md)
127
130
  - Testing guide (development/testing.md)
@@ -20,6 +20,7 @@ The package uses a hierarchical configuration system with the following priority
20
20
  | `ES_CREDENTIALS_SECRET` | AWS secret name for Elasticsearch credentials | `elasticsearch-credentials` |
21
21
  | `DB_CREDENTIALS_SECRET` | AWS secret name for database credentials | `database-credentials` |
22
22
  | `SLACK_CREDENTIALS_SECRET` | AWS secret name for Slack credentials | `slack-credentials` |
23
+ | `JWT_PUBLIC_KEY_SECRET` | AWS secret name for JWT public key | *(none)* |
23
24
  | `AWS_REGION` | AWS region for services | `us-east-1` |
24
25
 
25
26
  ### Alternative Variable Names
@@ -143,6 +144,16 @@ The package expects AWS secrets in specific JSON formats:
143
144
 
144
145
  - `webhook_url` - Alternative to bot token for simple messaging
145
146
 
147
+ #### JWT Public Key
148
+
149
+ ```json
150
+ {
151
+ "TOKEN_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...\n-----END PUBLIC KEY-----"
152
+ }
153
+ ```
154
+
155
+ The key must be in PKCS#8 PEM format (`BEGIN PUBLIC KEY`). The field name is configurable via the `key_field` parameter on `get_jwt_public_key()`.
156
+
146
157
  ### Creating Secrets
147
158
 
148
159
  #### Using AWS CLI
@@ -31,6 +31,9 @@ pip install nui-lambda-shared-utils[elasticsearch]
31
31
  # Database integration only
32
32
  pip install nui-lambda-shared-utils[database]
33
33
 
34
+ # JWT authentication only
35
+ pip install nui-lambda-shared-utils[jwt]
36
+
34
37
  # All integrations
35
38
  pip install nui-lambda-shared-utils[all]
36
39
 
@@ -75,6 +78,10 @@ pip install -e .[dev]
75
78
 
76
79
  - `slack-sdk>=3.19.0` - Official Slack SDK
77
80
 
81
+ #### jwt
82
+
83
+ - `rsa>=4.9` - Pure Python RSA implementation (~100KB, no C extensions)
84
+
78
85
  #### dev
79
86
 
80
87
  - `pytest>=7.0.0` - Testing framework
@@ -223,7 +223,42 @@ def lambda_handler(event, context):
223
223
  raise
224
224
  ```
225
225
 
226
- ### 5. AWS Powertools Integration
226
+ ### 7. JWT Authentication for API Gateway
227
+
228
+ ```python
229
+ import nui_lambda_shared_utils as nui
230
+
231
+ def lambda_handler(event, context):
232
+ """API Gateway Lambda with JWT auth."""
233
+ # Validate Bearer token from Authorization header
234
+ # Fetches public key from Secrets Manager (cached automatically)
235
+ try:
236
+ claims = nui.require_auth(event)
237
+ except nui.AuthenticationError:
238
+ return {"statusCode": 401, "body": "Unauthorized"}
239
+
240
+ # Access token claims
241
+ user_id = claims["sub"]
242
+ role = claims.get("role", "user")
243
+
244
+ return {"statusCode": 200, "body": f"Hello {user_id} ({role})"}
245
+ ```
246
+
247
+ **Installation:**
248
+
249
+ ```bash
250
+ pip install nui-lambda-shared-utils[jwt]
251
+ ```
252
+
253
+ **Requirements:**
254
+
255
+ - `JWT_PUBLIC_KEY_SECRET` env var pointing to Secrets Manager secret
256
+ - Secret contains `{"TOKEN_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\n..."}` (PKCS#8 PEM)
257
+ - IAM permission: `secretsmanager:GetSecretValue`
258
+
259
+ **See:** [JWT Authentication Guide](../guides/jwt-authentication.md) for key management and advanced usage.
260
+
261
+ ### 8. AWS Powertools Integration
227
262
 
228
263
  For production Lambda functions, use AWS Powertools for standardized logging, metrics, and error handling:
229
264
 
@@ -0,0 +1,148 @@
1
+ # JWT Authentication Guide
2
+
3
+ RS256 JWT token validation for AWS Lambda functions behind API Gateway.
4
+
5
+ ## Overview
6
+
7
+ The `jwt_auth` module validates RS256-signed JWTs using public keys stored in AWS Secrets Manager. It uses the pure-Python `rsa` package (~100KB) instead of PyJWT or `cryptography` (~35MB), keeping Lambda bundles small without needing a Lambda Layer.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install nui-lambda-shared-utils[jwt]
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ from nui_lambda_shared_utils import require_auth, AuthenticationError
19
+
20
+ def lambda_handler(event, context):
21
+ try:
22
+ claims = require_auth(event)
23
+ except AuthenticationError:
24
+ return {"statusCode": 401, "body": "Unauthorized"}
25
+
26
+ return {"statusCode": 200, "body": f"Hello {claims['sub']}"}
27
+ ```
28
+
29
+ ## API Reference
30
+
31
+ ### `require_auth(event, secret_name=None) -> dict`
32
+
33
+ One-call authentication for API Gateway Lambda handlers. Extracts the Bearer token from the Authorization header, fetches the public key from Secrets Manager, and validates the token.
34
+
35
+ - **event** — API Gateway Lambda proxy event (v1 REST or v2 HTTP API)
36
+ - **secret_name** — Secrets Manager secret name (falls back to `JWT_PUBLIC_KEY_SECRET` env var)
37
+ - **Returns** — Decoded claims dict
38
+ - **Raises** — `AuthenticationError` on any failure (missing header, bad token, expired, bad signature, missing key)
39
+
40
+ ### `validate_jwt(token, public_key) -> dict`
41
+
42
+ Lower-level validation when you already have the public key. Verifies token structure, RS256 signature, and expiration.
43
+
44
+ - **token** — Raw JWT string (no `Bearer` prefix)
45
+ - **public_key** — `rsa.PublicKey` instance
46
+ - **Returns** — Decoded claims dict
47
+ - **Raises** — `JWTValidationError` on failure
48
+
49
+ ### `get_jwt_public_key(secret_name=None, key_field="TOKEN_PUBLIC_KEY") -> rsa.PublicKey`
50
+
51
+ Fetches and parses the PEM public key from Secrets Manager. Uses `secrets_helper` cache — repeated calls don't make additional API calls.
52
+
53
+ - **secret_name** — Falls back to `JWT_PUBLIC_KEY_SECRET` env var
54
+ - **key_field** — JSON field in the secret containing the PEM string
55
+ - **Returns** — `rsa.PublicKey`
56
+ - **Raises** — `JWTValidationError` if secret or field is missing/invalid
57
+
58
+ ### Exceptions
59
+
60
+ - **`JWTValidationError`** — Base exception for token validation failures
61
+ - **`AuthenticationError(JWTValidationError)`** — Authentication-level failures (what `require_auth` raises)
62
+
63
+ ## AWS Setup
64
+
65
+ ### 1. Store the Public Key
66
+
67
+ ```bash
68
+ aws secretsmanager create-secret \
69
+ --name "prod/jwt-public-key" \
70
+ --secret-string '{"TOKEN_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...\n-----END PUBLIC KEY-----"}'
71
+ ```
72
+
73
+ The key must be PKCS#8 PEM format (`BEGIN PUBLIC KEY`, not `BEGIN RSA PUBLIC KEY`).
74
+
75
+ ### 2. Lambda Environment
76
+
77
+ ```yaml
78
+ # serverless.yml
79
+ environment:
80
+ JWT_PUBLIC_KEY_SECRET: prod/jwt-public-key
81
+ ```
82
+
83
+ ### 3. IAM Permissions
84
+
85
+ ```yaml
86
+ # serverless.yml
87
+ iamRoleStatements:
88
+ - Effect: Allow
89
+ Action: secretsmanager:GetSecretValue
90
+ Resource: arn:aws:secretsmanager:*:*:secret:prod/jwt-public-key-*
91
+ ```
92
+
93
+ ## Usage Patterns
94
+
95
+ ### Skip Auth for Health Endpoints
96
+
97
+ ```python
98
+ def lambda_handler(event, context):
99
+ path = event.get("path") or event.get("rawPath", "")
100
+ if path == "/health":
101
+ return {"statusCode": 200, "body": "ok"}
102
+
103
+ try:
104
+ claims = require_auth(event)
105
+ except AuthenticationError:
106
+ return {"statusCode": 401, "body": "Unauthorized"}
107
+
108
+ # Authenticated route handling
109
+ return handle_request(event, claims)
110
+ ```
111
+
112
+ ### Direct Validation (Pre-fetched Key)
113
+
114
+ ```python
115
+ from nui_lambda_shared_utils import get_jwt_public_key, validate_jwt, JWTValidationError
116
+
117
+ # Fetch key once at module level (cached by secrets_helper)
118
+ public_key = get_jwt_public_key(secret_name="prod/jwt-public-key")
119
+
120
+ def validate_token(token: str) -> dict:
121
+ try:
122
+ return validate_jwt(token, public_key)
123
+ except JWTValidationError as e:
124
+ raise ValueError(f"Bad token: {e}")
125
+ ```
126
+
127
+ ### Custom Key Field Name
128
+
129
+ ```python
130
+ # If your secret uses a different field name
131
+ key = get_jwt_public_key(
132
+ secret_name="prod/auth-keys",
133
+ key_field="RSA_PUBLIC_KEY"
134
+ )
135
+ ```
136
+
137
+ ## What Gets Validated
138
+
139
+ | Check | Behavior |
140
+ |-------|----------|
141
+ | Token structure | Must be 3 dot-separated base64url segments |
142
+ | Algorithm | Header `alg` must be `RS256` |
143
+ | Signature | RSA PKCS#1 v1.5 with SHA-256 |
144
+ | Expiration | `exp` claim checked against current time (optional — tokens without `exp` are accepted) |
145
+
146
+ ## Dependency Details
147
+
148
+ The module uses `rsa` (pure Python, ~100KB) for signature verification at runtime. This avoids the ~35MB `cryptography` C extension that PyJWT requires for RS256. The `cryptography` package is only needed in dev for generating test key pairs and signing test tokens.
@@ -0,0 +1,156 @@
1
+ # Log Processing Guide
2
+
3
+ This guide covers the log processing utilities for Lambda functions that stream
4
+ CloudWatch logs to Elasticsearch via Kinesis.
5
+
6
+ ## Overview
7
+
8
+ The `log_processors` module provides utilities for:
9
+
10
+ - Extracting CloudWatch logs from Kinesis stream records
11
+ - Deriving Elasticsearch index names from log metadata
12
+ - Type definitions for CloudWatch log structures
13
+
14
+ ## Quick Start
15
+
16
+ ### Basic Usage
17
+
18
+ ```python
19
+ from elasticsearch.helpers import streaming_bulk
20
+ from nui_lambda_shared_utils.log_processors import (
21
+ extract_cloudwatch_logs_from_kinesis,
22
+ derive_index_name,
23
+ )
24
+ from datetime import datetime, timezone
25
+
26
+
27
+ def process_log_events(log_group: str, log_stream: str, log_events: list):
28
+ """Process log events and yield ES documents."""
29
+ for event in log_events:
30
+ ts = datetime.fromtimestamp(event["timestamp"] / 1000.0, tz=timezone.utc)
31
+
32
+ yield {
33
+ "_index": derive_index_name(log_group, ts),
34
+ "_id": event["id"],
35
+ "_source": {
36
+ "message": event["message"],
37
+ "@timestamp": ts.isoformat(),
38
+ "log": {"group": log_group, "stream": log_stream},
39
+ }
40
+ }
41
+
42
+
43
+ def handler(event, context):
44
+ """Lambda handler for Kinesis -> ES streaming."""
45
+ es = get_elasticsearch_client()
46
+
47
+ for ok, response in streaming_bulk(
48
+ client=es,
49
+ actions=extract_cloudwatch_logs_from_kinesis(
50
+ event["Records"],
51
+ process_fn=process_log_events
52
+ ),
53
+ chunk_size=100,
54
+ raise_on_error=True,
55
+ ):
56
+ if not ok:
57
+ logger.error(f"Document indexing failed: {response}")
58
+ ```
59
+
60
+ ### Error Handling
61
+
62
+ Provide an `on_error` callback to handle failures without stopping the entire batch:
63
+
64
+ ```python
65
+ def handle_processing_error(exception: Exception, record_data: dict):
66
+ """Log errors but continue processing."""
67
+ logger.error(f"Failed to process record: {exception}")
68
+ # Optionally send to dead letter queue, metrics, etc.
69
+
70
+
71
+ for doc in extract_cloudwatch_logs_from_kinesis(
72
+ event["Records"],
73
+ process_fn=process_log_events,
74
+ on_error=handle_processing_error
75
+ ):
76
+ # Documents from successfully processed records
77
+ pass
78
+ ```
79
+
80
+ ### Custom Index Naming
81
+
82
+ Override the default index naming pattern:
83
+
84
+ ```python
85
+ from nui_lambda_shared_utils.log_processors import derive_index_name
86
+
87
+ # Default: log-{service}-{YYYY}-m{MM}
88
+ derive_index_name("/aws/lambda/orders", ts)
89
+ # -> "log-orders-2025-m01"
90
+
91
+ # Custom target
92
+ derive_index_name("/aws/lambda/orders", ts, target_override="order-service")
93
+ # -> "log-order-service-2025-m01"
94
+
95
+ # Custom prefix and date format
96
+ derive_index_name("/aws/lambda/orders", ts, prefix="logs", date_format="%Y-%m-%d")
97
+ # -> "logs-orders-2025-01-15"
98
+ ```
99
+
100
+ ## Migration Guide
101
+
102
+ ### From inline Kinesis extraction
103
+
104
+ Replace:
105
+
106
+ ```python
107
+ # Before
108
+ def extract_logs(records):
109
+ for row in records:
110
+ raw_data = row["kinesis"]["data"]
111
+ data = json.loads(
112
+ zlib.decompress(base64.b64decode(raw_data), 16 + zlib.MAX_WBITS).decode("utf-8")
113
+ )
114
+ if data["messageType"] == "CONTROL_MESSAGE":
115
+ continue
116
+ for item in process_log_events(...):
117
+ yield item
118
+ ```
119
+
120
+ With:
121
+
122
+ ```python
123
+ # After
124
+ from nui_lambda_shared_utils.log_processors import extract_cloudwatch_logs_from_kinesis
125
+
126
+ for doc in extract_cloudwatch_logs_from_kinesis(event["Records"], process_log_events):
127
+ yield doc
128
+ ```
129
+
130
+ ## API Reference
131
+
132
+ ### `extract_cloudwatch_logs_from_kinesis()`
133
+
134
+ Extract CloudWatch logs from Kinesis stream records.
135
+
136
+ **Parameters:**
137
+
138
+ - `records`: List of Kinesis event records (`event["Records"]`)
139
+ - `process_fn`: Callback to process log events
140
+ - `on_error`: Optional error handler (if None, exceptions are raised)
141
+
142
+ **Yields:** Dict documents ready for `streaming_bulk()`
143
+
144
+ ### `derive_index_name()`
145
+
146
+ Derive Elasticsearch index name from log metadata.
147
+
148
+ **Parameters:**
149
+
150
+ - `log_group`: CloudWatch log group name
151
+ - `timestamp`: Event timestamp for date suffix
152
+ - `prefix`: Index name prefix (default: "log")
153
+ - `date_format`: strftime format (default: "%Y-m%m")
154
+ - `target_override`: Custom service name (optional)
155
+
156
+ **Returns:** Index name string
@@ -121,9 +121,33 @@ except ImportError:
121
121
  get_powertools_logger = None # type: ignore
122
122
  powertools_handler = None # type: ignore
123
123
 
124
+ # Log processing utilities (no external dependencies)
125
+ from .log_processors import (
126
+ CloudWatchLogEvent,
127
+ CloudWatchLogsData,
128
+ derive_index_name,
129
+ extract_cloudwatch_logs_from_kinesis,
130
+ )
131
+
124
132
  # Lambda context helpers (no external dependencies)
125
133
  from .lambda_helpers import get_lambda_environment_info
126
134
 
135
+ # JWT authentication - optional import
136
+ try:
137
+ from .jwt_auth import (
138
+ validate_jwt,
139
+ require_auth,
140
+ get_jwt_public_key,
141
+ JWTValidationError,
142
+ AuthenticationError,
143
+ )
144
+ except ImportError:
145
+ validate_jwt = None # type: ignore
146
+ require_auth = None # type: ignore
147
+ get_jwt_public_key = None # type: ignore
148
+ JWTValidationError = None # type: ignore
149
+ AuthenticationError = None # type: ignore
150
+
127
151
  # Slack setup utilities (for CLI usage) - optional import
128
152
  try:
129
153
  from . import slack_setup
@@ -209,6 +233,17 @@ __all__ = [
209
233
  # AWS Powertools integration
210
234
  "get_powertools_logger",
211
235
  "powertools_handler",
236
+ # Log processing
237
+ "extract_cloudwatch_logs_from_kinesis",
238
+ "derive_index_name",
239
+ "CloudWatchLogEvent",
240
+ "CloudWatchLogsData",
212
241
  # Lambda context helpers
213
242
  "get_lambda_environment_info",
243
+ # JWT authentication
244
+ "validate_jwt",
245
+ "require_auth",
246
+ "get_jwt_public_key",
247
+ "JWTValidationError",
248
+ "AuthenticationError",
214
249
  ]