@fjall/components-infrastructure 0.88.1 → 0.88.4
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/dist/lib/app.d.ts +34 -11
- package/dist/lib/app.js +83 -39
- package/dist/lib/aspects/index.d.ts +1 -0
- package/dist/lib/aspects/index.js +6 -0
- package/dist/lib/config/aws/accountAuditRole.d.ts +20 -0
- package/dist/lib/config/aws/accountAuditRole.js +38 -0
- package/dist/lib/config/aws/accountMonitoringRole.d.ts +22 -0
- package/dist/lib/config/aws/accountMonitoringRole.js +133 -0
- package/dist/lib/config/aws/cloudTrail.d.ts +0 -3
- package/dist/lib/config/aws/cloudTrail.js +2 -2
- package/dist/lib/config/aws/disasterRecovery.js +26 -14
- package/dist/lib/config/aws/ecrDefaultImage.js +4 -3
- package/dist/lib/config/aws/identityCenter.d.ts +4 -4
- package/dist/lib/config/aws/identityCenter.js +17 -62
- package/dist/lib/config/aws/identityCenterGroupMembership.js +27 -37
- package/dist/lib/config/aws/index.d.ts +4 -7
- package/dist/lib/config/aws/index.js +5 -8
- package/dist/lib/config/aws/oidcConnector.d.ts +8 -0
- package/dist/lib/config/aws/oidcConnector.js +46 -0
- package/dist/lib/config/aws/platform.d.ts +2 -0
- package/dist/lib/config/aws/platform.js +6 -0
- package/dist/lib/config/index.d.ts +2 -0
- package/dist/lib/config/index.js +21 -0
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets +30 -0
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets.mjs +212 -0
- package/dist/lib/layers/secrets-resolver/bin/resolve-secrets +30 -0
- package/dist/lib/layers/secrets-resolver/bin/resolve-secrets.mjs +212 -0
- package/dist/lib/patterns/aws/account.js +45 -19
- package/dist/lib/patterns/aws/cdn.d.ts +19 -40
- package/dist/lib/patterns/aws/cdn.js +21 -17
- package/dist/lib/patterns/aws/compute.d.ts +6 -7
- package/dist/lib/patterns/aws/compute.js +7 -9
- package/dist/lib/patterns/aws/database.d.ts +9 -89
- package/dist/lib/patterns/aws/database.js +17 -40
- package/dist/lib/patterns/aws/index.d.ts +1 -1
- package/dist/lib/patterns/aws/index.js +2 -2
- package/dist/lib/patterns/aws/interfaces/cdn.d.ts +26 -0
- package/dist/lib/patterns/aws/interfaces/cdn.js +14 -0
- package/dist/lib/patterns/aws/interfaces/connector.d.ts +4 -181
- package/dist/lib/patterns/aws/interfaces/connector.js +16 -113
- package/dist/lib/patterns/aws/interfaces/index.d.ts +1 -0
- package/dist/lib/patterns/aws/interfaces/index.js +5 -2
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +6 -6
- package/dist/lib/patterns/aws/interfaces/pattern.js +1 -1
- package/dist/lib/patterns/aws/network.js +6 -9
- package/dist/lib/patterns/aws/organisation.d.ts +6 -17
- package/dist/lib/patterns/aws/organisation.js +22 -67
- package/dist/lib/patterns/aws/payload.js +11 -12
- package/dist/lib/patterns/aws/storage.d.ts +3 -2
- package/dist/lib/patterns/aws/storage.js +1 -1
- package/dist/lib/resources/aws/audit/auditRole.js +4 -4
- package/dist/lib/resources/aws/audit/index.d.ts +1 -0
- package/dist/lib/resources/aws/audit/index.js +6 -0
- package/dist/lib/resources/aws/backup/backupPlan.js +3 -2
- package/dist/lib/resources/aws/backup/backupVault.js +5 -3
- package/dist/lib/resources/aws/base/awsStack.d.ts +4 -2
- package/dist/lib/resources/aws/base/awsStack.js +8 -2
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +14 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +52 -18
- package/dist/lib/resources/aws/compute/ec2.js +18 -22
- package/dist/lib/resources/aws/compute/ecs.d.ts +9 -8
- package/dist/lib/resources/aws/compute/ecs.js +53 -41
- package/dist/lib/resources/aws/compute/index.d.ts +1 -0
- package/dist/lib/resources/aws/compute/index.js +2 -1
- package/dist/lib/resources/aws/compute/lambda.d.ts +12 -3
- package/dist/lib/resources/aws/compute/lambda.js +48 -36
- package/dist/lib/resources/aws/database/dynamodb.js +3 -13
- package/dist/lib/resources/aws/database/index.d.ts +8 -2
- package/dist/lib/resources/aws/database/index.js +19 -3
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +2 -3
- package/dist/lib/resources/aws/database/rdsAurora.js +33 -69
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +6 -6
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +25 -29
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +11 -0
- package/dist/lib/resources/aws/database/rdsDefaults.js +15 -0
- package/dist/lib/resources/aws/database/rdsHelpers.d.ts +39 -0
- package/dist/lib/resources/aws/database/rdsHelpers.js +75 -0
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +7 -8
- package/dist/lib/resources/aws/database/rdsInstance.js +40 -84
- package/dist/lib/resources/aws/database/rdsProxyOutput.d.ts +7 -0
- package/dist/lib/resources/aws/database/rdsProxyOutput.js +18 -0
- package/dist/lib/resources/aws/iam/identityCenter/assignment.d.ts +0 -2
- package/dist/lib/resources/aws/iam/identityCenter/assignment.js +9 -45
- package/dist/lib/resources/aws/iam/identityCenter/group.d.ts +1 -3
- package/dist/lib/resources/aws/iam/identityCenter/group.js +7 -82
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.d.ts +1 -3
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +9 -93
- package/dist/lib/resources/aws/iam/index.d.ts +0 -1
- package/dist/lib/resources/aws/iam/index.js +1 -2
- package/dist/lib/resources/aws/index.d.ts +0 -1
- package/dist/lib/resources/aws/index.js +1 -2
- package/dist/lib/resources/aws/logging/cloudTrail.js +13 -3
- package/dist/lib/resources/aws/logging/index.d.ts +2 -0
- package/dist/lib/resources/aws/logging/index.js +19 -0
- package/dist/lib/resources/aws/messaging/index.d.ts +3 -2
- package/dist/lib/resources/aws/messaging/index.js +4 -3
- package/dist/lib/resources/aws/messaging/sqs.js +14 -11
- package/dist/lib/resources/aws/messaging/utils.d.ts +1 -2
- package/dist/lib/resources/aws/messaging/utils.js +3 -4
- package/dist/lib/resources/aws/monitoring/index.d.ts +0 -1
- package/dist/lib/resources/aws/monitoring/index.js +4 -17
- package/dist/lib/resources/aws/networking/hostedZone.d.ts +28 -0
- package/dist/lib/resources/aws/networking/hostedZone.js +153 -0
- package/dist/lib/resources/aws/networking/index.d.ts +2 -0
- package/dist/lib/resources/aws/networking/index.js +3 -1
- package/dist/lib/resources/aws/networking/ipamPool.js +110 -31
- package/dist/lib/resources/aws/networking/securityGroup.d.ts +5 -0
- package/dist/lib/resources/aws/networking/securityGroup.js +14 -0
- package/dist/lib/resources/aws/networking/vpc.js +9 -4
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.d.ts +17 -0
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +66 -0
- package/dist/lib/resources/aws/organisation/index.d.ts +1 -0
- package/dist/lib/resources/aws/organisation/index.js +4 -2
- package/dist/lib/resources/aws/secrets/index.d.ts +0 -1
- package/dist/lib/resources/aws/secrets/index.js +1 -2
- package/dist/lib/resources/aws/secrets/parameter.js +5 -3
- package/dist/lib/resources/aws/storage/ecr.d.ts +0 -1
- package/dist/lib/resources/aws/storage/ecr.js +5 -7
- package/dist/lib/resources/aws/storage/s3.d.ts +3 -3
- package/dist/lib/resources/aws/storage/s3.js +1 -1
- package/dist/lib/resources/aws/utilities/index.d.ts +5 -0
- package/dist/lib/resources/aws/utilities/index.js +22 -0
- package/dist/lib/utils/backupTierMapping.d.ts +11 -0
- package/dist/lib/utils/backupTierMapping.js +17 -0
- package/dist/lib/utils/capitaliseString.d.ts +6 -0
- package/dist/lib/utils/capitaliseString.js +10 -1
- package/dist/lib/utils/connections.d.ts +46 -0
- package/dist/lib/utils/connections.js +159 -0
- package/dist/lib/utils/connector.d.ts +183 -0
- package/dist/lib/utils/connector.js +117 -0
- package/dist/lib/utils/databaseTypes.d.ts +85 -0
- package/dist/lib/utils/databaseTypes.js +34 -0
- package/dist/lib/utils/env.d.ts +42 -0
- package/dist/lib/utils/env.js +128 -0
- package/dist/lib/utils/getConfig.d.ts +0 -2
- package/dist/lib/utils/getConfig.js +1 -4
- package/dist/lib/utils/index.d.ts +6 -0
- package/dist/lib/utils/index.js +7 -1
- package/dist/lib/utils/removalPolicy.d.ts +2 -0
- package/dist/lib/utils/removalPolicy.js +16 -0
- package/dist/lib/utils/resourceNaming.js +4 -7
- package/dist/lib/utils/standardTagsAspect.d.ts +4 -0
- package/dist/lib/utils/standardTagsAspect.js +8 -8
- package/dist/lib/utils/vpcUtils.d.ts +14 -0
- package/dist/lib/utils/vpcUtils.js +28 -0
- package/package.json +8 -8
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fjall Secrets Resolver — runs before Lambda handler via AWS_LAMBDA_EXEC_WRAPPER.
|
|
3
|
+
*
|
|
4
|
+
* Resolves secrets from two sources using the AWS Parameters and Secrets Extension
|
|
5
|
+
* HTTP cache at localhost:2773:
|
|
6
|
+
*
|
|
7
|
+
* 1. SSM Parameter Store (user-managed secrets via `fjall secrets set`)
|
|
8
|
+
* Reads SSM_SECRETS_PATH + SSM_SECRET_NAMES, fetches each parameter,
|
|
9
|
+
* exports as environment variables.
|
|
10
|
+
*
|
|
11
|
+
* 2. Secrets Manager (CDK-managed secrets, e.g. database credentials)
|
|
12
|
+
* Scans for *_SECRET_ARN env vars, fetches each secret,
|
|
13
|
+
* optionally extracts a JSON field via the matching *_SECRET_FIELD var,
|
|
14
|
+
* exports as the prefix (e.g. DATABASE_PASSWORD_SECRET_ARN → DATABASE_PASSWORD).
|
|
15
|
+
*
|
|
16
|
+
* Outputs `export KEY='value'` lines to stdout. The bash wrapper evals this output
|
|
17
|
+
* and then execs into the Lambda runtime.
|
|
18
|
+
*
|
|
19
|
+
* The Extension may not be ready immediately during INIT phase, so all HTTP
|
|
20
|
+
* calls use a retry loop with exponential backoff.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EXTENSION_PORT = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT || "2773";
|
|
24
|
+
const EXTENSION_URL = `http://localhost:${EXTENSION_PORT}`;
|
|
25
|
+
const MAX_RETRIES = 5;
|
|
26
|
+
const INITIAL_DELAY_MS = 100;
|
|
27
|
+
/** Per-request timeout — generous for localhost; total budget bounded by Lambda INIT timeout */
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 2000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch from the Extension HTTP API with retry and exponential backoff.
|
|
32
|
+
* The Extension starts during INIT phase 1 (Extension init) and the wrapper
|
|
33
|
+
* runs during INIT phase 2 (Runtime init), so it is usually ready — but
|
|
34
|
+
* a retry loop handles the timing edge case.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchWithRetry(path) {
|
|
37
|
+
let delay = INITIAL_DELAY_MS;
|
|
38
|
+
let lastError;
|
|
39
|
+
|
|
40
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`${EXTENSION_URL}${path}`, {
|
|
43
|
+
headers: {
|
|
44
|
+
"X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN,
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return await response.json();
|
|
50
|
+
}
|
|
51
|
+
// 4xx errors (e.g. secret not found) — don't retry
|
|
52
|
+
if (response.status >= 400 && response.status < 500) {
|
|
53
|
+
const body = await response.text().catch(() => "");
|
|
54
|
+
const err = new Error(`Extension returned ${response.status}: ${body}`);
|
|
55
|
+
err.nonRetriable = true;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
// 5xx — retriable
|
|
59
|
+
const body = await response.text().catch(() => "");
|
|
60
|
+
lastError = new Error(`Extension returned ${response.status}: ${body}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err.nonRetriable) {
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
lastError = err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Retry with backoff (both 5xx and network errors)
|
|
69
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
71
|
+
delay *= 2;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Failed to reach Extension after ${MAX_RETRIES} attempts: ${lastError?.message}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Escape a value for safe inclusion in a shell `export KEY='value'` statement.
|
|
82
|
+
* Single-quotes are the safest quoting mechanism — only embedded single-quotes
|
|
83
|
+
* need escaping via the close-reopen pattern: ' → '\''
|
|
84
|
+
*/
|
|
85
|
+
function shellEscape(value) {
|
|
86
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// POSIX env var names: letters, digits, underscores; must not start with a digit
|
|
90
|
+
const VALID_ENV_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
91
|
+
|
|
92
|
+
function assertValidEnvName(name, source) {
|
|
93
|
+
if (!VALID_ENV_NAME.test(name)) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
`[fjall-resolver] Invalid env var name '${name}' from ${source}. ` +
|
|
96
|
+
`Names must match [a-zA-Z_][a-zA-Z0-9_]* (no dots or hyphens).\n`,
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve SSM Parameter Store secrets.
|
|
104
|
+
* Reads SSM_SECRETS_PATH and SSM_SECRET_NAMES, fetches each SecureString parameter.
|
|
105
|
+
*/
|
|
106
|
+
async function resolveSsmSecrets() {
|
|
107
|
+
const basePath = process.env.SSM_SECRETS_PATH;
|
|
108
|
+
const secretNames = process.env.SSM_SECRET_NAMES;
|
|
109
|
+
|
|
110
|
+
if (!basePath || !secretNames) return [];
|
|
111
|
+
|
|
112
|
+
const names = secretNames.split(",").filter(Boolean);
|
|
113
|
+
const exports = [];
|
|
114
|
+
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
assertValidEnvName(name, "SSM_SECRET_NAMES");
|
|
117
|
+
const paramPath = `${basePath}/${name}`;
|
|
118
|
+
const encodedPath = encodeURIComponent(paramPath);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const data = await fetchWithRetry(
|
|
122
|
+
`/systemsmanager/parameters/get?name=${encodedPath}&withDecryption=true`,
|
|
123
|
+
);
|
|
124
|
+
const value = data?.Parameter?.Value;
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
exports.push(`export ${name}=${shellEscape(value)}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`[fjall-resolver] Failed to resolve SSM parameter ${paramPath}: ${msg}\n`,
|
|
132
|
+
);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return exports;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve Secrets Manager secrets.
|
|
142
|
+
* Scans for *_SECRET_ARN env vars, fetches each secret from SM,
|
|
143
|
+
* optionally extracts a JSON field, and exports as the prefix.
|
|
144
|
+
*/
|
|
145
|
+
async function resolveSecretsManagerSecrets() {
|
|
146
|
+
const exports = [];
|
|
147
|
+
|
|
148
|
+
for (const [key, arn] of Object.entries(process.env)) {
|
|
149
|
+
if (!key.endsWith("_SECRET_ARN") || !arn) continue;
|
|
150
|
+
|
|
151
|
+
const prefix = key.slice(0, -"_SECRET_ARN".length);
|
|
152
|
+
assertValidEnvName(prefix, "Secrets Manager");
|
|
153
|
+
const fieldKey = `${prefix}_SECRET_FIELD`;
|
|
154
|
+
const field = process.env[fieldKey];
|
|
155
|
+
|
|
156
|
+
const encodedArn = encodeURIComponent(arn);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const data = await fetchWithRetry(
|
|
160
|
+
`/secretsmanager/get?secretId=${encodedArn}`,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let value;
|
|
164
|
+
if (field && data?.SecretString) {
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(data.SecretString);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Secret is not valid JSON but field '${field}' was requested. Store the secret as a JSON object.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
value = parsed[field];
|
|
174
|
+
if (value === undefined) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Field '${field}' not found in secret (${Object.keys(parsed).length} fields present).`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
value = data?.SecretString;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (value !== undefined && value !== null) {
|
|
184
|
+
exports.push(`export ${prefix}=${shellEscape(String(value))}`);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`[fjall-resolver] Failed to resolve SM secret ${prefix} (${arn}): ${msg}\n`,
|
|
190
|
+
);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return exports;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function main() {
|
|
199
|
+
const ssmExports = await resolveSsmSecrets();
|
|
200
|
+
const smExports = await resolveSecretsManagerSecrets();
|
|
201
|
+
const allExports = [...ssmExports, ...smExports];
|
|
202
|
+
|
|
203
|
+
if (allExports.length > 0) {
|
|
204
|
+
process.stdout.write(allExports.join("\n") + "\n");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main().catch((err) => {
|
|
209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
210
|
+
process.stderr.write(`[fjall-resolver] Fatal error: ${msg}\n`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Fjall Secrets Resolver Wrapper
|
|
3
|
+
#
|
|
4
|
+
# Invoked by AWS_LAMBDA_EXEC_WRAPPER before the Lambda runtime starts.
|
|
5
|
+
# Calls the Node.js resolver to fetch secrets from the AWS Parameters and
|
|
6
|
+
# Secrets Extension, then execs into the original runtime bootstrap.
|
|
7
|
+
#
|
|
8
|
+
# The resolver outputs `export KEY='value'` lines which we eval to inject
|
|
9
|
+
# secrets as environment variables visible to the handler.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
# Only run resolver if secrets are configured
|
|
14
|
+
if [ -n "${SSM_SECRET_NAMES:-}" ] || env | grep -q '_SECRET_ARN='; then
|
|
15
|
+
RESOLVER_OUTPUT=$(/var/lang/bin/node /opt/bin/resolve-secrets.mjs)
|
|
16
|
+
if [ -n "$RESOLVER_OUTPUT" ]; then
|
|
17
|
+
# Validate each line matches `export NAME='...'` before eval to prevent
|
|
18
|
+
# accidental code execution if the resolver ever emits unexpected output
|
|
19
|
+
while IFS= read -r line; do
|
|
20
|
+
if [[ "$line" =~ ^export\ [a-zA-Z_][a-zA-Z0-9_]*= ]]; then
|
|
21
|
+
eval "$line"
|
|
22
|
+
else
|
|
23
|
+
echo "[fjall-resolver] Unexpected output from resolver: ${line:0:80}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
done <<< "$RESOLVER_OUTPUT"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
exec "$@"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fjall Secrets Resolver — runs before Lambda handler via AWS_LAMBDA_EXEC_WRAPPER.
|
|
3
|
+
*
|
|
4
|
+
* Resolves secrets from two sources using the AWS Parameters and Secrets Extension
|
|
5
|
+
* HTTP cache at localhost:2773:
|
|
6
|
+
*
|
|
7
|
+
* 1. SSM Parameter Store (user-managed secrets via `fjall secrets set`)
|
|
8
|
+
* Reads SSM_SECRETS_PATH + SSM_SECRET_NAMES, fetches each parameter,
|
|
9
|
+
* exports as environment variables.
|
|
10
|
+
*
|
|
11
|
+
* 2. Secrets Manager (CDK-managed secrets, e.g. database credentials)
|
|
12
|
+
* Scans for *_SECRET_ARN env vars, fetches each secret,
|
|
13
|
+
* optionally extracts a JSON field via the matching *_SECRET_FIELD var,
|
|
14
|
+
* exports as the prefix (e.g. DATABASE_PASSWORD_SECRET_ARN → DATABASE_PASSWORD).
|
|
15
|
+
*
|
|
16
|
+
* Outputs `export KEY='value'` lines to stdout. The bash wrapper evals this output
|
|
17
|
+
* and then execs into the Lambda runtime.
|
|
18
|
+
*
|
|
19
|
+
* The Extension may not be ready immediately during INIT phase, so all HTTP
|
|
20
|
+
* calls use a retry loop with exponential backoff.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EXTENSION_PORT = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT || "2773";
|
|
24
|
+
const EXTENSION_URL = `http://localhost:${EXTENSION_PORT}`;
|
|
25
|
+
const MAX_RETRIES = 5;
|
|
26
|
+
const INITIAL_DELAY_MS = 100;
|
|
27
|
+
/** Per-request timeout — generous for localhost; total budget bounded by Lambda INIT timeout */
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 2000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch from the Extension HTTP API with retry and exponential backoff.
|
|
32
|
+
* The Extension starts during INIT phase 1 (Extension init) and the wrapper
|
|
33
|
+
* runs during INIT phase 2 (Runtime init), so it is usually ready — but
|
|
34
|
+
* a retry loop handles the timing edge case.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchWithRetry(path) {
|
|
37
|
+
let delay = INITIAL_DELAY_MS;
|
|
38
|
+
let lastError;
|
|
39
|
+
|
|
40
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`${EXTENSION_URL}${path}`, {
|
|
43
|
+
headers: {
|
|
44
|
+
"X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN,
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return await response.json();
|
|
50
|
+
}
|
|
51
|
+
// 4xx errors (e.g. secret not found) — don't retry
|
|
52
|
+
if (response.status >= 400 && response.status < 500) {
|
|
53
|
+
const body = await response.text().catch(() => "");
|
|
54
|
+
const err = new Error(`Extension returned ${response.status}: ${body}`);
|
|
55
|
+
err.nonRetriable = true;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
// 5xx — retriable
|
|
59
|
+
const body = await response.text().catch(() => "");
|
|
60
|
+
lastError = new Error(`Extension returned ${response.status}: ${body}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err.nonRetriable) {
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
lastError = err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Retry with backoff (both 5xx and network errors)
|
|
69
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
71
|
+
delay *= 2;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Failed to reach Extension after ${MAX_RETRIES} attempts: ${lastError?.message}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Escape a value for safe inclusion in a shell `export KEY='value'` statement.
|
|
82
|
+
* Single-quotes are the safest quoting mechanism — only embedded single-quotes
|
|
83
|
+
* need escaping via the close-reopen pattern: ' → '\''
|
|
84
|
+
*/
|
|
85
|
+
function shellEscape(value) {
|
|
86
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// POSIX env var names: letters, digits, underscores; must not start with a digit
|
|
90
|
+
const VALID_ENV_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
91
|
+
|
|
92
|
+
function assertValidEnvName(name, source) {
|
|
93
|
+
if (!VALID_ENV_NAME.test(name)) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
`[fjall-resolver] Invalid env var name '${name}' from ${source}. ` +
|
|
96
|
+
`Names must match [a-zA-Z_][a-zA-Z0-9_]* (no dots or hyphens).\n`,
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve SSM Parameter Store secrets.
|
|
104
|
+
* Reads SSM_SECRETS_PATH and SSM_SECRET_NAMES, fetches each SecureString parameter.
|
|
105
|
+
*/
|
|
106
|
+
async function resolveSsmSecrets() {
|
|
107
|
+
const basePath = process.env.SSM_SECRETS_PATH;
|
|
108
|
+
const secretNames = process.env.SSM_SECRET_NAMES;
|
|
109
|
+
|
|
110
|
+
if (!basePath || !secretNames) return [];
|
|
111
|
+
|
|
112
|
+
const names = secretNames.split(",").filter(Boolean);
|
|
113
|
+
const exports = [];
|
|
114
|
+
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
assertValidEnvName(name, "SSM_SECRET_NAMES");
|
|
117
|
+
const paramPath = `${basePath}/${name}`;
|
|
118
|
+
const encodedPath = encodeURIComponent(paramPath);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const data = await fetchWithRetry(
|
|
122
|
+
`/systemsmanager/parameters/get?name=${encodedPath}&withDecryption=true`,
|
|
123
|
+
);
|
|
124
|
+
const value = data?.Parameter?.Value;
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
exports.push(`export ${name}=${shellEscape(value)}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`[fjall-resolver] Failed to resolve SSM parameter ${paramPath}: ${msg}\n`,
|
|
132
|
+
);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return exports;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve Secrets Manager secrets.
|
|
142
|
+
* Scans for *_SECRET_ARN env vars, fetches each secret from SM,
|
|
143
|
+
* optionally extracts a JSON field, and exports as the prefix.
|
|
144
|
+
*/
|
|
145
|
+
async function resolveSecretsManagerSecrets() {
|
|
146
|
+
const exports = [];
|
|
147
|
+
|
|
148
|
+
for (const [key, arn] of Object.entries(process.env)) {
|
|
149
|
+
if (!key.endsWith("_SECRET_ARN") || !arn) continue;
|
|
150
|
+
|
|
151
|
+
const prefix = key.slice(0, -"_SECRET_ARN".length);
|
|
152
|
+
assertValidEnvName(prefix, "Secrets Manager");
|
|
153
|
+
const fieldKey = `${prefix}_SECRET_FIELD`;
|
|
154
|
+
const field = process.env[fieldKey];
|
|
155
|
+
|
|
156
|
+
const encodedArn = encodeURIComponent(arn);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const data = await fetchWithRetry(
|
|
160
|
+
`/secretsmanager/get?secretId=${encodedArn}`,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let value;
|
|
164
|
+
if (field && data?.SecretString) {
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(data.SecretString);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Secret is not valid JSON but field '${field}' was requested. Store the secret as a JSON object.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
value = parsed[field];
|
|
174
|
+
if (value === undefined) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Field '${field}' not found in secret (${Object.keys(parsed).length} fields present).`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
value = data?.SecretString;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (value !== undefined && value !== null) {
|
|
184
|
+
exports.push(`export ${prefix}=${shellEscape(String(value))}`);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`[fjall-resolver] Failed to resolve SM secret ${prefix} (${arn}): ${msg}\n`,
|
|
190
|
+
);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return exports;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function main() {
|
|
199
|
+
const ssmExports = await resolveSsmSecrets();
|
|
200
|
+
const smExports = await resolveSecretsManagerSecrets();
|
|
201
|
+
const allExports = [...ssmExports, ...smExports];
|
|
202
|
+
|
|
203
|
+
if (allExports.length > 0) {
|
|
204
|
+
process.stdout.write(allExports.join("\n") + "\n");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main().catch((err) => {
|
|
209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
210
|
+
process.stderr.write(`[fjall-resolver] Fatal error: ${msg}\n`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
});
|
|
@@ -4,6 +4,9 @@ exports.Account = void 0;
|
|
|
4
4
|
const aws_cdk_lib_1 = require("aws-cdk-lib");
|
|
5
5
|
const aws_1 = require("../../config/aws");
|
|
6
6
|
const cloudTrail_1 = require("../../config/aws/cloudTrail");
|
|
7
|
+
const oidcConnector_1 = require("../../config/aws/oidcConnector");
|
|
8
|
+
const accountMonitoringRole_1 = require("../../config/aws/accountMonitoringRole");
|
|
9
|
+
const accountAuditRole_1 = require("../../config/aws/accountAuditRole");
|
|
7
10
|
const getConfig_1 = require("../../utils/getConfig");
|
|
8
11
|
const disasterRecovery_1 = require("../../config/aws/disasterRecovery");
|
|
9
12
|
class Account extends aws_cdk_lib_1.Stack {
|
|
@@ -17,35 +20,58 @@ class Account extends aws_cdk_lib_1.Stack {
|
|
|
17
20
|
super(scope, id, props);
|
|
18
21
|
this.organisationType = "account";
|
|
19
22
|
this.resolvedRegion = region;
|
|
20
|
-
const
|
|
21
|
-
|
|
23
|
+
const orgId = this.node.tryGetContext("orgId");
|
|
24
|
+
if (orgId) {
|
|
25
|
+
new aws_cdk_lib_1.CfnOutput(this, "OrganisationIdOutput", {
|
|
26
|
+
key: "OrganisationId",
|
|
27
|
+
value: orgId,
|
|
28
|
+
exportName: "OrganisationId"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
new aws_cdk_lib_1.CfnOutput(this, "AccountIdOutput", {
|
|
32
|
+
key: "AccountId",
|
|
33
|
+
value: this.account,
|
|
34
|
+
exportName: "AccountId",
|
|
35
|
+
description: "AWS Account ID for this account"
|
|
36
|
+
});
|
|
22
37
|
const eventBus = new aws_1.DefaultEventBus(this, "EventBus");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const ipamPoolId = this.node.tryGetContext("ipamPoolId");
|
|
39
|
+
if (id === "Account" && ipamPoolId) {
|
|
40
|
+
const regionSuffix = region.replace(/-/g, "");
|
|
41
|
+
new aws_cdk_lib_1.CfnOutput(this, "IpamPoolIdOutput", {
|
|
42
|
+
key: `IpamPoolId${accountId}${regionSuffix}`,
|
|
43
|
+
value: ipamPoolId,
|
|
44
|
+
exportName: `IpamPoolId${accountId}${regionSuffix}`
|
|
28
45
|
});
|
|
29
46
|
}
|
|
47
|
+
const fjallOrgId = this.node.tryGetContext("fjallOrgId");
|
|
48
|
+
if (id === "Account" && fjallOrgId) {
|
|
49
|
+
new oidcConnector_1.OidcConnector(this, "OidcConnector", { fjallOrgId });
|
|
50
|
+
}
|
|
51
|
+
// Per-account monitoring role (unconditional; ExternalId added when orgId known)
|
|
52
|
+
new accountMonitoringRole_1.AccountMonitoringRole(this, "MonitoringRole", fjallOrgId ? { fjallOrgId } : undefined);
|
|
53
|
+
// Per-account audit role (conditional on fjallOrgId)
|
|
54
|
+
if (fjallOrgId) {
|
|
55
|
+
new accountAuditRole_1.AccountAuditRole(this, "AuditRole", { fjallOrgId });
|
|
56
|
+
}
|
|
30
57
|
new cloudTrail_1.ManagementEventsTrail(this, "CloudTrail", {
|
|
31
|
-
accountId: account
|
|
58
|
+
accountId: this.account,
|
|
32
59
|
region
|
|
33
60
|
});
|
|
34
61
|
new aws_1.EcrDefaultImage(this, "EcrDefaultImage", {
|
|
35
62
|
region,
|
|
36
|
-
accountId: account
|
|
63
|
+
accountId: this.account,
|
|
37
64
|
eventBusArn: eventBus.defaultEventBusArn.value
|
|
38
65
|
});
|
|
39
66
|
const environment = config.environment || "unknown";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
67
|
+
if (config.disasterRecoveryRegion) {
|
|
68
|
+
const isComplianceAccount = environment === "compliance";
|
|
69
|
+
if (environment === "production" || isComplianceAccount) {
|
|
70
|
+
new disasterRecovery_1.DisasterRecovery(this, "DisasterRecovery", {
|
|
71
|
+
region,
|
|
72
|
+
accountId
|
|
73
|
+
});
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
76
|
new aws_cdk_lib_1.CfnOutput(this, "Environment", {
|
|
51
77
|
key: "Environment",
|
|
@@ -56,4 +82,4 @@ class Account extends aws_cdk_lib_1.Stack {
|
|
|
56
82
|
}
|
|
57
83
|
}
|
|
58
84
|
exports.Account = Account;
|
|
59
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
85
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWNjb3VudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL2xpYi9wYXR0ZXJucy9hd3MvYWNjb3VudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBZ0U7QUFDaEUsMENBQW9FO0FBRXBFLDREQUFvRTtBQUNwRSxrRUFBK0Q7QUFDL0Qsa0ZBQStFO0FBQy9FLHdFQUFxRTtBQUNyRSxxREFBa0Q7QUFDbEQsd0VBQXFFO0FBUXJFLE1BQWEsT0FBUSxTQUFRLG1CQUFLO0lBSWhDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBbUI7UUFDM0QsTUFBTSxNQUFNLEdBQUcsSUFBQSxxQkFBUyxHQUFFLENBQUM7UUFDM0IsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLFNBQVMsSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDO1FBQ3RELE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxNQUFNLElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUU3QyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDZixNQUFNLElBQUksS0FBSyxDQUNiLG9HQUFvRyxDQUNyRyxDQUFDO1FBQ0osQ0FBQztRQUVELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBZFYscUJBQWdCLEdBQXFCLFNBQVMsQ0FBQztRQWdCN0QsSUFBSSxDQUFDLGNBQWMsR0FBRyxNQUFNLENBQUM7UUFFN0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDL0MsSUFBSSxLQUFLLEVBQUUsQ0FBQztZQUNWLElBQUksdUJBQVMsQ0FBQyxJQUFJLEVBQUUsc0JBQXNCLEVBQUU7Z0JBQzFDLEdBQUcsRUFBRSxnQkFBZ0I7Z0JBQ3JCLEtBQUssRUFBRSxLQUFLO2dCQUNaLFVBQVUsRUFBRSxnQkFBZ0I7YUFDN0IsQ0FBQyxDQUFDO1FBQ0wsQ0FBQztRQUVELElBQUksdUJBQVMsQ0FBQyxJQUFJLEVBQUUsaUJBQWlCLEVBQUU7WUFDckMsR0FBRyxFQUFFLFdBQVc7WUFDaEIsS0FBSyxFQUFFLElBQUksQ0FBQyxPQUFPO1lBQ25CLFVBQVUsRUFBRSxXQUFXO1lBQ3ZCLFdBQVcsRUFBRSxpQ0FBaUM7U0FDL0MsQ0FBQyxDQUFDO1FBRUgsTUFBTSxRQUFRLEdBQUcsSUFBSSxxQkFBZSxDQUFDLElBQUksRUFBRSxVQUFVLENBQUMsQ0FBQztRQUV2RCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUN6RCxJQUFJLEVBQUUsS0FBSyxTQUFTLElBQUksVUFBVSxFQUFFLENBQUM7WUFDbkMsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDOUMsSUFBSSx1QkFBUyxDQUFDLElBQUksRUFBRSxrQkFBa0IsRUFBRTtnQkFDdEMsR0FBRyxFQUFFLGFBQWEsU0FBUyxHQUFHLFlBQVksRUFBRTtnQkFDNUMsS0FBSyxFQUFFLFVBQVU7Z0JBQ2pCLFVBQVUsRUFBRSxhQUFhLFNBQVMsR0FBRyxZQUFZLEVBQUU7YUFDcEQsQ0FBQyxDQUFDO1FBQ0wsQ0FBQztRQUVELE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLFlBQVksQ0FBQyxDQUFDO1FBQ3pELElBQUksRUFBRSxLQUFLLFNBQVMsSUFBSSxVQUFVLEVBQUUsQ0FBQztZQUNuQyxJQUFJLDZCQUFhLENBQUMsSUFBSSxFQUFFLGVBQWUsRUFBRSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDM0QsQ0FBQztRQUVELGlGQUFpRjtRQUNqRixJQUFJLDZDQUFxQixDQUN2QixJQUFJLEVBQ0osZ0JBQWdCLEVBQ2hCLFVBQVUsQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUN4QyxDQUFDO1FBRUYscURBQXFEO1FBQ3JELElBQUksVUFBVSxFQUFFLENBQUM7WUFDZixJQUFJLG1DQUFnQixDQUFDLElBQUksRUFBRSxXQUFXLEVBQUUsRUFBRSxVQUFVLEVBQUUsQ0FBQyxDQUFDO1FBQzFELENBQUM7UUFFRCxJQUFJLGtDQUFxQixDQUFDLElBQUksRUFBRSxZQUFZLEVBQUU7WUFDNUMsU0FBUyxFQUFFLElBQUksQ0FBQyxPQUFPO1lBQ3ZCLE1BQU07U0FDUCxDQUFDLENBQUM7UUFFSCxJQUFJLHFCQUFlLENBQUMsSUFBSSxFQUFFLGlCQUFpQixFQUFFO1lBQzNDLE1BQU07WUFDTixTQUFTLEVBQUUsSUFBSSxDQUFDLE9BQU87WUFDdkIsV0FBVyxFQUFFLFFBQVEsQ0FBQyxrQkFBa0IsQ0FBQyxLQUFLO1NBQy9DLENBQUMsQ0FBQztRQUVILE1BQU0sV0FBVyxHQUFHLE1BQU0sQ0FBQyxXQUFXLElBQUksU0FBUyxDQUFDO1FBRXBELElBQUksTUFBTSxDQUFDLHNCQUFzQixFQUFFLENBQUM7WUFDbEMsTUFBTSxtQkFBbUIsR0FBRyxXQUFXLEtBQUssWUFBWSxDQUFDO1lBRXpELElBQUksV0FBVyxLQUFLLFlBQVksSUFBSSxtQkFBbUIsRUFBRSxDQUFDO2dCQUN4RCxJQUFJLG1DQUFnQixDQUFDLElBQUksRUFBRSxrQkFBa0IsRUFBRTtvQkFDN0MsTUFBTTtvQkFDTixTQUFTO2lCQUNWLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO1FBRUQsSUFBSSx1QkFBUyxDQUFDLElBQUksRUFBRSxhQUFhLEVBQUU7WUFDakMsR0FBRyxFQUFFLGFBQWE7WUFDbEIsS0FBSyxFQUFFLFdBQVc7WUFDbEIsVUFBVSxFQUFFLGFBQWE7WUFDekIsV0FBVyxFQUNULDRFQUE0RTtTQUMvRSxDQUFDLENBQUM7SUFDTCxDQUFDO0NBQ0Y7QUFoR0QsMEJBZ0dDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ2ZuT3V0cHV0LCBTdGFjaywgdHlwZSBTdGFja1Byb3BzIH0gZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQgeyBFY3JEZWZhdWx0SW1hZ2UsIERlZmF1bHRFdmVudEJ1cyB9IGZyb20gXCIuLi8uLi9jb25maWcvYXdzXCI7XG5pbXBvcnQgeyB0eXBlIENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5pbXBvcnQgeyBNYW5hZ2VtZW50RXZlbnRzVHJhaWwgfSBmcm9tIFwiLi4vLi4vY29uZmlnL2F3cy9jbG91ZFRyYWlsXCI7XG5pbXBvcnQgeyBPaWRjQ29ubmVjdG9yIH0gZnJvbSBcIi4uLy4uL2NvbmZpZy9hd3Mvb2lkY0Nvbm5lY3RvclwiO1xuaW1wb3J0IHsgQWNjb3VudE1vbml0b3JpbmdSb2xlIH0gZnJvbSBcIi4uLy4uL2NvbmZpZy9hd3MvYWNjb3VudE1vbml0b3JpbmdSb2xlXCI7XG5pbXBvcnQgeyBBY2NvdW50QXVkaXRSb2xlIH0gZnJvbSBcIi4uLy4uL2NvbmZpZy9hd3MvYWNjb3VudEF1ZGl0Um9sZVwiO1xuaW1wb3J0IHsgZ2V0Q29uZmlnIH0gZnJvbSBcIi4uLy4uL3V0aWxzL2dldENvbmZpZ1wiO1xuaW1wb3J0IHsgRGlzYXN0ZXJSZWNvdmVyeSB9IGZyb20gXCIuLi8uLi9jb25maWcvYXdzL2Rpc2FzdGVyUmVjb3ZlcnlcIjtcbmltcG9ydCB0eXBlIHsgT3JnYW5pc2F0aW9uVHlwZSB9IGZyb20gXCIuL2ludGVyZmFjZXMvb3JnYW5pc2F0aW9uXCI7XG5cbmV4cG9ydCBpbnRlcmZhY2UgQWNjb3VudFByb3BzIGV4dGVuZHMgU3RhY2tQcm9wcyB7XG4gIGFjY291bnRJZD86IHN0cmluZztcbiAgcmVnaW9uPzogc3RyaW5nO1xufVxuXG5leHBvcnQgY2xhc3MgQWNjb3VudCBleHRlbmRzIFN0YWNrIHtcbiAgcHVibGljIHJlYWRvbmx5IG9yZ2FuaXNhdGlvblR5cGU6IE9yZ2FuaXNhdGlvblR5cGUgPSBcImFjY291bnRcIjtcbiAgcHJvdGVjdGVkIHJlYWRvbmx5IHJlc29sdmVkUmVnaW9uOiBzdHJpbmc7XG5cbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM6IEFjY291bnRQcm9wcykge1xuICAgIGNvbnN0IGNvbmZpZyA9IGdldENvbmZpZygpO1xuICAgIGNvbnN0IGFjY291bnRJZCA9IHByb3BzLmFjY291bnRJZCA/PyBjb25maWcuYWNjb3VudElkO1xuICAgIGNvbnN0IHJlZ2lvbiA9IHByb3BzLnJlZ2lvbiA/PyBjb25maWcucmVnaW9uO1xuXG4gICAgaWYgKCFhY2NvdW50SWQpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgXCJBY2NvdW50IHJlcXVpcmVzIGFuIGFjY291bnQgSUQuIFByb3ZpZGUgaXQgdmlhIGFjY291bnRJZCBvciBlbnN1cmUgQ0RLIGNvbnRleHQgaW5jbHVkZXMgYWNjb3VudElkLlwiXG4gICAgICApO1xuICAgIH1cblxuICAgIHN1cGVyKHNjb3BlLCBpZCwgcHJvcHMpO1xuXG4gICAgdGhpcy5yZXNvbHZlZFJlZ2lvbiA9IHJlZ2lvbjtcblxuICAgIGNvbnN0IG9yZ0lkID0gdGhpcy5ub2RlLnRyeUdldENvbnRleHQoXCJvcmdJZFwiKTtcbiAgICBpZiAob3JnSWQpIHtcbiAgICAgIG5ldyBDZm5PdXRwdXQodGhpcywgXCJPcmdhbmlzYXRpb25JZE91dHB1dFwiLCB7XG4gICAgICAgIGtleTogXCJPcmdhbmlzYXRpb25JZFwiLFxuICAgICAgICB2YWx1ZTogb3JnSWQsXG4gICAgICAgIGV4cG9ydE5hbWU6IFwiT3JnYW5pc2F0aW9uSWRcIlxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgbmV3IENmbk91dHB1dCh0aGlzLCBcIkFjY291bnRJZE91dHB1dFwiLCB7XG4gICAgICBrZXk6IFwiQWNjb3VudElkXCIsXG4gICAgICB2YWx1ZTogdGhpcy5hY2NvdW50LFxuICAgICAgZXhwb3J0TmFtZTogXCJBY2NvdW50SWRcIixcbiAgICAgIGRlc2NyaXB0aW9uOiBcIkFXUyBBY2NvdW50IElEIGZvciB0aGlzIGFjY291bnRcIlxuICAgIH0pO1xuXG4gICAgY29uc3QgZXZlbnRCdXMgPSBuZXcgRGVmYXVsdEV2ZW50QnVzKHRoaXMsIFwiRXZlbnRCdXNcIik7XG5cbiAgICBjb25zdCBpcGFtUG9vbElkID0gdGhpcy5ub2RlLnRyeUdldENvbnRleHQoXCJpcGFtUG9vbElkXCIpO1xuICAgIGlmIChpZCA9PT0gXCJBY2NvdW50XCIgJiYgaXBhbVBvb2xJZCkge1xuICAgICAgY29uc3QgcmVnaW9uU3VmZml4ID0gcmVnaW9uLnJlcGxhY2UoLy0vZywgXCJcIik7XG4gICAgICBuZXcgQ2ZuT3V0cHV0KHRoaXMsIFwiSXBhbVBvb2xJZE91dHB1dFwiLCB7XG4gICAgICAgIGtleTogYElwYW1Qb29sSWQke2FjY291bnRJZH0ke3JlZ2lvblN1ZmZpeH1gLFxuICAgICAgICB2YWx1ZTogaXBhbVBvb2xJZCxcbiAgICAgICAgZXhwb3J0TmFtZTogYElwYW1Qb29sSWQke2FjY291bnRJZH0ke3JlZ2lvblN1ZmZpeH1gXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICBjb25zdCBmamFsbE9yZ0lkID0gdGhpcy5ub2RlLnRyeUdldENvbnRleHQoXCJmamFsbE9yZ0lkXCIpO1xuICAgIGlmIChpZCA9PT0gXCJBY2NvdW50XCIgJiYgZmphbGxPcmdJZCkge1xuICAgICAgbmV3IE9pZGNDb25uZWN0b3IodGhpcywgXCJPaWRjQ29ubmVjdG9yXCIsIHsgZmphbGxPcmdJZCB9KTtcbiAgICB9XG5cbiAgICAvLyBQZXItYWNjb3VudCBtb25pdG9yaW5nIHJvbGUgKHVuY29uZGl0aW9uYWw7IEV4dGVybmFsSWQgYWRkZWQgd2hlbiBvcmdJZCBrbm93bilcbiAgICBuZXcgQWNjb3VudE1vbml0b3JpbmdSb2xlKFxuICAgICAgdGhpcyxcbiAgICAgIFwiTW9uaXRvcmluZ1JvbGVcIixcbiAgICAgIGZqYWxsT3JnSWQgPyB7IGZqYWxsT3JnSWQgfSA6IHVuZGVmaW5lZFxuICAgICk7XG5cbiAgICAvLyBQZXItYWNjb3VudCBhdWRpdCByb2xlIChjb25kaXRpb25hbCBvbiBmamFsbE9yZ0lkKVxuICAgIGlmIChmamFsbE9yZ0lkKSB7XG4gICAgICBuZXcgQWNjb3VudEF1ZGl0Um9sZSh0aGlzLCBcIkF1ZGl0Um9sZVwiLCB7IGZqYWxsT3JnSWQgfSk7XG4gICAgfVxuXG4gICAgbmV3IE1hbmFnZW1lbnRFdmVudHNUcmFpbCh0aGlzLCBcIkNsb3VkVHJhaWxcIiwge1xuICAgICAgYWNjb3VudElkOiB0aGlzLmFjY291bnQsXG4gICAgICByZWdpb25cbiAgICB9KTtcblxuICAgIG5ldyBFY3JEZWZhdWx0SW1hZ2UodGhpcywgXCJFY3JEZWZhdWx0SW1hZ2VcIiwge1xuICAgICAgcmVnaW9uLFxuICAgICAgYWNjb3VudElkOiB0aGlzLmFjY291bnQsXG4gICAgICBldmVudEJ1c0FybjogZXZlbnRCdXMuZGVmYXVsdEV2ZW50QnVzQXJuLnZhbHVlXG4gICAgfSk7XG5cbiAgICBjb25zdCBlbnZpcm9ubWVudCA9IGNvbmZpZy5lbnZpcm9ubWVudCB8fCBcInVua25vd25cIjtcblxuICAgIGlmIChjb25maWcuZGlzYXN0ZXJSZWNvdmVyeVJlZ2lvbikge1xuICAgICAgY29uc3QgaXNDb21wbGlhbmNlQWNjb3VudCA9IGVudmlyb25tZW50ID09PSBcImNvbXBsaWFuY2VcIjtcblxuICAgICAgaWYgKGVudmlyb25tZW50ID09PSBcInByb2R1Y3Rpb25cIiB8fCBpc0NvbXBsaWFuY2VBY2NvdW50KSB7XG4gICAgICAgIG5ldyBEaXNhc3RlclJlY292ZXJ5KHRoaXMsIFwiRGlzYXN0ZXJSZWNvdmVyeVwiLCB7XG4gICAgICAgICAgcmVnaW9uLFxuICAgICAgICAgIGFjY291bnRJZFxuICAgICAgICB9KTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBuZXcgQ2ZuT3V0cHV0KHRoaXMsIFwiRW52aXJvbm1lbnRcIiwge1xuICAgICAga2V5OiBcIkVudmlyb25tZW50XCIsXG4gICAgICB2YWx1ZTogZW52aXJvbm1lbnQsXG4gICAgICBleHBvcnROYW1lOiBcIkVudmlyb25tZW50XCIsXG4gICAgICBkZXNjcmlwdGlvbjpcbiAgICAgICAgXCJFbnZpcm9ubWVudCB0eXBlIGZvciB0aGlzIGFjY291bnQgKGUuZy4sIHByb2R1Y3Rpb24sIHN0YWdpbmcsIGRldmVsb3BtZW50KVwiXG4gICAgfSk7XG4gIH1cbn1cbiJdfQ==
|