canopycms-cdk 0.0.67-int.89 → 0.0.67-int.90
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/constructs/cms-service.d.ts +122 -9
- package/dist/constructs/cms-service.js +415 -12
- package/lambda/asset-transform/dist/handler.js +30 -3
- package/package.json +5 -4
- package/worker/dist/index.js +7443 -191
|
@@ -34,11 +34,18 @@ export interface CanopyCmsServiceProps {
|
|
|
34
34
|
/** Lambda reserved concurrency cap (default: 10) */
|
|
35
35
|
reservedConcurrency?: number;
|
|
36
36
|
/**
|
|
37
|
-
* Lambda architecture (default: `Architecture.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* Lambda architecture (default: `Architecture.ARM_64`, matching the EC2
|
|
38
|
+
* worker and AssetSupport's transform Lambda).
|
|
39
|
+
*
|
|
40
|
+
* This also decides the image's architecture for
|
|
41
|
+
* `DockerImageCode.fromImageAsset`: the construct always passes a resolved
|
|
42
|
+
* architecture to the function, and CDK derives the Docker build platform
|
|
43
|
+
* from it. So omit `platform` on `fromImageAsset`. An explicit `platform`
|
|
44
|
+
* overrides the derived one, and an image built for the other architecture
|
|
45
|
+
* cannot run on the function: an arm64 image on an x86_64 function fails at
|
|
46
|
+
* invoke with `Runtime.InvalidEntrypoint` (see "Where the image is built" in
|
|
47
|
+
* docs/deploying-to-aws.md). A prebuilt image (`DockerImageCode.fromEcr`) has
|
|
48
|
+
* no build for CDK to steer, so it must already be built for this architecture.
|
|
42
49
|
*/
|
|
43
50
|
architecture?: lambda.Architecture;
|
|
44
51
|
/** EC2 spot max price (default: on-demand rate for t4g.nano) */
|
|
@@ -70,8 +77,114 @@ export interface CanopyCmsServiceProps {
|
|
|
70
77
|
githubRepo: string;
|
|
71
78
|
/** Secrets Manager ARN for the GitHub bot token */
|
|
72
79
|
githubTokenSecretArn?: string;
|
|
80
|
+
/**
|
|
81
|
+
* The key within a JSON secret document at `githubTokenSecretArn`; omit when
|
|
82
|
+
* the secret's whole value is the credential.
|
|
83
|
+
*
|
|
84
|
+
* Stamped into the worker's `CANOPYCMS_GITHUB_TOKEN_SECRET_JSON_FIELD`, which
|
|
85
|
+
* `getSecret` (packages/canopycms-cdk/worker/secrets.ts) reads to pull one
|
|
86
|
+
* field out of the document instead of using the whole string. Omitting it is
|
|
87
|
+
* the default and the common case — a secret holding a bare `ghp_…` needs
|
|
88
|
+
* nothing here.
|
|
89
|
+
*
|
|
90
|
+
* This is NOT the ECS/CloudFormation `arn:…:secret:name-AbCdEf:KEY::`
|
|
91
|
+
* convention. The worker calls the `GetSecretValue` API, which does not parse
|
|
92
|
+
* that suffix; a literal ARN carrying one is refused at synth (see
|
|
93
|
+
* `assertSecretArnHasNoFieldSuffix` below, and the known limit recorded on
|
|
94
|
+
* `SECRET_ARN_WITH_EMPTY_VERSION_TAIL` for the one token spelling that gets
|
|
95
|
+
* through) and the field belongs here instead.
|
|
96
|
+
*/
|
|
97
|
+
githubTokenSecretJsonField?: string;
|
|
98
|
+
/**
|
|
99
|
+
* GitHub App ID, to authenticate the worker as a GitHub App installation
|
|
100
|
+
* instead of as a personal access token.
|
|
101
|
+
*
|
|
102
|
+
* **The token is the default and stays first-class.** Registering a GitHub
|
|
103
|
+
* App under an organisation takes an owner of that organisation (or a GitHub
|
|
104
|
+
* App manager for all its Apps), which many adopters are not, so this is the
|
|
105
|
+
* "if your organisation requires it" option, not a direction of travel.
|
|
106
|
+
* Nothing about
|
|
107
|
+
* `githubTokenSecretArn` is deprecated or warned about.
|
|
108
|
+
*
|
|
109
|
+
* All three App props (`githubAppId`, `githubAppInstallationId`,
|
|
110
|
+
* `githubAppPrivateKeySecretArn`) are set together or not at all, and App
|
|
111
|
+
* auth is mutually exclusive with `githubTokenSecretArn` — both are refused
|
|
112
|
+
* at synth. That mirrors `resolveWorkerGitHubAuth` in core
|
|
113
|
+
* (packages/canopycms/src/worker/github-auth.ts), which refuses the same two
|
|
114
|
+
* shapes at boot; checking here turns a 5-second systemd restart loop into a
|
|
115
|
+
* failed `cdk synth`.
|
|
116
|
+
*
|
|
117
|
+
* Stamped into the worker's `CANOPYCMS_GITHUB_APP_ID`.
|
|
118
|
+
*/
|
|
119
|
+
githubAppId?: string;
|
|
120
|
+
/**
|
|
121
|
+
* The App's installation ID on your repository — NOT the App ID above.
|
|
122
|
+
*
|
|
123
|
+
* An App can be installed on several accounts, and a token is minted per
|
|
124
|
+
* installation, so both numbers are needed. It is the trailing number in the
|
|
125
|
+
* URL of the App's install page under your organisation's settings.
|
|
126
|
+
*
|
|
127
|
+
* Stamped into the worker's `CANOPYCMS_GITHUB_APP_INSTALLATION_ID`.
|
|
128
|
+
*/
|
|
129
|
+
githubAppInstallationId?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Secrets Manager ARN for the App's PEM private key.
|
|
132
|
+
*
|
|
133
|
+
* **ARN-only: there is deliberately no plaintext prop for this key**, unlike
|
|
134
|
+
* every other credential the construct knows about, and the reason is
|
|
135
|
+
* mechanical rather than a matter of taste. Every value the construct puts in
|
|
136
|
+
* the worker's environment is written into a `.env` file that systemd reads
|
|
137
|
+
* as `EnvironmentFile=`, where a newline starts a new variable — so
|
|
138
|
+
* `assertEnvSafe` refuses one, and a PEM is inherently multi-line. A
|
|
139
|
+
* plaintext key could not be delivered to the worker intact by this path at
|
|
140
|
+
* all. Passing the PEM itself here is caught by name at synth (see
|
|
141
|
+
* `assertNotInlinePrivateKey`, and the note there about the generated stack
|
|
142
|
+
* reaching CDK's own ARN complaint first) rather than surfacing as a puzzling
|
|
143
|
+
* "an ARN must not contain a newline".
|
|
144
|
+
*
|
|
145
|
+
* The ARN is unioned into the worker's IAM policy alongside the other secret
|
|
146
|
+
* ARN props; you do not need to repeat it in `secretsArns`.
|
|
147
|
+
*/
|
|
148
|
+
githubAppPrivateKeySecretArn?: string;
|
|
149
|
+
/**
|
|
150
|
+
* The key within a JSON secret document at `githubAppPrivateKeySecretArn`;
|
|
151
|
+
* omit when the secret's whole value is the PEM.
|
|
152
|
+
*
|
|
153
|
+
* Stamped into the worker's
|
|
154
|
+
* `CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD`. See
|
|
155
|
+
* `githubTokenSecretJsonField` above — same mechanism, same non-relationship
|
|
156
|
+
* to the ECS `:KEY::` ARN suffix. This is the case that motivated the JSON
|
|
157
|
+
* field support in the first place: an App private key is exactly the kind of
|
|
158
|
+
* material an organisation keeps inside one credential document per
|
|
159
|
+
* environment.
|
|
160
|
+
*
|
|
161
|
+
* A PEM stored as a JSON string value carries its newlines as `\n` escapes,
|
|
162
|
+
* which `JSON.parse` turns back into real newlines — and the worker
|
|
163
|
+
* additionally runs whatever it reads through
|
|
164
|
+
* `normalizeGitHubAppPrivateKey`, which unescapes and base64-unwraps, so a
|
|
165
|
+
* key mangled by a single-line config field still works.
|
|
166
|
+
*/
|
|
167
|
+
githubAppPrivateKeySecretJsonField?: string;
|
|
73
168
|
/** Secrets Manager ARN for the Clerk secret key */
|
|
74
169
|
clerkSecretKeySecretArn?: string;
|
|
170
|
+
/**
|
|
171
|
+
* The key within a JSON secret document at `clerkSecretKeySecretArn`; omit
|
|
172
|
+
* when the secret's whole value is the credential.
|
|
173
|
+
*
|
|
174
|
+
* Stamped into the worker's `CLERK_SECRET_KEY_SECRET_JSON_FIELD`. See
|
|
175
|
+
* `githubTokenSecretJsonField` above — same mechanism, same non-relationship
|
|
176
|
+
* to the ECS `:KEY::` ARN suffix.
|
|
177
|
+
*
|
|
178
|
+
* Note that this covers `CLERK_SECRET_KEY` only. A Clerk JSON document
|
|
179
|
+
* typically also holds `CLERK_JWT_KEY` and the publishable key, and NEITHER
|
|
180
|
+
* can be sourced from Secrets Manager at all: `CLERK_JWT_KEY` reaches the
|
|
181
|
+
* Lambda as a plain value through `environment`, and the publishable key is a
|
|
182
|
+
* Docker build arg inlined into the client bundle. Both are public material
|
|
183
|
+
* (docs/deploying-to-aws.md, "Security Model"), so that is by design rather
|
|
184
|
+
* than an omission — but it means pointing this prop at your document does
|
|
185
|
+
* not relieve you of supplying those two separately.
|
|
186
|
+
*/
|
|
187
|
+
clerkSecretKeySecretJsonField?: string;
|
|
75
188
|
/**
|
|
76
189
|
* The GitHub repository's default branch name (default: 'main').
|
|
77
190
|
*
|
|
@@ -227,10 +340,10 @@ export interface CanopyCmsServiceProps {
|
|
|
227
340
|
* - Lambda function (Docker image, EFS mount, private subnet, no internet)
|
|
228
341
|
* - Lambda Function URL (for CloudFront origin)
|
|
229
342
|
* - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd) -
|
|
230
|
-
* rolled
|
|
231
|
-
* bundle
|
|
232
|
-
* launch template until the next spot interruption (see
|
|
233
|
-
* below)
|
|
343
|
+
* rolled via the ASG's UpdatePolicy by every deploy that changes its launch
|
|
344
|
+
* template, so a changed worker bundle reaches the instance instead of
|
|
345
|
+
* sitting unused in a launch template until the next spot interruption (see
|
|
346
|
+
* the UpdatePolicy below)
|
|
234
347
|
* - Dedicated CloudWatch log groups for the CMS Lambda and the worker's
|
|
235
348
|
* stdout/stderr (the worker's is shipped via the amazon-cloudwatch-agent -
|
|
236
349
|
* journald is not agent-readable), each with a custom name/retention/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { Construct } from 'constructs';
|
|
4
|
-
import { Duration, RemovalPolicy, Stack, aws_ec2 as ec2, aws_efs as efs, aws_iam as iam, aws_lambda as lambda, aws_autoscaling as autoscaling, aws_s3_assets as s3assets, aws_logs as logs, } from 'aws-cdk-lib';
|
|
4
|
+
import { Duration, RemovalPolicy, Stack, Token, aws_ec2 as ec2, aws_efs as efs, aws_iam as iam, aws_lambda as lambda, aws_autoscaling as autoscaling, aws_s3_assets as s3assets, aws_logs as logs, } from 'aws-cdk-lib';
|
|
5
5
|
import { attachLambdaExecutionPolicies } from './lambda-execution-role.js';
|
|
6
6
|
// This package (`canopycms-cdk`) is `"type": "module"`, so its compiled
|
|
7
7
|
// output is real ESM - `__dirname` is not a global there. Found while
|
|
@@ -97,6 +97,27 @@ function assertEnvSafe(name, value) {
|
|
|
97
97
|
`(got ${JSON.stringify(value)}). It is written into the worker's .env file with a ` +
|
|
98
98
|
`<< '${ENV_HEREDOC_DELIMITER}' heredoc, which that value would terminate early.`);
|
|
99
99
|
}
|
|
100
|
+
// The same systemd `EnvironmentFile=` parser the leading-quote rule above is
|
|
101
|
+
// about also treats a backslash as an escape: `a\b` arrives as `ab`, and a
|
|
102
|
+
// value ENDING in a backslash continues onto the next line, swallowing the
|
|
103
|
+
// .env entry that follows it. That is the quote hazard again in a quieter
|
|
104
|
+
// form -- it corrupts a neighbouring variable rather than the one it appears
|
|
105
|
+
// in -- so it is refused here rather than debugged on an instance.
|
|
106
|
+
if (value.includes('\\')) {
|
|
107
|
+
throw new Error(`CanopyCmsService: ${name} must not contain a backslash (got ${JSON.stringify(value)}). ` +
|
|
108
|
+
`It is written into the worker's .env file, which systemd reads as EnvironmentFile -- ` +
|
|
109
|
+
`there a backslash escapes the next character, and a trailing one continues the value ` +
|
|
110
|
+
`onto the following line, consuming the next variable entirely.`);
|
|
111
|
+
}
|
|
112
|
+
// Leading/trailing whitespace is stripped by that same parser, so a value
|
|
113
|
+
// that is only whitespace reaches the worker as an empty string and every
|
|
114
|
+
// caller downstream treats it as unset -- the silent-discard case, arriving
|
|
115
|
+
// by a route no charset check upstream can see.
|
|
116
|
+
if (value !== value.trim()) {
|
|
117
|
+
throw new Error(`CanopyCmsService: ${name} must not start or end with whitespace ` +
|
|
118
|
+
`(got ${JSON.stringify(value)}). systemd strips it when reading the worker's .env, so ` +
|
|
119
|
+
`the value the worker sees would differ from the one configured here.`);
|
|
120
|
+
}
|
|
100
121
|
return value;
|
|
101
122
|
}
|
|
102
123
|
/**
|
|
@@ -174,6 +195,298 @@ function assertValidGitBranchName(propName, value) {
|
|
|
174
195
|
}
|
|
175
196
|
return value;
|
|
176
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A Secrets Manager ARN carrying the ECS/CloudFormation JSON-field suffix,
|
|
200
|
+
* i.e. `arn:…:secret:name-AbCdEf:MY_KEY::` rather than `arn:…:secret:name-AbCdEf`.
|
|
201
|
+
*
|
|
202
|
+
* Keyed on "a colon anywhere after `:secret:`", because a secret NAME cannot
|
|
203
|
+
* contain one: Secrets Manager's documented name charset is ASCII letters,
|
|
204
|
+
* digits and `/_+=.@-`. So everything after `:secret:` in a secret ARN is the
|
|
205
|
+
* name -- plus the six random characters AWS appends, when the ARN is a
|
|
206
|
+
* complete one rather than the partial form this guard deliberately accepts --
|
|
207
|
+
* and a further colon can only begin the `:json-key:version-stage:version-id`
|
|
208
|
+
* tail.
|
|
209
|
+
*
|
|
210
|
+
* An earlier version of this anchored on the six-character suffix itself
|
|
211
|
+
* (`-[A-Za-z0-9]{6}:`) and therefore missed the suffix form built on a
|
|
212
|
+
* name-only ARN -- `arn:…:secret:gh:MY_KEY::`, which is the shape ECS's own
|
|
213
|
+
* documentation shows. That ARN was accepted, stamped, and written into the IAM
|
|
214
|
+
* policy, producing exactly the AccessDenied restart-loop this guard exists to
|
|
215
|
+
* prevent.
|
|
216
|
+
*/
|
|
217
|
+
const SECRET_ARN_WITH_FIELD_SUFFIX = /:secret:[^:]*:/;
|
|
218
|
+
/**
|
|
219
|
+
* The last two characters of the `…:json-key::` spelling -- the ECS suffix with
|
|
220
|
+
* `version-stage` and `version-id` left empty, which is how the convention is
|
|
221
|
+
* almost always written and how AWS's own examples show it.
|
|
222
|
+
*
|
|
223
|
+
* Checked in ADDITION to the regex above, for one case the regex cannot see: an
|
|
224
|
+
* unresolved CDK token, `${Token[TOKEN.42]}:MY_KEY::`, carries no `:secret:` to
|
|
225
|
+
* anchor on because the ARN has not been rendered yet. No well-formed secret
|
|
226
|
+
* ARN, token or literal, ends in two colons, so this is safe to refuse.
|
|
227
|
+
*
|
|
228
|
+
* KNOWN LIMIT, stated rather than fixed: a token ARN with NON-empty version
|
|
229
|
+
* parts (`${Token[…]}:MY_KEY:AWSCURRENT:v1`) is caught by neither check and is
|
|
230
|
+
* stamped verbatim. Recognising it needs `Token.isUnresolved` plus a guess at
|
|
231
|
+
* where the token ends; the spelling adopters actually copy has the empty
|
|
232
|
+
* parts, and every LITERAL ARN is caught by the regex whatever its version
|
|
233
|
+
* parts say.
|
|
234
|
+
*/
|
|
235
|
+
const SECRET_ARN_WITH_EMPTY_VERSION_TAIL = '::';
|
|
236
|
+
/**
|
|
237
|
+
* Guards one (secret ARN, JSON field) prop pair at synth.
|
|
238
|
+
*
|
|
239
|
+
* Both checks exist because the failure they replace is SILENT, and both
|
|
240
|
+
* failures land on the worker at boot -- where systemd's `Restart=always` turns
|
|
241
|
+
* a misconfiguration into an indefinite 5-second restart loop rather than
|
|
242
|
+
* anything `cdk deploy` reports.
|
|
243
|
+
*
|
|
244
|
+
* 1. A JSON-field prop with no ARN prop. The field env var is stamped, the ARN
|
|
245
|
+
* is not, and the credential is then read from nowhere: for Clerk that
|
|
246
|
+
* leaves `refreshAuthCache` undefined, which disables auth-cache refresh
|
|
247
|
+
* with NO log line at all; for GitHub the worker reports "CANOPYCMS_GITHUB_TOKEN
|
|
248
|
+
* or CANOPYCMS_GITHUB_TOKEN_SECRET_ARN is required" while the adopter is
|
|
249
|
+
* looking at a stack that plainly configures a GitHub secret.
|
|
250
|
+
*
|
|
251
|
+
* 2. An ARN carrying the ECS `:KEY::` suffix. That form is a
|
|
252
|
+
* CloudFormation-dynamic-reference and ECS `secrets.valueFrom` convention;
|
|
253
|
+
* the `GetSecretValue` API this worker calls does not parse it. Through the
|
|
254
|
+
* scaffolded stack the adopter gets CDK's own cryptic complaint
|
|
255
|
+
* ("does not appear to be complete; missing 6-character suffix" --
|
|
256
|
+
* `Secret.fromSecretCompleteArn` gates on `/-[a-z0-9]{6}$/i`); hand-rolling
|
|
257
|
+
* the stack, the string lands verbatim in the worker's IAM `Resource`, where
|
|
258
|
+
* it can never match the real secret, and the worker gets AccessDenied.
|
|
259
|
+
* Naming the JSON-field prop in the message is the whole point of the check:
|
|
260
|
+
* the adopter's intent is supported, just spelled differently here.
|
|
261
|
+
*
|
|
262
|
+
* An empty JSON field is rejected for the same reason `settingsBranch: ''` is:
|
|
263
|
+
* the worker reads a blank env var as "not configured" (`|| undefined` at both
|
|
264
|
+
* call sites in worker/index.ts), so stamping it would discard an explicitly
|
|
265
|
+
* set prop without a word.
|
|
266
|
+
*/
|
|
267
|
+
function assertSecretPropPair(arnPropName, arn, jsonFieldPropName, jsonField) {
|
|
268
|
+
if (jsonField !== undefined) {
|
|
269
|
+
// `.trim()`, not `=== ''`: systemd's EnvironmentFile parser strips leading
|
|
270
|
+
// and trailing whitespace from a value, so `" "` reaches the worker as `""`
|
|
271
|
+
// and takes the same silently-ignored path an empty string would. The
|
|
272
|
+
// untrimmed check let exactly the case it was written for through.
|
|
273
|
+
if (jsonField.trim() === '') {
|
|
274
|
+
throw new Error(`CanopyCmsService: ${jsonFieldPropName} must name a key, but it is ` +
|
|
275
|
+
`${JSON.stringify(jsonField)}. The worker reads a blank value as "no field configured" ` +
|
|
276
|
+
`and falls back to using the secret's whole value, silently ignoring this prop -- name ` +
|
|
277
|
+
`the key you want, or omit the prop entirely.`);
|
|
278
|
+
}
|
|
279
|
+
if (!arn) {
|
|
280
|
+
throw new Error(`CanopyCmsService: ${jsonFieldPropName} is set but ${arnPropName} is not. ` +
|
|
281
|
+
`The JSON field names a key INSIDE a secret, so it does nothing without the secret's ` +
|
|
282
|
+
`ARN -- the worker would be told which key to read and never told where to read it ` +
|
|
283
|
+
`from. Set ${arnPropName}, or drop ${jsonFieldPropName}.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (arn !== undefined) {
|
|
287
|
+
assertSecretArnHasNoFieldSuffix(arnPropName, arn, jsonFieldPropName);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Rejects the ECS `:KEY::` ARN suffix on any prop that carries a secret ARN.
|
|
292
|
+
*
|
|
293
|
+
* Separate from `assertSecretPropPair` because `secretsArns` has no JSON-field
|
|
294
|
+
* prop of its own and still needs the check: its values go verbatim into the
|
|
295
|
+
* worker's IAM policy, where a suffixed ARN is an unmatchable `Resource` and
|
|
296
|
+
* produces exactly the AccessDenied restart-loop described above.
|
|
297
|
+
*/
|
|
298
|
+
function assertSecretArnHasNoFieldSuffix(propName, arn, jsonFieldPropName) {
|
|
299
|
+
// `unknown`, and narrowed here, because the types are not the whole story:
|
|
300
|
+
// `secretsArns: [process.env.EXTRA_SECRET_ARN!]` is the idiom a CDK app that
|
|
301
|
+
// reads its config from the environment reaches for -- the scaffolded
|
|
302
|
+
// `bin/app.ts` does exactly that everywhere else -- and `!` turns an unset
|
|
303
|
+
// variable into `undefined` with the compiler none the wiser.
|
|
304
|
+
//
|
|
305
|
+
// Throwing is a deliberate change from what that used to do. The entry was
|
|
306
|
+
// silently dropped by the `typeof arn === 'string'` filter on the IAM union
|
|
307
|
+
// below, so the worker was told to read a secret it had no grant for and got
|
|
308
|
+
// AccessDenied at boot -- the failure that filter's own comment is about.
|
|
309
|
+
if (typeof arn !== 'string' || arn.length === 0) {
|
|
310
|
+
throw new Error(`CanopyCmsService: ${propName} must be a non-empty secret ARN string, but it is ` +
|
|
311
|
+
`${JSON.stringify(arn) ?? String(arn)}. An unset environment variable asserted with '!' ` +
|
|
312
|
+
`arrives here as undefined; it would otherwise be dropped from the worker's IAM policy ` +
|
|
313
|
+
`in silence, leaving a worker that knows which secret to read and cannot read it.`);
|
|
314
|
+
}
|
|
315
|
+
if (!SECRET_ARN_WITH_FIELD_SUFFIX.test(arn) && !arn.endsWith(SECRET_ARN_WITH_EMPTY_VERSION_TAIL))
|
|
316
|
+
return;
|
|
317
|
+
const alternative = jsonFieldPropName
|
|
318
|
+
? `Pass the plain secret ARN (everything up to and including the six-character suffix) and ` +
|
|
319
|
+
`name the key with ${jsonFieldPropName} instead.`
|
|
320
|
+
: `Pass the plain secret ARN, ending at the six-character suffix.`;
|
|
321
|
+
throw new Error(`CanopyCmsService: ${propName} ${JSON.stringify(arn)} carries a ':KEY::' JSON-field suffix. ` +
|
|
322
|
+
`That is the ECS / CloudFormation dynamic-reference convention; the worker reads secrets ` +
|
|
323
|
+
`with the GetSecretValue API, which does not parse it -- the suffixed string would be ` +
|
|
324
|
+
`written into the worker's IAM policy, where it can never match the real secret, and the ` +
|
|
325
|
+
`worker would fail with AccessDenied at boot. ${alternative}`);
|
|
326
|
+
}
|
|
327
|
+
/** The three props that together configure GitHub App authentication. */
|
|
328
|
+
const GITHUB_APP_PROP_NAMES = [
|
|
329
|
+
'githubAppId',
|
|
330
|
+
'githubAppInstallationId',
|
|
331
|
+
'githubAppPrivateKeySecretArn',
|
|
332
|
+
];
|
|
333
|
+
/**
|
|
334
|
+
* The props that mean "this deployment authenticates with a personal access
|
|
335
|
+
* token". The JSON field belongs here as well as the ARN: on its own it cannot
|
|
336
|
+
* authenticate anything, but its PRESENCE still says which credential the
|
|
337
|
+
* adopter thinks they are configuring, which is what the exclusivity rule needs
|
|
338
|
+
* to know.
|
|
339
|
+
*/
|
|
340
|
+
const GITHUB_TOKEN_PROP_NAMES = ['githubTokenSecretArn', 'githubTokenSecretJsonField'];
|
|
341
|
+
/**
|
|
342
|
+
* Rejects a PEM private key passed where a prop expects an identifier or an ARN.
|
|
343
|
+
*
|
|
344
|
+
* There is no plaintext private-key prop, and there cannot be one: the value
|
|
345
|
+
* would be written into the worker's `.env`, which systemd reads as
|
|
346
|
+
* `EnvironmentFile=` where a newline begins a new variable. So the realistic
|
|
347
|
+
* mistake is to paste the key into `githubAppPrivateKeySecretArn` -- the prop
|
|
348
|
+
* whose name contains "PrivateKey" -- instead of the ARN of a secret holding
|
|
349
|
+
* it.
|
|
350
|
+
*
|
|
351
|
+
* Without this, that lands on `assertEnvSafe`'s generic rule and reports "must
|
|
352
|
+
* not contain a newline" about an ARN, which explains the mechanism and not the
|
|
353
|
+
* mistake. Checked on each of `GITHUB_APP_PROP_NAMES` because the same
|
|
354
|
+
* misunderstanding puts the key in any of them, and each would otherwise
|
|
355
|
+
* produce a differently confusing message.
|
|
356
|
+
* `githubAppPrivateKeySecretJsonField` is deliberately not checked: a JSON key
|
|
357
|
+
* NAME is not somewhere anyone mistakes a PEM for, and it keeps this aligned
|
|
358
|
+
* with the one list that defines what "the App props" are.
|
|
359
|
+
*
|
|
360
|
+
* **Reached only by a hand-written stack.** The generated
|
|
361
|
+
* `infrastructure/lib/cms-stack.ts` resolves the ARN with
|
|
362
|
+
* `Secret.fromSecretCompleteArn` BEFORE it constructs `CanopyCmsService`, so a
|
|
363
|
+
* scaffolded adopter gets CDK's own complaint ("does not appear to be complete;
|
|
364
|
+
* missing 6-character suffix") first. That asymmetry is the same one
|
|
365
|
+
* `assertSecretPropPair` documents for the token ARN, and is why this guard is
|
|
366
|
+
* worth having rather than redundant: the hand-written path has nothing else.
|
|
367
|
+
*
|
|
368
|
+
* A one-line key (a base64-wrapped PEM, say) is NOT caught here, and cannot be:
|
|
369
|
+
* it is indistinguishable from a malformed ARN at synth. It fails at boot in
|
|
370
|
+
* `normalizeGitHubAppPrivateKey`, or -- for the ARN prop -- as a Secrets
|
|
371
|
+
* Manager error naming the string it tried to fetch.
|
|
372
|
+
*/
|
|
373
|
+
function assertNotInlinePrivateKey(propName, value) {
|
|
374
|
+
if (value === undefined || !value.includes('-----BEGIN'))
|
|
375
|
+
return;
|
|
376
|
+
throw new Error(`CanopyCmsService: ${propName} looks like a PEM private key, not ${propName === 'githubAppPrivateKeySecretArn' ? 'a secret ARN' : 'an identifier'}. ` +
|
|
377
|
+
`The GitHub App private key can ONLY be supplied as a Secrets Manager ARN -- it is ` +
|
|
378
|
+
`multi-line, and every value this construct configures goes into the worker's .env file, ` +
|
|
379
|
+
`which systemd reads as EnvironmentFile where a newline starts a new variable. Store the ` +
|
|
380
|
+
`PEM in Secrets Manager and pass that secret's full ARN as githubAppPrivateKeySecretArn ` +
|
|
381
|
+
`(optionally with githubAppPrivateKeySecretJsonField if it lives inside a JSON document).`);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Rejects a GitHub App identifier that is not a whole number.
|
|
385
|
+
*
|
|
386
|
+
* Both identifiers are numeric, and the two wrong values an adopter reaches for
|
|
387
|
+
* are the App's *slug* and its `Iv1.…` OAuth client id — both are on the same
|
|
388
|
+
* settings page as the number, and neither works.
|
|
389
|
+
*
|
|
390
|
+
* The two fail differently at boot, and both are worse than failing here.
|
|
391
|
+
* `createAppAuth` refuses a non-numeric `appId` at construction (measured
|
|
392
|
+
* against `@octokit/auth-app@6.1.4`: `Number.isFinite(+options.appId)`), so the
|
|
393
|
+
* app id at least produces a named error. The installation id is checked only
|
|
394
|
+
* for falsiness there, so a non-numeric one is interpolated into
|
|
395
|
+
* `/app/installations/NaN/access_tokens` and comes back as a 404 that reads as
|
|
396
|
+
* "the app is not installed" — sending the operator to re-install a perfectly
|
|
397
|
+
* good App.
|
|
398
|
+
*
|
|
399
|
+
* Stricter than `createAppAuth`'s own `+value` coercion, deliberately: that
|
|
400
|
+
* accepts `' 12 '`, `12.5` and `0x1f`. None of them is an id.
|
|
401
|
+
*
|
|
402
|
+
* Two values pass through untouched, and both would otherwise be reported as the
|
|
403
|
+
* wrong problem:
|
|
404
|
+
*
|
|
405
|
+
* - **Empty**, which is what `process.env.GITHUB_APP_ID ?? ''` and an Actions
|
|
406
|
+
* `vars.` reference to a variable nobody created both produce. That is not a
|
|
407
|
+
* malformed id, it is an ABSENT one, and `assertGitHubAuthProps` says so by
|
|
408
|
+
* name. Reporting `must be the numeric id (got "")` instead would send the
|
|
409
|
+
* adopter to correct a value they never set.
|
|
410
|
+
* - **An unresolved CDK token**, e.g.
|
|
411
|
+
* `ssm.StringParameter.valueForStringParameter(…)` or `Fn.importValue(…)`,
|
|
412
|
+
* whose value does not exist until deploy. Refusing it would make a legitimate
|
|
413
|
+
* configuration unrepresentable; it is unverifiable here either way, so it
|
|
414
|
+
* goes through and fails at the worker if it is wrong. `githubAppPrivateKeySecretArn`
|
|
415
|
+
* already accepts a token (one trips neither the `:secret:X:` regex nor
|
|
416
|
+
* `assertEnvSafe`), so this keeps the App props consistent with each other.
|
|
417
|
+
*/
|
|
418
|
+
function assertNumericId(propName, value) {
|
|
419
|
+
if (value === undefined || value === '' || /^\d+$/.test(value))
|
|
420
|
+
return;
|
|
421
|
+
if (Token.isUnresolved(value))
|
|
422
|
+
return;
|
|
423
|
+
throw new Error(`CanopyCmsService: ${propName} must be the numeric id GitHub shows for the app ` +
|
|
424
|
+
`(got ${JSON.stringify(value)}). The app's slug and its 'Iv1.…' client id both appear on ` +
|
|
425
|
+
`the same settings page and neither works here — githubAppId is the number labelled ` +
|
|
426
|
+
`"App ID", and githubAppInstallationId is the trailing number in the URL of the app's ` +
|
|
427
|
+
`install page under your organisation's settings.`);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Guards the GitHub credential props at synth: exactly one shape, fully given.
|
|
431
|
+
*
|
|
432
|
+
* Only the SECOND rule restates `resolveWorkerGitHubAuth`
|
|
433
|
+
* (packages/canopycms/src/worker/github-auth.ts:237-248), which refuses both
|
|
434
|
+
* credentials and refuses neither. The first has no counterpart there and could
|
|
435
|
+
* not: core takes one already-built `githubAppAuth` object, so a partial set of
|
|
436
|
+
* three props is not representable by the time it sees anything. Restating the
|
|
437
|
+
* second rather than leaving it to core is the point: a worker that throws at
|
|
438
|
+
* boot is restarted by systemd every 5 seconds indefinitely while `cdk deploy`
|
|
439
|
+
* reports success, so a rule that only exists at boot is a rule the adopter
|
|
440
|
+
* discovers from CloudWatch. The core check stays because core is reachable
|
|
441
|
+
* without this construct.
|
|
442
|
+
*
|
|
443
|
+
* 1. **All three App props or none.** Two of the three is not a partial
|
|
444
|
+
* configuration that could still work: `createAppAuth` needs the App ID, the
|
|
445
|
+
* installation ID and the key, and the message names the missing ones rather
|
|
446
|
+
* than saying the set is incomplete.
|
|
447
|
+
* 2. **Not both an App and a token.** Rejected rather than resolved by
|
|
448
|
+
* precedence, because it would otherwise be undefined which identity a push
|
|
449
|
+
* or a pull request acts as -- and a PR opened by the wrong identity is not
|
|
450
|
+
* something an adopter notices quickly.
|
|
451
|
+
*
|
|
452
|
+
* Configuring NEITHER is deliberately not an error here. The worker also reads
|
|
453
|
+
* `CANOPYCMS_GITHUB_TOKEN` directly from its environment, which an adopter can
|
|
454
|
+
* supply outside this construct, and core refuses the genuinely empty case at
|
|
455
|
+
* boot with a message naming both options.
|
|
456
|
+
*/
|
|
457
|
+
function assertGitHubAuthProps(props) {
|
|
458
|
+
for (const name of GITHUB_APP_PROP_NAMES) {
|
|
459
|
+
assertNotInlinePrivateKey(name, props[name]);
|
|
460
|
+
}
|
|
461
|
+
assertNumericId('githubAppId', props.githubAppId);
|
|
462
|
+
assertNumericId('githubAppInstallationId', props.githubAppInstallationId);
|
|
463
|
+
const missing = GITHUB_APP_PROP_NAMES.filter((name) => !props[name]);
|
|
464
|
+
const provided = GITHUB_APP_PROP_NAMES.filter((name) => props[name]);
|
|
465
|
+
if (provided.length > 0 && missing.length > 0) {
|
|
466
|
+
throw new Error(`CanopyCmsService: GitHub App authentication needs all of ` +
|
|
467
|
+
`${GITHUB_APP_PROP_NAMES.join(', ')}, but ${missing.join(' and ')} ` +
|
|
468
|
+
`${missing.length === 1 ? 'is' : 'are'} not set (${provided.join(' and ')} ` +
|
|
469
|
+
`${provided.length === 1 ? 'is' : 'are'}). An App's installation token is minted from ` +
|
|
470
|
+
`all three together, so a partial set cannot authenticate at all -- supply the rest, or ` +
|
|
471
|
+
`drop them and use githubTokenSecretArn.`);
|
|
472
|
+
}
|
|
473
|
+
// The JSON field counts as "a token is configured", not just the ARN. An
|
|
474
|
+
// adopter following docs/adopter-migration.md removes `githubTokenSecretArn`
|
|
475
|
+
// and overlooks `githubTokenSecretJsonField`; left out of this check, that
|
|
476
|
+
// lands on `assertSecretPropPair` instead, which answers "Set
|
|
477
|
+
// githubTokenSecretArn, or drop githubTokenSecretJsonField" -- pointing them
|
|
478
|
+
// back at the credential they were just told to delete, and at a
|
|
479
|
+
// configuration this rule would then refuse anyway.
|
|
480
|
+
const tokenPropsSet = GITHUB_TOKEN_PROP_NAMES.filter((name) => props[name]);
|
|
481
|
+
if (provided.length > 0 && tokenPropsSet.length > 0) {
|
|
482
|
+
throw new Error(`CanopyCmsService: configure either the githubToken* props or the githubApp* props, not ` +
|
|
483
|
+
`both (${tokenPropsSet.join(' and ')} ${tokenPropsSet.length === 1 ? 'is' : 'are'} set ` +
|
|
484
|
+
`alongside ${provided.join(' and ')}). Two credentials would leave it undefined which ` +
|
|
485
|
+
`identity the worker's pushes and pull requests act as. A personal access token is the ` +
|
|
486
|
+
`default and needs no App props; GitHub App auth replaces it, so drop ` +
|
|
487
|
+
`${tokenPropsSet.join(' and ')} when you adopt it.`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
177
490
|
/**
|
|
178
491
|
* Default CMS Lambda timeout.
|
|
179
492
|
*
|
|
@@ -204,10 +517,10 @@ export const MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT = Duration.seconds(60);
|
|
|
204
517
|
* - Lambda function (Docker image, EFS mount, private subnet, no internet)
|
|
205
518
|
* - Lambda Function URL (for CloudFront origin)
|
|
206
519
|
* - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd) -
|
|
207
|
-
* rolled
|
|
208
|
-
* bundle
|
|
209
|
-
* launch template until the next spot interruption (see
|
|
210
|
-
* below)
|
|
520
|
+
* rolled via the ASG's UpdatePolicy by every deploy that changes its launch
|
|
521
|
+
* template, so a changed worker bundle reaches the instance instead of
|
|
522
|
+
* sitting unused in a launch template until the next spot interruption (see
|
|
523
|
+
* the UpdatePolicy below)
|
|
211
524
|
* - Dedicated CloudWatch log groups for the CMS Lambda and the worker's
|
|
212
525
|
* stdout/stderr (the worker's is shipped via the amazon-cloudwatch-agent -
|
|
213
526
|
* journald is not agent-readable), each with a custom name/retention/
|
|
@@ -273,14 +586,45 @@ export class CanopyCmsService extends Construct {
|
|
|
273
586
|
? assertValidGitBranchName('settingsBranch', props.settingsBranch)
|
|
274
587
|
: undefined;
|
|
275
588
|
// ------------------------------------------------------------------
|
|
589
|
+
// Secret ARNs and their JSON fields
|
|
590
|
+
// ------------------------------------------------------------------
|
|
591
|
+
//
|
|
592
|
+
// Checked here, at the top, so a misconfigured pair fails `cdk synth`
|
|
593
|
+
// rather than `cdk deploy`-then-restart-loop. See `assertSecretPropPair`
|
|
594
|
+
// for what each of the two checks costs when it is absent.
|
|
595
|
+
//
|
|
596
|
+
// WHICH CREDENTIAL first, then whether each is well formed. The order is
|
|
597
|
+
// load-bearing and the reverse produces self-contradictory advice: an
|
|
598
|
+
// adopter following docs/adopter-migration.md removes `githubTokenSecretArn`
|
|
599
|
+
// and overlooks `githubTokenSecretJsonField`, and the pair check answers
|
|
600
|
+
// "Set githubTokenSecretArn, or drop githubTokenSecretJsonField" -- pointing
|
|
601
|
+
// them straight back at the credential they were just told to delete, and at
|
|
602
|
+
// a configuration the exclusivity rule below would then refuse anyway.
|
|
603
|
+
// `assertGitHubAuthProps` is silent when no App prop is set, so this costs
|
|
604
|
+
// the token-only path nothing.
|
|
605
|
+
assertGitHubAuthProps(props);
|
|
606
|
+
assertSecretPropPair('githubTokenSecretArn', props.githubTokenSecretArn, 'githubTokenSecretJsonField', props.githubTokenSecretJsonField);
|
|
607
|
+
assertSecretPropPair('clerkSecretKeySecretArn', props.clerkSecretKeySecretArn, 'clerkSecretKeySecretJsonField', props.clerkSecretKeySecretJsonField);
|
|
608
|
+
assertSecretPropPair('githubAppPrivateKeySecretArn', props.githubAppPrivateKeySecretArn, 'githubAppPrivateKeySecretJsonField', props.githubAppPrivateKeySecretJsonField);
|
|
609
|
+
// `secretsArns` gets the suffix half of the same guard: it has no
|
|
610
|
+
// JSON-field prop, but its entries are written verbatim into the worker's
|
|
611
|
+
// IAM policy below, so a suffixed ARN fails there in precisely the way the
|
|
612
|
+
// policy's own comment describes.
|
|
613
|
+
for (const [index, arn] of (props.secretsArns ?? []).entries()) {
|
|
614
|
+
assertSecretArnHasNoFieldSuffix(`secretsArns[${index}]`, arn);
|
|
615
|
+
}
|
|
616
|
+
// ------------------------------------------------------------------
|
|
276
617
|
// Operating mode
|
|
277
618
|
// ------------------------------------------------------------------
|
|
278
619
|
//
|
|
279
620
|
// The adopter's `canopycms.config.ts` is shared by local dev, the image
|
|
280
|
-
// build and this deployment, and it
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
621
|
+
// build and this deployment, and it says `dev`. `next dev` needs that, and
|
|
622
|
+
// the image build should stay in dev mode too: build-time reads come from
|
|
623
|
+
// the working tree in either mode (`readsFromCheckout` in canopycms's
|
|
624
|
+
// build-mode.ts), so nothing in a build needs prod, while prod would hold
|
|
625
|
+
// the image builder to checks it has no reason to meet (gitBotAuthorName/
|
|
626
|
+
// gitBotAuthorEmail, a credential-verifying auth plugin). So the deployed
|
|
627
|
+
// mode is supplied here, at run time: `resolveOperatingMode`
|
|
284
628
|
// (packages/canopycms/src/operating-mode/mode-env.ts) reads CANOPY_MODE
|
|
285
629
|
// and it wins over the config literal. Without it the Lambda runs dev
|
|
286
630
|
// mode, resolves its workspace to `<cwd>/.canopy-dev`, and fails EROFS on
|
|
@@ -413,6 +757,13 @@ export class CanopyCmsService extends Construct {
|
|
|
413
757
|
if (props.lambdaRole) {
|
|
414
758
|
attachLambdaExecutionPolicies(props.lambdaRole, { vpc: true });
|
|
415
759
|
}
|
|
760
|
+
// Always resolved, never passed through as `undefined`. DockerImageFunction
|
|
761
|
+
// hands it to the image code's `_bind`, and for `fromImageAsset` that is
|
|
762
|
+
// what sets the Docker build platform. Unset, CDK sets no platform at all:
|
|
763
|
+
// Docker builds for whatever machine runs `cdk deploy` (arm64 on Apple
|
|
764
|
+
// Silicon, amd64 on an x86 CI runner) while the function stays x86_64, and
|
|
765
|
+
// the mismatch only shows at invoke. See `architecture`'s doc comment.
|
|
766
|
+
const architecture = props.architecture ?? lambda.Architecture.ARM_64;
|
|
416
767
|
this.lambdaFunction = new lambda.DockerImageFunction(this, 'CmsFunction', {
|
|
417
768
|
code: props.cmsDockerImage,
|
|
418
769
|
// Default (unset) leaves CDK to create the execution role, with its own
|
|
@@ -421,7 +772,7 @@ export class CanopyCmsService extends Construct {
|
|
|
421
772
|
memorySize: props.memorySize ?? 2048,
|
|
422
773
|
timeout: this.timeout,
|
|
423
774
|
reservedConcurrentExecutions: props.reservedConcurrency ?? 10,
|
|
424
|
-
architecture
|
|
775
|
+
architecture,
|
|
425
776
|
vpc: this.vpc,
|
|
426
777
|
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
|
|
427
778
|
securityGroups: [lambdaSg],
|
|
@@ -540,12 +891,23 @@ export class CanopyCmsService extends Construct {
|
|
|
540
891
|
// got AccessDenied from GetSecretValue, exited, and systemd restart-looped
|
|
541
892
|
// it every 5s forever. Nothing flagged it at synth.
|
|
542
893
|
//
|
|
543
|
-
// Deduped
|
|
544
|
-
//
|
|
894
|
+
// Deduped for the benefit of anyone reading this list, NOT of the emitted
|
|
895
|
+
// template: measured against aws-cdk-lib 2.265, `PolicyStatement` already
|
|
896
|
+
// collapses a repeated `resources` entry, so passing the same ARN twice
|
|
897
|
+
// renders one `Resource` either way. Removing this `new Set` changes no
|
|
898
|
+
// synthesized output and breaks no test -- which is worth saying out loud,
|
|
899
|
+
// because the comment here previously claimed the opposite and a reader
|
|
900
|
+
// could reasonably have trusted it while refactoring.
|
|
545
901
|
const secretsArns = [
|
|
546
902
|
...new Set([
|
|
547
903
|
...(props.secretsArns ?? []),
|
|
548
904
|
props.githubTokenSecretArn,
|
|
905
|
+
// The App private key is read by the same `getSecret` call path as
|
|
906
|
+
// the token it replaces, from the same instance profile, so omitting
|
|
907
|
+
// it here would reproduce that AccessDenied restart-loop exactly --
|
|
908
|
+
// on the credential path an adopter reaches for precisely because
|
|
909
|
+
// the token path was not good enough.
|
|
910
|
+
props.githubAppPrivateKeySecretArn,
|
|
549
911
|
props.clerkSecretKeySecretArn,
|
|
550
912
|
].filter((arn) => typeof arn === 'string' && arn.length > 0)),
|
|
551
913
|
];
|
|
@@ -600,9 +962,50 @@ export class CanopyCmsService extends Construct {
|
|
|
600
962
|
if (props.githubTokenSecretArn) {
|
|
601
963
|
envEntries.push(['CANOPYCMS_GITHUB_TOKEN_SECRET_ARN', props.githubTokenSecretArn]);
|
|
602
964
|
}
|
|
965
|
+
// The JSON-field vars need NO IAM change, and that is not an oversight: a
|
|
966
|
+
// field is a key inside a secret's value, not a separately grantable
|
|
967
|
+
// resource. `secretsmanager:GetSecretValue` on the secret -- already
|
|
968
|
+
// granted above from the same ARN prop -- returns the whole document, and
|
|
969
|
+
// the worker picks the field out of it in `getSecret`. Said explicitly
|
|
970
|
+
// because the deduped union above exists precisely because someone
|
|
971
|
+
// previously assumed the two prop families were connected when they were
|
|
972
|
+
// not, and shipped a worker that knew which secret to read and had no
|
|
973
|
+
// permission to read it.
|
|
974
|
+
if (props.githubTokenSecretJsonField) {
|
|
975
|
+
envEntries.push([
|
|
976
|
+
'CANOPYCMS_GITHUB_TOKEN_SECRET_JSON_FIELD',
|
|
977
|
+
props.githubTokenSecretJsonField,
|
|
978
|
+
]);
|
|
979
|
+
}
|
|
980
|
+
// GitHub App credentials. Stamped individually rather than as a group even
|
|
981
|
+
// though `assertGitHubAuthProps` has already established they are all set
|
|
982
|
+
// or all unset: each `if` is then the same shape as every other entry here,
|
|
983
|
+
// and a future prop added to the App set cannot be silently dropped by a
|
|
984
|
+
// condition that names only its siblings.
|
|
985
|
+
if (props.githubAppId) {
|
|
986
|
+
envEntries.push(['CANOPYCMS_GITHUB_APP_ID', props.githubAppId]);
|
|
987
|
+
}
|
|
988
|
+
if (props.githubAppInstallationId) {
|
|
989
|
+
envEntries.push(['CANOPYCMS_GITHUB_APP_INSTALLATION_ID', props.githubAppInstallationId]);
|
|
990
|
+
}
|
|
991
|
+
if (props.githubAppPrivateKeySecretArn) {
|
|
992
|
+
envEntries.push([
|
|
993
|
+
'CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_ARN',
|
|
994
|
+
props.githubAppPrivateKeySecretArn,
|
|
995
|
+
]);
|
|
996
|
+
}
|
|
997
|
+
if (props.githubAppPrivateKeySecretJsonField) {
|
|
998
|
+
envEntries.push([
|
|
999
|
+
'CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD',
|
|
1000
|
+
props.githubAppPrivateKeySecretJsonField,
|
|
1001
|
+
]);
|
|
1002
|
+
}
|
|
603
1003
|
if (props.clerkSecretKeySecretArn) {
|
|
604
1004
|
envEntries.push(['CLERK_SECRET_KEY_SECRET_ARN', props.clerkSecretKeySecretArn]);
|
|
605
1005
|
}
|
|
1006
|
+
if (props.clerkSecretKeySecretJsonField) {
|
|
1007
|
+
envEntries.push(['CLERK_SECRET_KEY_SECRET_JSON_FIELD', props.clerkSecretKeySecretJsonField]);
|
|
1008
|
+
}
|
|
606
1009
|
if (settingsBranch !== undefined) {
|
|
607
1010
|
// Only when explicitly set - an absent prop must keep today's behavior
|
|
608
1011
|
// (the worker falls through to the computed `canopycms-settings-<name>`),
|