cdk-factory 0.12.0__py3-none-any.whl → 0.13.1__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.

Potentially problematic release.


This version of cdk-factory might be problematic. Click here for more details.

@@ -1,104 +1,187 @@
1
1
  """
2
- Lambda@Edge Origin-Request Handler for IP-based Access Gating
2
+ Lambda@Edge function for IP-based access gating.
3
3
  Geek Cafe, LLC
4
4
  Maintainers: Eric Wilson
5
- """
6
5
 
7
- import ipaddress
6
+ Since Lambda@Edge does not support environment variables, configuration
7
+ is fetched from SSM Parameter Store at runtime (with caching).
8
+ """
8
9
  import json
9
- import os
10
+ import ipaddress
11
+ import boto3
12
+ from functools import lru_cache
10
13
 
14
+ # SSM client - will be created in the region where the function executes
15
+ ssm = None
11
16
 
12
- def lambda_handler(event, context):
17
+ @lru_cache(maxsize=128)
18
+ def get_ssm_parameter(parameter_name: str, region: str = 'us-east-1') -> str:
13
19
  """
14
- Lambda@Edge origin-request handler that implements IP-based gating
15
- with maintenance site fallback.
20
+ Fetch SSM parameter with caching.
21
+ Lambda@Edge cannot use environment variables, so we fetch from SSM.
16
22
 
17
- Features:
18
- - Inject X-Viewer-IP header for origin visibility
19
- - Check viewer IP against allowlist when gate is enabled
20
- - Rewrite blocked IPs to maintenance CloudFront distribution, and serve up maintenance site
21
- - Toggle via GATE_ENABLED environment variable
22
- """
23
+ Args:
24
+ parameter_name: Name of the SSM parameter
25
+ region: AWS region (default us-east-1)
23
26
 
24
- # Extract request from CloudFront event
25
- request = event['Records'][0]['cf']['request']
26
- client_ip = request['clientIp']
27
+ Returns:
28
+ Parameter value
29
+ """
30
+ global ssm
31
+ if ssm is None:
32
+ ssm = boto3.client('ssm', region_name=region)
27
33
 
28
- # Configuration from environment variables
29
- gate_enabled = os.environ.get('GATE_ENABLED', 'false').lower() == 'true'
30
- allow_cidrs_str = os.environ.get('ALLOW_CIDRS', '')
31
- maint_cf_host = os.environ.get('MAINT_CF_HOST', '')
34
+ try:
35
+ response = ssm.get_parameter(Name=parameter_name, WithDecryption=False)
36
+ return response['Parameter']['Value']
37
+ except Exception as e:
38
+ print(f"Error fetching SSM parameter {parameter_name}: {str(e)}")
39
+ raise
40
+
41
+
42
+ def get_client_ip(request):
43
+ """Extract client IP from CloudFront request."""
44
+ if 'clientIp' in request:
45
+ return request['clientIp']
32
46
 
33
- # Parse allowed CIDRs
34
- allow_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
47
+ # Fallback to headers
48
+ headers = request.get('headers', {})
49
+ if 'x-forwarded-for' in headers:
50
+ xff = headers['x-forwarded-for'][0]['value']
51
+ return xff.split(',')[0].strip()
35
52
 
36
- # Always inject viewer IP header
37
- if 'headers' not in request:
38
- request['headers'] = {}
53
+ return None
54
+
55
+
56
+ def is_ip_allowed(client_ip: str, allowed_cidrs: list) -> bool:
57
+ """
58
+ Check if client IP is in any of the allowed CIDR ranges.
39
59
 
40
- request['headers']['x-viewer-ip'] = [{
41
- 'key': 'X-Viewer-IP',
42
- 'value': client_ip
43
- }]
60
+ Args:
61
+ client_ip: Client IP address
62
+ allowed_cidrs: List of CIDR ranges (e.g., ['10.0.0.0/8', '192.168.1.0/24'])
44
63
 
45
- # If gate is disabled, pass through to origin
46
- if not gate_enabled:
47
- return request
64
+ Returns:
65
+ True if IP is allowed, False otherwise
66
+ """
67
+ if not client_ip:
68
+ return False
48
69
 
49
- # Check if IP is in allowlist
50
- ip_allowed = False
51
70
  try:
52
71
  client_ip_obj = ipaddress.ip_address(client_ip)
53
- for cidr in allow_cidrs:
54
- try:
55
- network = ipaddress.ip_network(cidr, strict=False)
56
- if client_ip_obj in network:
57
- ip_allowed = True
58
- break
59
- except (ValueError, ipaddress.AddressValueError):
60
- # Invalid CIDR, skip
61
- continue
62
- except (ValueError, ipaddress.AddressValueError):
63
- # Invalid client IP, block by default
64
- ip_allowed = False
72
+ except ValueError as e:
73
+ print(f"Invalid client IP address: {e}")
74
+ return False
65
75
 
66
- # If IP is allowed, pass through to origin
67
- if ip_allowed:
68
- return request
76
+ # Check each CIDR individually, skipping invalid ones
77
+ for cidr in allowed_cidrs:
78
+ try:
79
+ network = ipaddress.ip_network(cidr.strip(), strict=False)
80
+ if client_ip_obj in network:
81
+ return True
82
+ except ValueError as e:
83
+ # Invalid CIDR, log and continue checking others
84
+ print(f"Invalid CIDR '{cidr}': {e}")
85
+ continue
69
86
 
70
- # IP not allowed - redirect to maintenance site
71
- if not maint_cf_host:
72
- # Safety: if maintenance host not configured, pass through with warning
73
- # In production, you might want to return a fixed error response instead
74
- return request
87
+ return False
88
+
89
+
90
+ def lambda_handler(event, context):
91
+ """
92
+ Lambda@Edge function for IP-based gating.
75
93
 
76
- # Rewrite origin to maintenance CloudFront distribution
77
- request['origin'] = {
78
- 'custom': {
79
- 'domainName': maint_cf_host,
80
- 'port': 443,
81
- 'protocol': 'https',
82
- 'path': '',
83
- 'sslProtocols': ['TLSv1.2'],
84
- 'readTimeout': 30,
85
- 'keepaliveTimeout': 5,
86
- 'customHeaders': {}
87
- }
88
- }
94
+ Configuration (fetched from SSM Parameter Store):
95
+ - GATE_ENABLED: Whether IP gating is enabled (true/false)
96
+ - ALLOW_CIDRS: Comma-separated list of allowed CIDR ranges
97
+ - MAINT_CF_HOST: CloudFront domain for maintenance/lockout page
89
98
 
90
- # Update Host header to match new origin
91
- request['headers']['host'] = [{
92
- 'key': 'Host',
93
- 'value': maint_cf_host
94
- }]
99
+ Runtime configuration is bundled in runtime_config.json by CDK.
100
+ SSM parameter paths are auto-generated by CDK as:
101
+ /{environment}/{full-function-name}/{env-var-name-kebab-case}
102
+ """
103
+ request = event['Records'][0]['cf']['request']
104
+
105
+ # Load runtime configuration bundled by CDK
106
+ # This file is created during deployment and contains environment, function name, etc.
107
+ try:
108
+ with open('runtime_config.json', 'r') as f:
109
+ runtime_config = json.load(f)
110
+
111
+ env = runtime_config.get('environment', 'dev')
112
+ function_base_name = runtime_config.get('function_name', 'ip-gate')
113
+
114
+ print(f"Runtime config loaded: environment={env}, function_name={function_base_name}")
115
+ except FileNotFoundError:
116
+ # Fallback: extract from Lambda context (less reliable)
117
+ print("Warning: runtime_config.json not found, falling back to function name parsing")
118
+ function_full_name = context.function_name
119
+
120
+ # Parse environment from function name as fallback
121
+ parts = function_full_name.split('-')
122
+ common_envs = ['dev', 'prod', 'staging', 'test', 'qa', 'uat']
123
+ env = 'dev'
124
+
125
+ for part in parts:
126
+ if part in common_envs:
127
+ env = part
128
+ break
129
+
130
+ function_base_name = 'ip-gate'
131
+ print(f"Fallback: environment={env}, function_name={function_base_name}")
95
132
 
96
- # Normalize URI - redirect directory requests to index.html
97
- uri = request.get('uri', '/')
98
- if uri.endswith('/'):
99
- request['uri'] = uri + 'index.html'
100
- elif '.' not in uri.split('/')[-1]:
101
- # No file extension, likely a directory
102
- request['uri'] = uri + '/index.html'
133
+ # Full function name for SSM paths
134
+ function_name = context.function_name
135
+ print(f"Lambda function ARN: {context.invoked_function_arn}")
103
136
 
104
- return request
137
+ try:
138
+ # Fetch configuration from SSM Parameter Store
139
+ # Auto-generated paths: /{env}/{function-name}/{key}
140
+ gate_enabled = get_ssm_parameter(f'/{env}/{function_name}/gate-enabled', 'us-east-1')
141
+
142
+ # If gating is disabled, allow all traffic
143
+ if gate_enabled.lower() not in ('true', '1', 'yes'):
144
+ print(f"IP gating is disabled (GATE_ENABLED={gate_enabled})")
145
+ return request
146
+
147
+ # Get allowed CIDRs and maintenance host
148
+ allow_cidrs_str = get_ssm_parameter(f'/{env}/{function_name}/allow-cidrs', 'us-east-1')
149
+ maint_cf_host = get_ssm_parameter(f'/{env}/{function_name}/maint-cf-host', 'us-east-1')
150
+
151
+ # Parse allowed CIDRs
152
+ allowed_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
153
+
154
+ # Get client IP
155
+ client_ip = get_client_ip(request)
156
+ print(f"Client IP: {client_ip}")
157
+
158
+ # Check if IP is allowed
159
+ if is_ip_allowed(client_ip, allowed_cidrs):
160
+ print(f"IP {client_ip} is allowed")
161
+ return request
162
+
163
+ # IP not allowed - redirect to maintenance page
164
+ print(f"IP {client_ip} is NOT allowed, redirecting to {maint_cf_host}")
165
+
166
+ response = {
167
+ 'status': '302',
168
+ 'statusDescription': 'Found',
169
+ 'headers': {
170
+ 'location': [{
171
+ 'key': 'Location',
172
+ 'value': f'https://{maint_cf_host}'
173
+ }],
174
+ 'cache-control': [{
175
+ 'key': 'Cache-Control',
176
+ 'value': 'no-cache, no-store, must-revalidate'
177
+ }]
178
+ }
179
+ }
180
+
181
+ return response
182
+
183
+ except Exception as e:
184
+ print(f"Error in IP gating function: {str(e)}")
185
+ # On error, allow the request to proceed (fail open)
186
+ # Change this to fail closed if preferred
187
+ return request
@@ -8,6 +8,9 @@ MIT License. See Project Root for the license information.
8
8
 
9
9
  from typing import Optional, Dict
10
10
  from pathlib import Path
11
+ import json
12
+ import tempfile
13
+ import shutil
11
14
 
12
15
  import aws_cdk as cdk
13
16
  from aws_cdk import aws_lambda as _lambda
@@ -139,6 +142,34 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
139
142
 
140
143
  logger.info(f"Loading Lambda code from: {code_path}")
141
144
 
145
+ # Create isolated temp directory for this function instance
146
+ # This prevents conflicts when multiple functions use the same handler code
147
+ temp_code_dir = Path(tempfile.mkdtemp(prefix=f"{function_name.replace('/', '-')}-"))
148
+ logger.info(f"Creating isolated code directory at: {temp_code_dir}")
149
+
150
+ # Copy source code to temp directory
151
+ shutil.copytree(code_path, temp_code_dir, dirs_exist_ok=True)
152
+ logger.info(f"Copied code from {code_path} to {temp_code_dir}")
153
+
154
+ # Create runtime configuration file for Lambda@Edge
155
+ # Since Lambda@Edge doesn't support environment variables, we bundle a config file
156
+ runtime_config = {
157
+ 'environment': self.deployment.environment,
158
+ 'function_name': self.edge_config.name,
159
+ 'region': self.deployment.region
160
+ }
161
+
162
+ runtime_config_path = temp_code_dir / 'runtime_config.json'
163
+ logger.info(f"Creating runtime config at: {runtime_config_path}")
164
+
165
+ with open(runtime_config_path, 'w') as f:
166
+ json.dump(runtime_config, f, indent=2)
167
+
168
+ logger.info(f"Runtime config: {runtime_config}")
169
+
170
+ # Use the temp directory for the Lambda code asset
171
+ code_path = temp_code_dir
172
+
142
173
  # Map runtime string to CDK Runtime
143
174
  runtime_map = {
144
175
  "python3.11": _lambda.Runtime.PYTHON_3_11,
@@ -154,10 +185,23 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
154
185
  _lambda.Runtime.PYTHON_3_11
155
186
  )
156
187
 
157
- # Resolve environment variables (handles SSM parameter references)
158
- resolved_environment = self._resolve_environment_variables()
188
+ # Lambda@Edge does NOT support environment variables
189
+ # Configuration must be handled via:
190
+ # 1. Hardcoded in the function code
191
+ # 2. Fetched from SSM Parameter Store at runtime
192
+ # 3. Other configuration mechanisms
193
+
194
+ # Log warning if environment variables are configured
195
+ if self.edge_config.environment:
196
+ logger.warning(
197
+ f"Lambda@Edge function '{function_name}' has environment variables configured, "
198
+ "but Lambda@Edge does not support environment variables. "
199
+ "The function must fetch these values from SSM Parameter Store at runtime."
200
+ )
201
+ for key, value in self.edge_config.environment.items():
202
+ logger.warning(f" - {key}: {value}")
159
203
 
160
- # Create execution role with CloudWatch Logs permissions
204
+ # Create execution role with CloudWatch Logs and SSM permissions
161
205
  execution_role = iam.Role(
162
206
  self,
163
207
  f"{function_name}-Role",
@@ -173,7 +217,23 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
173
217
  ]
174
218
  )
175
219
 
176
- # Create the Lambda function
220
+ # Add SSM read permissions if environment variables reference SSM parameters
221
+ if self.edge_config.environment:
222
+ execution_role.add_to_policy(
223
+ iam.PolicyStatement(
224
+ effect=iam.Effect.ALLOW,
225
+ actions=[
226
+ "ssm:GetParameter",
227
+ "ssm:GetParameters",
228
+ "ssm:GetParametersByPath"
229
+ ],
230
+ resources=[
231
+ f"arn:aws:ssm:*:{cdk.Aws.ACCOUNT_ID}:parameter/*"
232
+ ]
233
+ )
234
+ )
235
+
236
+ # Create the Lambda function WITHOUT environment variables
177
237
  self.function = _lambda.Function(
178
238
  self,
179
239
  function_name,
@@ -185,7 +245,7 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
185
245
  timeout=cdk.Duration.seconds(self.edge_config.timeout),
186
246
  description=self.edge_config.description,
187
247
  role=execution_role,
188
- environment=resolved_environment,
248
+ # Lambda@Edge does NOT support environment variables
189
249
  log_retention=logs.RetentionDays.ONE_WEEK,
190
250
  )
191
251
 
@@ -256,3 +316,30 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
256
316
  param_path,
257
317
  description=f"{key} for Lambda@Edge function {function_name}"
258
318
  )
319
+
320
+ # Export environment variables as SSM parameters
321
+ # Since Lambda@Edge doesn't support environment variables, we export them
322
+ # to SSM so the Lambda function can fetch them at runtime
323
+ if self.edge_config.environment:
324
+ logger.info("Exporting Lambda@Edge environment variables as SSM parameters")
325
+ env_ssm_exports = self.edge_config.dictionary.get("environment_ssm_exports", {})
326
+
327
+ # If no explicit environment_ssm_exports, create default SSM paths
328
+ if not env_ssm_exports:
329
+ # Auto-generate SSM parameter names based on environment variable names
330
+ for env_key in self.edge_config.environment.keys():
331
+ # Use snake_case version of the key for SSM path
332
+ ssm_key = env_key.lower().replace('_', '-')
333
+ env_ssm_exports[env_key] = f"/{self.deployment.environment}/{function_name}/{ssm_key}"
334
+
335
+ # Resolve and export environment variables to SSM
336
+ resolved_env = self._resolve_environment_variables()
337
+ for env_key, ssm_path in env_ssm_exports.items():
338
+ if env_key in resolved_env:
339
+ self.export_ssm_parameter(
340
+ self,
341
+ f"env-{env_key}-param",
342
+ resolved_env[env_key],
343
+ ssm_path,
344
+ description=f"Configuration for Lambda@Edge: {env_key}"
345
+ )
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.12.0"
1
+ __version__ = "0.13.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.12.0
3
+ Version: 0.13.1
4
4
  Summary: CDK Factory. A QuickStarter and best practices setup for CDK projects
5
5
  Author-email: Eric Wilson <eric.wilson@geekcafe.com>
6
6
  License: MIT License
@@ -2,7 +2,7 @@ cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cdk_factory/app.py,sha256=RnX0-pwdTAPAdKJK_j13Zl8anf9zYKBwboR0KA8K8xM,10346
3
3
  cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
4
4
  cdk_factory/cli.py,sha256=FGbCTS5dYCNsfp-etshzvFlGDCjC28r6rtzYbe7KoHI,6407
5
- cdk_factory/version.py,sha256=eHjt9DPsMbptabS2yGx9Yhbyzq5hFSUHXb7zc8Q_8-o,23
5
+ cdk_factory/version.py,sha256=Zg3oo58_HXe_ieb_PwWnYkKGH2zTvu6G2jly-7GnPGo,23
6
6
  cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
7
7
  cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
8
8
  cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
@@ -66,7 +66,7 @@ cdk_factory/interfaces/istack.py,sha256=bhTBs-o9FgKwvJMSuwxjUV6D3nUlvZHVzfm27jP9
66
66
  cdk_factory/interfaces/live_ssm_resolver.py,sha256=3FIr9a02SXqZmbFs3RT0WxczWEQR_CF7QSt7kWbDrVE,8163
67
67
  cdk_factory/interfaces/ssm_parameter_mixin.py,sha256=uA2j8HmAOpuEA9ynRj51s0WjUHMVLsbLQN-QS9NKyHA,12089
68
68
  cdk_factory/lambdas/health_handler.py,sha256=dd40ykKMxWCFEIyp2ZdQvAGNjw_ylI9CSm1N24Hp2ME,196
69
- cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=MPSAOQGYVMSALdH6f9QTXqMSKKJ1tva-yidtaDTbXsg,3241
69
+ cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=NCa16_B66zIczPb9whf_CaspnetuTYKTFpIX6Z7Wqjw,6393
70
70
  cdk_factory/pipeline/path_utils.py,sha256=fvWdrcb4onmpIu1APkHLhXg8zWfK74HcW3Ra2ynxfXM,2586
71
71
  cdk_factory/pipeline/pipeline_factory.py,sha256=rvtkdlTPJG477nTVRN8S2ksWt4bwpd9eVLFd9WO02pM,17248
72
72
  cdk_factory/pipeline/stage.py,sha256=Be7ExMB9A-linRM18IQDOzQ-cP_I2_ThRNzlT4FIrUg,437
@@ -95,7 +95,7 @@ cdk_factory/stack_library/ecr/ecr_stack.py,sha256=1xA68sxFVyqreYjXrP_7U9I8RF9RtF
95
95
  cdk_factory/stack_library/ecs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
96
  cdk_factory/stack_library/ecs/ecs_service_stack.py,sha256=zuGdZEP5KmeVDTJb-H47LYhvs-85-Fi4Xb78nsA-lF4,24685
97
97
  cdk_factory/stack_library/lambda_edge/__init__.py,sha256=ByBJ_CWdc4UtTmFBZH-6pzBMNkjkdtE65AmnB0Fs6lM,156
98
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=KWef9HgwZV6NZGql0cYOAm1hG85PtHFklzlQgcUBuIc,9963
98
+ cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=5MXYXFLVpLX2DNDU9doRfUciyS8h1KM6YdpiiEAkMqY,14141
99
99
  cdk_factory/stack_library/load_balancer/__init__.py,sha256=wZpKw2OecLJGdF5mPayCYAEhu2H3c2gJFFIxwXftGDU,52
100
100
  cdk_factory/stack_library/load_balancer/load_balancer_stack.py,sha256=t5JUe5lMUbQCRFZR08k8nO-g-53yWY8gKB9v8ZnedBs,24391
101
101
  cdk_factory/stack_library/monitoring/__init__.py,sha256=k1G_KDx47Aw0UugaL99PN_TKlyLK4nkJVApCaAK7GJg,153
@@ -129,8 +129,8 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
129
129
  cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
130
130
  cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
131
131
  cdk_factory/workload/workload_factory.py,sha256=mM8GU_5mKq_0OyK060T3JrUSUiGAcKf0eqNlT9mfaws,6028
132
- cdk_factory-0.12.0.dist-info/METADATA,sha256=lR6Hw7lTDtaiBoW2ZjwAe-QbH0QUrEmoWcnOHfp3Q1c,2451
133
- cdk_factory-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- cdk_factory-0.12.0.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
- cdk_factory-0.12.0.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
- cdk_factory-0.12.0.dist-info/RECORD,,
132
+ cdk_factory-0.13.1.dist-info/METADATA,sha256=GC7Guc1LQ_wDzflKXg8VUxUj7ra97GYDF66Ugz3p0eE,2451
133
+ cdk_factory-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
+ cdk_factory-0.13.1.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
+ cdk_factory-0.13.1.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
+ cdk_factory-0.13.1.dist-info/RECORD,,