ac-awssecrets 2.5.6 → 3.0.1

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.
package/index.js CHANGED
@@ -1,44 +1,76 @@
1
1
  const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
2
2
  const { SSMClient, GetParameterCommand, GetParametersCommand, GetParametersByPathCommand } = require("@aws-sdk/client-ssm")
3
3
 
4
- const testConfig = require('./test/config')
5
4
  const functionName = 'ac-awsSecrets'.padEnd(15)
6
5
 
7
6
  /**
8
7
  * Replaces configuration variables with secrets
9
-
10
- KEY is the variable name
11
- NAME is the name of the secret
12
-
13
- OPT
14
- serverName
15
-
16
- MULTISECRETS
17
- Multisecrets -> the secret contains a list of secrets that should be fetched
18
8
  *
19
- * TESTMODES
20
- * 3 -> use secrets from testConfig
9
+ * KEY is the variable name
10
+ * NAME is the name of the secret
11
+ *
12
+ * MULTISECRETS
13
+ * Multisecrets -> the secret contains a list of secrets that should be fetched
21
14
  */
22
15
 
23
16
  const awsSecrets = () => {
24
17
 
25
- const getKey = (obj, key) => key.split('.').reduce((acc, cur) => acc[cur], obj)
18
+ // Helper function to check for unsafe key segments
19
+ const isUnsafeKeySegment = (segment) => (
20
+ segment === '__proto__' ||
21
+ segment === 'constructor' ||
22
+ segment === 'prototype'
23
+ )
24
+
25
+ const getKey = (obj, key) => {
26
+ if (!obj || typeof key !== 'string') {
27
+ return undefined
28
+ }
29
+
30
+ return key.split('.').reduce((acc, cur) => {
31
+ if (isUnsafeKeySegment(cur)) {
32
+ return undefined
33
+ }
34
+ if (acc === undefined || acc === null) {
35
+ return undefined
36
+ }
37
+ return acc[cur]
38
+ }, obj)
39
+ }
40
+
41
+ const setKey = (obj, key, value) => {
42
+ if (!obj || typeof key !== 'string') {
43
+ return
44
+ }
26
45
 
27
- const setKey = (obj, key, value ) => {
28
46
  const [head, ...rest] = key.split('.')
29
- !rest.length
30
- ? obj[head] = value
31
- : setKey(obj[head], rest.join('.'), value)
47
+
48
+ if (isUnsafeKeySegment(head)) {
49
+ throw new Error('Refusing to set unsafe key segment: ' + head)
50
+ }
51
+
52
+ if (!rest.length) {
53
+ obj[head] = value
54
+ }
55
+ else {
56
+ if (obj[head] === undefined || obj[head] === null || typeof obj[head] !== 'object') {
57
+ obj[head] = {}
58
+ }
59
+ setKey(obj[head], rest.join('.'), value)
60
+ }
32
61
  }
33
62
 
34
63
  const deepMerge = (target, source) => {
35
64
  if (Array.isArray(target) && Array.isArray(source)) {
36
65
  return [...new Set([...target, ...source])]
37
66
  }
38
-
67
+
39
68
  if (typeof source === 'object' && source !== null && typeof target === 'object' && target !== null) {
40
69
  const result = { ...target }
41
70
  for (const key in source) {
71
+ if (isUnsafeKeySegment(key)) {
72
+ continue
73
+ }
42
74
  if (key in result) {
43
75
  result[key] = deepMerge(result[key], source[key])
44
76
  }
@@ -48,23 +80,31 @@ const awsSecrets = () => {
48
80
  }
49
81
  return result
50
82
  }
51
-
83
+
52
84
  return source
53
85
  }
54
86
 
55
87
  const setValue = (config, { path, value, array = false, property, merge = false }) => {
56
- // path can be from AWS parametes store (/a/b/c) or a real JSON path (a.b.c)
88
+ // path can be from AWS parameter store (/a/b/c) or a real JSON path (a.b.c)
57
89
  const keys = path.includes('/') ? path.split('/').filter(Boolean) : path.split('.')
58
90
  const lastKey = keys.pop()
91
+
92
+ if (isUnsafeKeySegment(lastKey)) {
93
+ throw new Error('Refusing to set unsafe key segment: ' + lastKey)
94
+ }
95
+
59
96
  let pointer = config
60
-
97
+
61
98
  for (const key of keys) {
99
+ if (isUnsafeKeySegment(key)) {
100
+ throw new Error('Refusing to traverse unsafe key segment: ' + key)
101
+ }
62
102
  if (!pointer[key]) {
63
103
  pointer[key] = {}
64
104
  }
65
105
  pointer = pointer[key]
66
106
  }
67
-
107
+
68
108
  if (array) {
69
109
  if (!Array.isArray(pointer[lastKey])) {
70
110
  pointer[lastKey] = []
@@ -72,331 +112,246 @@ const awsSecrets = () => {
72
112
  if (property) {
73
113
  const [propKey, propValue] = Object.entries(property)[0]
74
114
  const index = pointer[lastKey].findIndex(item => item[propKey] === propValue)
75
-
115
+
76
116
  if (index !== -1) {
77
117
  if (typeof value !== 'object' || Array.isArray(value)) {
78
118
  throw new Error("Value must be an object when replacing an entry in the array.")
79
119
  }
80
120
  // Merge existing properties with new ones
81
121
  pointer[lastKey][index] = { ...pointer[lastKey][index], ...value }
82
- }
122
+ }
83
123
  else {
84
124
  pointer[lastKey].push(value)
85
125
  }
86
- }
126
+ }
87
127
  else {
88
128
  pointer[lastKey].push(value)
89
129
  }
90
- }
130
+ }
91
131
  else {
92
132
  if (merge && typeof pointer[lastKey] === 'object' && !Array.isArray(pointer[lastKey]) && typeof value === 'object' && !Array.isArray(value)) {
93
- pointer[lastKey] = deepMerge(pointer[lastKey], value) // Use deepMerge instead of spread
133
+ pointer[lastKey] = deepMerge(pointer[lastKey], value)
94
134
  }
95
135
  else {
96
136
  pointer[lastKey] = value
97
137
  }
98
138
  }
99
139
  }
100
-
101
140
 
102
- const loadSecretParameters = async({ secretParameters = [], config = {}, testMode = 0, debug = false, throwError = false, region = 'eu-central-1' } = {}) => {
141
+
142
+ const loadSecretParameters = async({ secretParameters = [], config = {}, debug = false, throwError = false, region = 'eu-central-1' } = {}) => {
103
143
  const environment = config?.environment || process.env.NODE_ENV || 'development'
104
-
105
- const awsConfig = {
106
- region
107
- }
108
- const ssmClient = new SSMClient(awsConfig)
109
-
144
+
145
+ const ssmClient = new SSMClient({ region })
146
+
110
147
  // Process parameters in batches of 10 (AWS limit for GetParametersCommand)
111
148
  const processBatchedParameters = async(paramList) => {
112
- // Skip if no parameters
113
149
  if (paramList.length === 0) return
114
-
115
- // Split parameters into batches of 10
150
+
116
151
  const batchSize = 10
117
152
  const batches = []
118
-
153
+
119
154
  for (let i = 0; i < paramList.length; i += batchSize) {
120
155
  batches.push(paramList.slice(i, i + batchSize))
121
156
  }
122
-
123
- // Process each batch
157
+
124
158
  for (const batch of batches) {
125
- if (testMode === 3) {
126
- // For test mode, process individually as before
127
- await Promise.all(batch.map(async param => {
128
- const parameterName = `/${environment}/${param.name}`
129
- const found = testConfig.parameterStore.find(item => item.name === parameterName)
130
- let value = found?.value
131
-
132
- if (param.json && value) {
159
+ try {
160
+ const parameterNames = batch.map(param => `/${environment}/${param.name}`)
161
+
162
+ const command = new GetParametersCommand({
163
+ Names: parameterNames,
164
+ WithDecryption: true
165
+ })
166
+
167
+ const response = await ssmClient.send(command)
168
+ const parameters = response?.Parameters || []
169
+
170
+ await Promise.all(parameters.map(async parameter => {
171
+ const parameterName = parameter.Name
172
+ const paramConfig = batch.find(p => `/${environment}/${p.name}` === parameterName)
173
+
174
+ if (!paramConfig) return
175
+
176
+ let value = parameter.Value
177
+
178
+ if (paramConfig.json && value) {
133
179
  try {
134
180
  value = JSON.parse(value)
135
- }
181
+ }
136
182
  catch (e) {
137
183
  console.error('%s | %s | %s', functionName, parameterName, e?.message)
138
184
  if (throwError) throw e
185
+ return
139
186
  }
140
187
  }
141
-
188
+
142
189
  if (debug) {
143
190
  console.warn('P %s | T %s | V %j', parameterName, typeof value, value)
144
191
  }
145
- setValue(config, {
146
- path: (param.path || param.name),
147
- value,
148
- array: param.array || false,
149
- property: param.property,
150
- merge: param.merge || false
192
+
193
+ setValue(config, {
194
+ path: (paramConfig.path || paramConfig.name),
195
+ value,
196
+ array: paramConfig.array || false,
197
+ property: paramConfig.property,
198
+ merge: paramConfig.merge || false
151
199
  })
152
200
  }))
153
- }
154
- else {
155
- // For production mode, use GetParametersCommand to fetch multiple parameters at once
156
- try {
157
- // Get parameter names for this batch
158
- const parameterNames = batch.map(param => `/${environment}/${param.name}`)
159
-
160
- // Fetch all parameters in this batch with a single API call
161
- const command = new GetParametersCommand({
162
- Names: parameterNames,
163
- WithDecryption: true
164
- })
165
-
166
- const response = await ssmClient.send(command)
167
- const parameters = response?.Parameters || []
168
-
169
- // Process each parameter
170
- await Promise.all(parameters.map(async parameter => {
171
- // Find corresponding parameter config
172
- const paramName = parameter.Name
173
- const paramConfig = batch.find(p => `/${environment}/${p.name}` === paramName)
174
-
175
- if (!paramConfig) return // Skip if no matching config found
176
-
177
- let value = parameter.Value
178
-
179
- if (paramConfig.json && value) {
180
- try {
181
- value = JSON.parse(value)
182
- }
183
- catch (e) {
184
- console.error('%s | %s | %s', functionName, paramName, e?.message)
185
- if (throwError) throw e
186
- return // Skip this parameter if JSON parsing fails
187
- }
188
- }
189
-
190
- if (debug) {
191
- console.warn('P %s | T %s | V %j', paramName, typeof value, value)
192
- }
193
-
194
- setValue(config, {
195
- path: (paramConfig.path || paramConfig.name),
196
- value,
197
- array: paramConfig.array || false,
198
- property: paramConfig.property,
199
- merge: paramConfig.merge || false
200
- })
201
- }))
202
-
203
- // Handle invalid parameters
204
- if (response?.InvalidParameters?.length > 0) {
205
- console.error('%s | Invalid parameters: %j', functionName, response.InvalidParameters)
206
- if (throwError) {
207
- throw new Error(`Invalid parameters: ${response.InvalidParameters.join(', ')}`)
208
- }
201
+
202
+ if (response?.InvalidParameters?.length > 0) {
203
+ console.error('%s | Invalid parameters: %j', functionName, response.InvalidParameters)
204
+ if (throwError) {
205
+ throw new Error(`Invalid parameters: ${response.InvalidParameters.join(', ')}`)
209
206
  }
210
- }
211
- catch (e) {
212
- console.error('%s | Batch parameter fetch error: %s', functionName, e?.message)
213
- if (throwError) throw e
214
-
215
- // Fallback: process parameters individually if batch fails
216
- await Promise.all(batch.map(param => getSecretParameter(param)))
217
207
  }
218
208
  }
209
+ catch (e) {
210
+ // Security errors must always propagate, regardless of throwError flag
211
+ if (e?.message?.includes('unsafe key segment')) throw e
212
+
213
+ console.error('%s | Batch parameter fetch error: %s', functionName, e?.message)
214
+ if (throwError) throw e
215
+
216
+ // Fallback: process parameters individually if batch fails
217
+ await Promise.all(batch.map(param => getSecretParameter(param)))
218
+ }
219
219
  }
220
220
  }
221
-
222
- // Keep the original getSecretParameter as fallback
221
+
222
+ // Fallback for individual parameter fetching
223
223
  const getSecretParameter = async(param) => {
224
224
  const parameterName = `/${environment}/${param.name}`
225
225
  try {
226
- let value
227
- if (testMode === 3) {
228
- // fetch from availableSecrets
229
- let found = testConfig.parameterStore.find(item => item.name === parameterName)
230
- value = found?.value
231
- }
232
- else {
233
- const command = new GetParameterCommand({
234
- Name: parameterName,
235
- WithDecryption: true,
236
- })
237
-
238
- // Send the command to retrieve the parameter
239
- const response = await ssmClient.send(command)
240
- value = response?.Parameter?.Value
241
- }
242
-
243
- // Extract and return the parameter value
226
+ const command = new GetParameterCommand({
227
+ Name: parameterName,
228
+ WithDecryption: true,
229
+ })
230
+
231
+ const response = await ssmClient.send(command)
232
+ let value = response?.Parameter?.Value
233
+
244
234
  if (param.json) {
245
235
  value = JSON.parse(value)
246
236
  }
247
-
237
+
248
238
  if (debug) {
249
239
  console.warn('P %s | T %s | V %j', parameterName, typeof value, value)
250
240
  }
251
241
  setValue(config, { path: (param.path || param.name), value, array: param.array, property: param.property, merge: param.merge })
252
- }
242
+ }
253
243
  catch (e) {
254
244
  console.error('%s | %s | %s', functionName, parameterName, e?.message)
255
245
  if (throwError) throw e
256
246
  }
257
247
  }
258
-
259
- // Keep the original getSecretParametersByPath for wildcard parameters
248
+
249
+ // Fetch all parameters under a path (wildcard support)
260
250
  const getSecretParametersByPath = async({ path, name, json = false, array, property, merge }) => {
261
251
  if (!path) throw new Error('pathMustBeSet')
262
252
  const parameterName = `/${environment}/${name}`
263
253
  try {
264
- let valueArray
265
- if (testMode === 3) {
266
- // fetch from availableSecrets
267
- valueArray = testConfig.parameterStore.filter(item => {
268
- return item.name.startsWith(parameterName.replace('*', ''))
269
- })
270
- valueArray = valueArray.map(item => {
271
- return {
272
- Name: item?.name,
273
- Type: 'SecureString',
274
- Value: item?.value,
275
- Version: 1,
276
- LastModifiedDate: new Date(),
277
- ARN: `arn:aws:ssm:region:account-id:parameter/${item?.name}`,
278
- DataType: 'text'
279
- }
280
- })
281
- }
282
- else {
283
- // fetch all paramters with the path
254
+ let valueArray = []
255
+ let nextToken = undefined
256
+ do {
284
257
  const command = new GetParametersByPathCommand({
285
- Path: parameterName.replace('*', ''),
258
+ Path: parameterName.replaceAll('*', ''),
286
259
  Recursive: true,
287
260
  WithDecryption: true,
261
+ NextToken: nextToken,
288
262
  })
289
263
  const response = await ssmClient.send(command)
290
- valueArray = response?.Parameters
264
+ valueArray.push(...response.Parameters)
265
+ nextToken = response.NextToken
291
266
  }
292
-
267
+ while (nextToken)
268
+
293
269
  for (const item of valueArray) {
294
270
  let value = item?.Value
295
- // Extract and return the parameter value
271
+
296
272
  if (json) {
297
273
  value = JSON.parse(value)
298
274
  }
299
-
275
+
300
276
  if (debug) {
301
277
  console.warn('P %s | T %s | V %j', item?.Name, typeof value, value)
302
278
  }
303
279
  setValue(config, { path, value, array, property, merge })
304
280
  }
305
- }
281
+ }
306
282
  catch (e) {
307
283
  console.error('%s | %s | %s', functionName, parameterName, e?.message)
308
284
  if (throwError) throw e
309
285
  }
310
286
  }
311
-
287
+
312
288
  // Filter out parameters with ignoreInTestMode = true in test environment
313
289
  let filteredParams = secretParameters
314
290
  if (environment === 'test') {
315
291
  filteredParams = secretParameters.filter(param => !param.ignoreInTestMode)
316
292
  }
317
-
318
- // Add debug if needed
293
+
319
294
  if (debug) {
320
295
  filteredParams.forEach(param => param.debug = true)
321
296
  }
322
-
297
+
323
298
  // Split parameters into regular and wildcard ones
324
299
  const wildcardParams = filteredParams.filter(param => param.name.endsWith('*'))
325
300
  const regularParams = filteredParams.filter(param => !param.name.endsWith('*'))
326
-
327
- // Process parameters in parallel
301
+
328
302
  await Promise.all([
329
- // Process regular parameters in batches
330
303
  processBatchedParameters(regularParams),
331
-
332
- // Process wildcard parameters individually (using original method)
333
304
  ...wildcardParams.map(param => getSecretParametersByPath(param))
334
305
  ])
335
306
  }
336
307
 
337
308
 
338
- const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, testMode = 0, debug = false, region = 'eu-central-1' } = {}) => {
309
+ const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, debug = false, region = 'eu-central-1' } = {}) => {
339
310
  const environment = config?.environment || 'development'
340
311
 
341
- const awsConfig = {
342
- region
343
- }
344
- const client = new SecretsManagerClient(awsConfig)
312
+ const client = new SecretsManagerClient({ region })
345
313
 
346
-
347
314
  const getSecret = async({ secret }) => {
348
315
  const secretName = (environment === 'test' ? 'test.' : '') + secret?.name + (secret?.suffix ? '.' + secret?.suffix : '')
349
316
 
350
- // TESTMODE
351
- if (testMode === 3) {
352
- // fetch from availableSecrets
353
- let found = testConfig.availableSecrets.find(item => item.name === secret.name)
354
- secret.value = found?.value
355
- }
356
- else {
357
- const command = new GetSecretValueCommand({
358
- SecretId: secretName
359
- })
360
- try {
361
- const response = await client.send(command)
362
- if (response?.SecretString) {
363
- secret.value = JSON.parse(response?.SecretString)
364
- }
365
- }
366
- catch(e) {
367
- console.error('%s | %s | %s', functionName, secretName, e?.message)
317
+ const command = new GetSecretValueCommand({
318
+ SecretId: secretName
319
+ })
320
+ try {
321
+ const response = await client.send(command)
322
+ if (response?.SecretString) {
323
+ secret.value = JSON.parse(response?.SecretString)
368
324
  }
369
325
  }
326
+ catch(e) {
327
+ console.error('%s | %s | %s', functionName, secretName, e?.message)
328
+ }
370
329
  return secret
371
330
  }
372
331
 
373
332
  const fetchSecrets = async({ secrets }) => {
374
- // filter out secrets with ignoreInTestMode = true
333
+ // Filter out secrets with ignoreInTestMode = true in test environment
375
334
  if (environment === 'test') {
376
335
  secrets = secrets.filter(secret => !secret.ignoreInTestMode)
377
336
  }
378
337
  return Promise.all(secrets.map(secret => getSecret({ secret })))
379
338
  }
380
339
 
381
- // fetch placeholder
340
+ // Fetch multisecrets first and expand them into the secrets list
382
341
  if (multisecrets.length > 0) {
383
- // some keys can have multiple entries (e.g. cloudfrontCOnfigs can have 1 - n entries)
384
- // we have to fetch them first from a secret and add them to the secrets to fetch
385
- let secretsToAdd = await fetchSecrets({ secrets: multisecrets })
386
- // iterate each multisecret and add the values as new secrets
342
+ const secretsToAdd = await fetchSecrets({ secrets: multisecrets })
387
343
  secretsToAdd.forEach(secadd => {
388
- let items = JSON.parse(secadd?.value?.values) || []
344
+ const items = JSON.parse(secadd?.value?.values) || []
389
345
  if (typeof items !== 'object' || items.length < 1) {
390
346
  console.error('%s | %s | MultiSecret has no valid property values', functionName, secadd.name)
391
347
  throw new Error('MultiSecret has no valid property values')
392
348
  }
393
349
  items.forEach(item => {
394
- let p = {
350
+ secrets.push({
395
351
  key: secadd.key,
396
352
  name: item,
397
- type: 'arrayObject' // multisecrets contain multiple secrets that belong to the same config property (which is an array of objects)
398
- }
399
- secrets.push(p)
353
+ type: 'arrayObject'
354
+ })
400
355
  })
401
356
  })
402
357
  }
@@ -405,11 +360,14 @@ const awsSecrets = () => {
405
360
  await fetchSecrets({ secrets })
406
361
  for (const secret of secrets) {
407
362
  let existingValue = getKey(config, secret.key) || {}
408
- let value = secret?.value
363
+ const value = secret?.value
409
364
  if (value) {
410
- // convert values
365
+ // Convert string booleans and JSON values
411
366
  if (typeof value === 'object') {
412
- Object.keys(value).forEach((key) => {
367
+ for (const key of Object.keys(value)) {
368
+ if (isUnsafeKeySegment(key)) {
369
+ continue
370
+ }
413
371
  let val = value[key]
414
372
  if (val === 'true') val = true
415
373
  else if (val === 'false') val = false
@@ -417,32 +375,27 @@ const awsSecrets = () => {
417
375
  try {
418
376
  val = JSON.parse(val.substring(5))
419
377
  }
420
- catch(e) {
378
+ catch {
421
379
  console.error('%s | %s | JSON could not be parsed %j', functionName, secret.name, val)
422
380
  throw new Error('invalidJSON')
423
381
  }
424
382
  }
425
383
  value[key] = val
426
- })
384
+ }
427
385
  }
428
386
 
429
387
  if (secret.servers) {
430
388
  if (typeof secret.servers === 'boolean') {
431
- let servers = existingValue?.servers || []
432
- config[secret.key].servers = servers.map(server => {
389
+ const servers = existingValue?.servers || []
390
+ const updatedServers = servers.map(server => {
433
391
  if (server.server === secret.serverName) {
434
392
  server = { ...server, ...value }
435
393
  }
436
394
  return server
437
395
  })
438
- }
439
- else {
440
- // NEW NOTATION AS OBJECT
441
- /* TODO: Probably not used anywhere, so legacy is ok
442
- let match = {}
443
- _.set(match, _.get(secret.servers, 'identifier'), _.get(secret.servers, 'value'))
444
- existingValue = _.find(_.get(config, key, []), match)
445
- */
396
+ if (!isUnsafeKeySegment('servers')) {
397
+ config[secret.key].servers = updatedServers
398
+ }
446
399
  }
447
400
  }
448
401
  else if (secret?.type === 'arrayObject') {
@@ -462,7 +415,7 @@ const awsSecrets = () => {
462
415
  console.warn('%s | %s | %j', functionName, secret?.name, existingValue)
463
416
  }
464
417
  }
465
- }
418
+ }
466
419
  }
467
420
 
468
421
  return {
package/package.json CHANGED
@@ -3,16 +3,19 @@
3
3
  "author": "Mark Poepping (https://www.admiralcloud.com)",
4
4
  "license": "MIT",
5
5
  "repository": "admiralcloud/ac-awssecrets",
6
- "version": "2.5.6",
6
+ "version": "3.0.1",
7
7
  "dependencies": {
8
- "@aws-sdk/client-secrets-manager": "^3.980.0",
9
- "@aws-sdk/client-ssm": "^3.980.0"
8
+ "@aws-sdk/client-secrets-manager": "^3.997.0",
9
+ "@aws-sdk/client-ssm": "^3.997.0"
10
10
  },
11
11
  "devDependencies": {
12
12
  "ac-semantic-release": "^0.4.10",
13
- "c8": "^10.1.3",
13
+ "aws-sdk-client-mock": "^4.1.0",
14
+ "c8": "^11.0.0",
14
15
  "chai": "^4.x",
15
- "eslint": "9.x",
16
+ "eslint": "10.x",
17
+ "@eslint/js": "^10.0.1",
18
+ "globals": "^17.3.0",
16
19
  "mocha": "^11.7.5"
17
20
  },
18
21
  "scripts": {
@@ -20,7 +23,11 @@
20
23
  "coverage": "./node_modules/c8/bin/c8.js yarn test"
21
24
  },
22
25
  "engines": {
23
- "node": ">=8.0.0"
26
+ "node": ">=20.0.0"
27
+ },
28
+ "resolutions": {
29
+ "mocha/diff": "^8.0.3",
30
+ "minimatch": "^10.2.1"
24
31
  },
25
32
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
26
33
  }