@anthusai/papyrus 2.0.0-next.11 → 2.0.0-next.110
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/amplify/auth/resource.js +16 -1
- package/amplify/auth/resource.ts +19 -1
- package/amplify/data/function-handler.js +20 -0
- package/amplify/data/function-handler.ts +40 -0
- package/amplify/data/resource.js +152 -62
- package/amplify/data/resource.ts +158 -62
- package/amplify/data/schema.js +2101 -1987
- package/amplify/data/schema.ts +197 -70
- package/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/automation-view/handler.py +17 -0
- package/amplify/functions/automation-view/resource.js +24 -0
- package/amplify/functions/automation-view/resource.ts +34 -0
- package/amplify/functions/automation-view/schema.js +26 -0
- package/amplify/functions/automation-view/schema.ts +28 -0
- package/amplify/functions/automation-view/wire.js +56 -0
- package/amplify/functions/automation-view/wire.ts +76 -0
- package/amplify/functions/content-actions/handler.py +64 -3
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/editorial-checks/handler.py +2 -2
- package/amplify/functions/editorial-checks/requirements.txt +1 -1
- package/amplify/functions/editorial-checks/resource.js +3 -3
- package/amplify/functions/editorial-checks/resource.ts +3 -3
- package/amplify/functions/editorial-checks/schema.js +38 -8
- package/amplify/functions/editorial-checks/schema.ts +38 -8
- package/amplify/functions/editorial-checks/wire.js +4 -0
- package/amplify/functions/editorial-checks/wire.ts +4 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +29 -0
- package/amplify/functions/monitoring-view/schema.ts +31 -0
- package/amplify/functions/monitoring-view/wire.js +64 -0
- package/amplify/functions/monitoring-view/wire.ts +81 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
- package/amplify/plugins/newsletter/schema.js +57 -0
- package/amplify/plugins/newsletter/schema.ts +62 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +182 -0
- package/amplify/plugins/reader-steering/config.ts +290 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1589 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/proposals.py +487 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +187 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +213 -0
- package/amplify/plugins/reader-steering/schema.js +252 -0
- package/amplify/plugins/reader-steering/schema.ts +254 -0
- package/amplify/plugins/registry.js +4 -0
- package/amplify/plugins/registry.ts +4 -0
- package/amplify/plugins/signup/schema.js +2 -1
- package/amplify/plugins/signup/schema.ts +2 -1
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.js +19 -0
- package/amplify/site-backend-config.ts +45 -1
- package/amplify/site-backend-names.js +4 -2
- package/amplify/site-backend-names.ts +4 -2
- package/amplify/site-backend.js +42 -4
- package/amplify/site-backend.ts +45 -4
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/chat/[...path]/route.ts +17 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/flag/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/app/layout.tsx +5 -1
- package/bin/papyrus-infra.mjs +20 -1
- package/components/automation-panel.tsx +49 -0
- package/components/automation-run-now.tsx +97 -0
- package/components/automation-view.tsx +195 -0
- package/components/editorial-checks-admin-panel.tsx +32 -17
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +422 -0
- package/components/monitoring-panel.tsx +60 -0
- package/components/news-desk-client-provider.tsx +18 -18
- package/components/news-desk-taxonomy-client.ts +28 -8
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-access-gate.tsx +16 -2
- package/components/newsroom-article-editor.tsx +9 -7
- package/components/newsroom-articles-view.tsx +6 -6
- package/components/newsroom-assignment-detail.tsx +6 -6
- package/components/newsroom-board-view.tsx +8 -3
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-ops-overview.tsx +26 -96
- package/components/newsroom-ops-shell.tsx +2 -2
- package/components/newsroom-organizer.css +33 -0
- package/components/newsroom-overview-skeleton.tsx +25 -0
- package/components/newsroom-references-view.tsx +192 -49
- package/components/notification-center.tsx +281 -0
- package/components/papyrus-console-conversation.css +14 -0
- package/components/papyrus-console-conversation.tsx +55 -0
- package/components/papyrus-console-shell.tsx +62 -420
- package/components/papyrus-private-chat-panel.tsx +134 -0
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reader-auth-state.ts +1 -1
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +10 -6
- package/components/reference-priming.tsx +14 -10
- package/components/reference-relevance-review.tsx +3 -3
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +230 -144
- package/infra/amplify-app-shell.js +96 -16
- package/infra/amplify-app-shell.ts +108 -18
- package/infra/cloud-email-config.js +32 -0
- package/infra/cloud-email-config.ts +39 -0
- package/infra/cloud-email.js +57 -0
- package/infra/cloud-email.test.js +69 -0
- package/infra/cloud-email.test.ts +66 -0
- package/infra/cloud-email.ts +60 -0
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +92 -6
- package/infra/site-config.ts +138 -6
- package/lib/automation-client.ts +54 -0
- package/lib/automation-fixture.ts +93 -0
- package/lib/automation-overview.ts +252 -0
- package/lib/category-repository.ts +14 -12
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/console-web-context.ts +1 -1
- package/lib/curation-routes.ts +345 -0
- package/lib/cyclotron-suggestions.ts +4 -4
- package/lib/editorial-checks-client.ts +18 -0
- package/lib/editorial-checks-demo.ts +1 -1
- package/lib/editorial-checks.ts +8 -4
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +83 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +191 -0
- package/lib/news-desk-session.ts +12 -21
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/papyrus-chat-admission.ts +65 -0
- package/lib/papyrus-hosted-chat.ts +217 -0
- package/lib/papyrus-private-chat-client.ts +81 -0
- package/lib/prospectus-session.ts +30 -14
- package/lib/prospectus-sign-in-routes.ts +106 -31
- package/lib/prospectus-site-identity.ts +46 -6
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/lib/reference-review-details.ts +71 -0
- package/lib/reference-review-request.ts +14 -0
- package/lib/relevance-decisions.ts +42 -10
- package/lib/relevance-status.ts +2 -2
- package/lib/review-client.ts +120 -0
- package/lib/review-fixture.ts +45 -0
- package/lib/review-requests.ts +246 -0
- package/lib/semantic-graph.ts +2 -0
- package/lib/shared-chat-admission.ts +182 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin-value.ts +50 -0
- package/lib/site-origin.ts +8 -0
- package/lib/use-reference-review-data.ts +44 -0
- package/middleware.ts +14 -3
- package/package.json +67 -3
- package/routes.manifest.json +238 -2
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.d.mts +1 -0
- package/src/with-papyrus.mjs +17 -1
package/infra/site-config.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
+
import { parseCloudEmail } from "./cloud-email-config.js";
|
|
1
2
|
export const READER_REDIRECT_STATUSES = ["301", "302", "200", "404", "404-200"];
|
|
2
3
|
export function hasCmsApp(config) {
|
|
3
4
|
return config.cms !== undefined;
|
|
4
5
|
}
|
|
6
|
+
export function resolveGithubRepository(config) {
|
|
7
|
+
if (config.github)
|
|
8
|
+
return { owner: config.github.owner, repo: config.github.repo };
|
|
9
|
+
const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
|
|
10
|
+
return { owner, repo };
|
|
11
|
+
}
|
|
12
|
+
/** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
|
|
13
|
+
export function githubOidcSubjects(config, ref) {
|
|
14
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
15
|
+
const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
|
|
16
|
+
const { ownerId, repoId } = config.github ?? {};
|
|
17
|
+
if (ownerId !== undefined && repoId !== undefined) {
|
|
18
|
+
subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
|
|
19
|
+
}
|
|
20
|
+
return subjects;
|
|
21
|
+
}
|
|
5
22
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
6
23
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
7
24
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
8
25
|
const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
9
26
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
10
27
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
28
|
+
const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
|
|
11
29
|
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
12
30
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
13
31
|
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
@@ -18,7 +36,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
18
36
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
19
37
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
20
38
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
21
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "
|
|
39
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_EMAIL_FROM_ADDRESS", "PAPYRUS_EMAIL_IDENTITY_ARN", "PAPYRUS_EMAIL_CONFIGURATION_SET", "PAPYRUS_EMAIL_EVENT_TOPIC_NAME", "PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
22
40
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
23
41
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
24
42
|
"GET/v1/identities/me",
|
|
@@ -43,6 +61,12 @@ function originOfRedirectUrl(value) {
|
|
|
43
61
|
return value;
|
|
44
62
|
}
|
|
45
63
|
}
|
|
64
|
+
export function resolveProspectusSessionSealingParameterName(config) {
|
|
65
|
+
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
66
|
+
}
|
|
67
|
+
export function resolveReaderPollSecretParameterName(config) {
|
|
68
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
69
|
+
}
|
|
46
70
|
export function resolveRevalidateSecretParameterName(config) {
|
|
47
71
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
48
72
|
}
|
|
@@ -168,6 +192,9 @@ export function resolveCmsHostName(config) {
|
|
|
168
192
|
return undefined;
|
|
169
193
|
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
170
194
|
}
|
|
195
|
+
export function resolveSiteOrigin(config) {
|
|
196
|
+
return config.cms.siteOrigin;
|
|
197
|
+
}
|
|
171
198
|
export function resolveStagingDomainName(config) {
|
|
172
199
|
if (!isStagingEnabled(config))
|
|
173
200
|
return undefined;
|
|
@@ -199,7 +226,7 @@ function parseStagingPreview(value) {
|
|
|
199
226
|
}
|
|
200
227
|
function parseCms(value) {
|
|
201
228
|
const cmsRecord = requireObject(value, "cms");
|
|
202
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
229
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
203
230
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
204
231
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
205
232
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -210,6 +237,12 @@ function parseCms(value) {
|
|
|
210
237
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
211
238
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
212
239
|
}
|
|
240
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
241
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
242
|
+
}
|
|
243
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
244
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
245
|
+
}
|
|
213
246
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
214
247
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
215
248
|
}
|
|
@@ -217,6 +250,7 @@ function parseCms(value) {
|
|
|
217
250
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
218
251
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
219
252
|
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
253
|
+
siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
|
|
220
254
|
staging: cmsRecord.staging,
|
|
221
255
|
cognitoDomainPrefix: requireString(cmsRecord.cognitoDomainPrefix, "cms.cognitoDomainPrefix", COGNITO_DOMAIN_PREFIX_PATTERN, "lowercase letters, digits and hyphens; globally unique per region"),
|
|
222
256
|
applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix,
|
|
@@ -226,6 +260,7 @@ function parseCms(value) {
|
|
|
226
260
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
227
261
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
228
262
|
deployStackInBuild: cmsRecord.deployStackInBuild,
|
|
263
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll,
|
|
229
264
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
230
265
|
prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
|
|
231
266
|
};
|
|
@@ -235,9 +270,16 @@ function parseCms(value) {
|
|
|
235
270
|
}
|
|
236
271
|
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
237
272
|
if (key in cms.environment) {
|
|
238
|
-
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
|
|
273
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn and the public site origin comes from the site host");
|
|
239
274
|
}
|
|
240
275
|
}
|
|
276
|
+
const privateChatEnabled = cms.environment.PAPYRUS_PRIVATE_CHAT_ENABLED;
|
|
277
|
+
if (privateChatEnabled !== undefined && privateChatEnabled !== "true" && privateChatEnabled !== "false") {
|
|
278
|
+
fail("cms.environment.PAPYRUS_PRIVATE_CHAT_ENABLED", "must be the string \"true\" or \"false\"");
|
|
279
|
+
}
|
|
280
|
+
if (cms.siteOrigin && cms.domainName) {
|
|
281
|
+
fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
|
|
282
|
+
}
|
|
241
283
|
if (cms.staging === false && cms.stagingDomainName) {
|
|
242
284
|
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
243
285
|
}
|
|
@@ -300,9 +342,20 @@ function validateOauthRedirectUrls(config) {
|
|
|
300
342
|
}
|
|
301
343
|
}
|
|
302
344
|
}
|
|
345
|
+
function parseAws(value) {
|
|
346
|
+
if (value === undefined)
|
|
347
|
+
return undefined;
|
|
348
|
+
const record = requireObject(value, "aws");
|
|
349
|
+
rejectUnknownKeys(record, ["profile", "accountId", "region"], "aws");
|
|
350
|
+
return {
|
|
351
|
+
profile: optionalString(record.profile, "aws.profile", /^[A-Za-z0-9_.-]+$/, "AWS CLI profile name"),
|
|
352
|
+
accountId: optionalString(record.accountId, "aws.accountId", /^\d{12}$/, "12-digit account id"),
|
|
353
|
+
region: optionalString(record.region, "aws.region", /^[a-z]{2}(-[a-z]+)+-\d$/, "AWS region such as us-east-1"),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
303
356
|
export function parseSiteConfig(raw) {
|
|
304
357
|
const record = requireObject(raw, "site");
|
|
305
|
-
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
358
|
+
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "email", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"], "site");
|
|
306
359
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
307
360
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
308
361
|
const repositoryAccessTokenSecretArn = record.repositoryAccessTokenSecretArn === undefined
|
|
@@ -315,6 +368,7 @@ export function parseSiteConfig(raw) {
|
|
|
315
368
|
}
|
|
316
369
|
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
317
370
|
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
371
|
+
const aws = parseAws(record.aws);
|
|
318
372
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
319
373
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
320
374
|
const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
|
|
@@ -352,12 +406,15 @@ export function parseSiteConfig(raw) {
|
|
|
352
406
|
if (cms?.stagingPreview !== undefined && cms.staging === false) {
|
|
353
407
|
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
354
408
|
}
|
|
409
|
+
const autoDeploy = record.autoDeploy ?? true;
|
|
410
|
+
if (typeof autoDeploy !== "boolean")
|
|
411
|
+
fail("autoDeploy", "must be a boolean");
|
|
355
412
|
let github;
|
|
356
413
|
if (record.github !== undefined) {
|
|
357
414
|
if (cms === undefined)
|
|
358
415
|
fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
359
416
|
const githubRecord = requireObject(record.github, "github");
|
|
360
|
-
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
417
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
|
|
361
418
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
362
419
|
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
363
420
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
@@ -378,7 +435,18 @@ export function parseSiteConfig(raw) {
|
|
|
378
435
|
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
379
436
|
if (typeof ciCanDeployInfra !== "boolean")
|
|
380
437
|
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
381
|
-
|
|
438
|
+
const optionalId = (value, name) => {
|
|
439
|
+
if (value === undefined)
|
|
440
|
+
return undefined;
|
|
441
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
|
|
442
|
+
fail(name, "must be a positive integer (GitHub numeric id)");
|
|
443
|
+
return value;
|
|
444
|
+
};
|
|
445
|
+
const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
|
|
446
|
+
const repoId = optionalId(githubRecord.repoId, "github.repoId");
|
|
447
|
+
if ((ownerId === undefined) !== (repoId === undefined))
|
|
448
|
+
fail("github", "ownerId and repoId must be set together");
|
|
449
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
|
|
382
450
|
}
|
|
383
451
|
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
384
452
|
if (hasDomain && hostedZoneId === undefined) {
|
|
@@ -395,13 +463,31 @@ export function parseSiteConfig(raw) {
|
|
|
395
463
|
frontend,
|
|
396
464
|
hostedZoneId,
|
|
397
465
|
stackName,
|
|
466
|
+
aws,
|
|
398
467
|
cms,
|
|
468
|
+
email: parseCloudEmail(record.email, cms !== undefined, siteId),
|
|
399
469
|
reader,
|
|
400
470
|
papyrusVersion,
|
|
401
471
|
storagePreviewPrefix,
|
|
472
|
+
autoDeploy: autoDeploy,
|
|
402
473
|
github,
|
|
474
|
+
...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
|
|
403
475
|
};
|
|
404
476
|
if (hasCmsApp(config))
|
|
405
477
|
validateOauthRedirectUrls(config);
|
|
406
478
|
return config;
|
|
407
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The CDK environment for a site's app-shell stack, taken only from the site's own `aws` block. There is
|
|
482
|
+
* deliberately no default account: a deploy that guesses an account can land in, or fail against, the wrong
|
|
483
|
+
* one.
|
|
484
|
+
*/
|
|
485
|
+
export function resolveDeployEnvironment(config) {
|
|
486
|
+
const account = config.aws?.accountId;
|
|
487
|
+
const region = config.aws?.region;
|
|
488
|
+
const missing = [account ? "" : "aws.accountId", region ? "" : "aws.region"].filter(Boolean);
|
|
489
|
+
if (!account || !region) {
|
|
490
|
+
throw new Error(`site "${config.siteId}" does not say where it deploys: add ${missing.join(" and ")} to its site.json, e.g. "aws": { "accountId": "<12-digit account>", "region": "us-east-1" }. No account is assumed.`);
|
|
491
|
+
}
|
|
492
|
+
return { account, region };
|
|
493
|
+
}
|
package/infra/site-config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseCloudEmail, type CloudEmailConfig } from "./cloud-email-config";
|
|
2
|
+
|
|
1
3
|
export type CmsRedirect = {
|
|
2
4
|
source: string;
|
|
3
5
|
status: 301 | 302;
|
|
@@ -32,6 +34,8 @@ export type CmsConfig = {
|
|
|
32
34
|
appName?: string;
|
|
33
35
|
domainName?: string;
|
|
34
36
|
domainPrefix?: string;
|
|
37
|
+
/** Public https origin of a site whose host is served by its own edge (no Amplify domain association). */
|
|
38
|
+
siteOrigin?: string;
|
|
35
39
|
staging?: boolean;
|
|
36
40
|
stagingPreview?: StagingPreviewConfig;
|
|
37
41
|
cognitoDomainPrefix: string;
|
|
@@ -42,6 +46,12 @@ export type CmsConfig = {
|
|
|
42
46
|
stagingDomainPrefix?: string;
|
|
43
47
|
redirects?: CmsRedirect[];
|
|
44
48
|
prospectusAccountsApiStageArn?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The site is a public-reader site that polls Prospectus for retired reader subjects. The template then generates a
|
|
51
|
+
* shared secret that the scheduled snapshot run presents to the CMS route `/api/reader/retired` (requires
|
|
52
|
+
* `prospectusAccountsApiStageArn`; docs/reader-steering.md).
|
|
53
|
+
*/
|
|
54
|
+
readerRetirementPoll?: boolean;
|
|
45
55
|
deployStackInBuild?: boolean;
|
|
46
56
|
};
|
|
47
57
|
|
|
@@ -53,7 +63,10 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
53
63
|
frontend: "pretext" | "markus-static";
|
|
54
64
|
hostedZoneId?: string;
|
|
55
65
|
stackName?: string;
|
|
66
|
+
aws?: SiteAwsConfig;
|
|
56
67
|
cms?: CmsConfig;
|
|
68
|
+
/** Required for cloud CMS sites; independent of newsletter feature enablement. */
|
|
69
|
+
email?: CloudEmailConfig;
|
|
57
70
|
reader?: {
|
|
58
71
|
appName?: string;
|
|
59
72
|
domainName?: string;
|
|
@@ -69,20 +82,67 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
69
82
|
};
|
|
70
83
|
papyrusVersion: string;
|
|
71
84
|
storagePreviewPrefix?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The scheduled cloud knowledge run for this publication. The app-shell stack ignores it; it is validated by
|
|
87
|
+
* infra/scheduled-jobs/lib/knowledge-run-config.ts, which owns the schema.
|
|
88
|
+
*/
|
|
89
|
+
knowledgeRun?: unknown;
|
|
90
|
+
/**
|
|
91
|
+
* Merge-to-main deploy from GitHub Actions through OIDC. When true (the default) the stack creates the
|
|
92
|
+
* `<siteId>-github-deploy` role and turns off Amplify's own auto-build on the production branches, so the
|
|
93
|
+
* public repository's workflow is the only trigger. Set false for a site whose repository is private:
|
|
94
|
+
* private repositories get no Actions minutes, so Amplify's own repository-connected auto-build stays on.
|
|
95
|
+
*/
|
|
96
|
+
autoDeploy: boolean;
|
|
72
97
|
github?: {
|
|
73
98
|
owner: string;
|
|
74
99
|
repo: string;
|
|
75
100
|
branches: string[];
|
|
76
101
|
ciCanDeployInfra: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* GitHub's numeric ids of the owner and the repository. Repositories with the immutable OIDC subject
|
|
104
|
+
* (`repo:<owner>@<ownerId>/<repo>@<repoId>:ref:...`, see `gh api repos/<owner>/<repo>/actions/oidc/customization/sub`)
|
|
105
|
+
* present that form instead of `repo:<owner>/<repo>:ref:...`; with both ids set, roles trust both forms.
|
|
106
|
+
*/
|
|
107
|
+
ownerId?: number;
|
|
108
|
+
repoId?: number;
|
|
77
109
|
};
|
|
78
110
|
};
|
|
79
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Where the site's AWS resources live. `bin/app-shell.ts` deploys the stack into exactly this account and
|
|
114
|
+
* region (see `resolveDeployEnvironment`), and `papyrus site google-secrets` asserts `accountId` with STS
|
|
115
|
+
* before it writes anything.
|
|
116
|
+
*/
|
|
117
|
+
export type SiteAwsConfig = {
|
|
118
|
+
profile?: string;
|
|
119
|
+
accountId?: string;
|
|
120
|
+
region?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
80
123
|
export type CmsSiteConfig = AmplifyAppShellSiteConfig & { cms: CmsConfig };
|
|
81
124
|
|
|
82
125
|
export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteConfig {
|
|
83
126
|
return config.cms !== undefined;
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
export function resolveGithubRepository(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">): { owner: string; repo: string } {
|
|
130
|
+
if (config.github) return { owner: config.github.owner, repo: config.github.repo };
|
|
131
|
+
const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
|
|
132
|
+
return { owner, repo };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
|
|
136
|
+
export function githubOidcSubjects(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">, ref: string): string[] {
|
|
137
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
138
|
+
const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
|
|
139
|
+
const { ownerId, repoId } = config.github ?? {};
|
|
140
|
+
if (ownerId !== undefined && repoId !== undefined) {
|
|
141
|
+
subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
|
|
142
|
+
}
|
|
143
|
+
return subjects;
|
|
144
|
+
}
|
|
145
|
+
|
|
86
146
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
87
147
|
|
|
88
148
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
@@ -90,6 +150,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
|
|
|
90
150
|
const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
91
151
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
92
152
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
153
|
+
const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
|
|
93
154
|
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
94
155
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
95
156
|
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
@@ -100,7 +161,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
100
161
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
101
162
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
102
163
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
103
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "
|
|
164
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_EMAIL_FROM_ADDRESS", "PAPYRUS_EMAIL_IDENTITY_ARN", "PAPYRUS_EMAIL_CONFIGURATION_SET", "PAPYRUS_EMAIL_EVENT_TOPIC_NAME", "PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
104
165
|
|
|
105
166
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
106
167
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
@@ -127,6 +188,14 @@ function originOfRedirectUrl(value: string): string {
|
|
|
127
188
|
}
|
|
128
189
|
}
|
|
129
190
|
|
|
191
|
+
export function resolveProspectusSessionSealingParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
192
|
+
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveReaderPollSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
196
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
131
200
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
132
201
|
}
|
|
@@ -251,6 +320,10 @@ export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string |
|
|
|
251
320
|
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
252
321
|
}
|
|
253
322
|
|
|
323
|
+
export function resolveSiteOrigin(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
324
|
+
return config.cms.siteOrigin;
|
|
325
|
+
}
|
|
326
|
+
|
|
254
327
|
export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
255
328
|
if (!isStagingEnabled(config)) return undefined;
|
|
256
329
|
if (config.cms.domainPrefix) {
|
|
@@ -282,7 +355,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
|
282
355
|
|
|
283
356
|
function parseCms(value: unknown): CmsConfig {
|
|
284
357
|
const cmsRecord = requireObject(value, "cms");
|
|
285
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
358
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
286
359
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
287
360
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
288
361
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -293,6 +366,12 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
293
366
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
294
367
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
295
368
|
}
|
|
369
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
370
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
371
|
+
}
|
|
372
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
373
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
374
|
+
}
|
|
296
375
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
297
376
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
298
377
|
}
|
|
@@ -300,6 +379,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
300
379
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
301
380
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
302
381
|
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
382
|
+
siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
|
|
303
383
|
staging: cmsRecord.staging as boolean | undefined,
|
|
304
384
|
cognitoDomainPrefix: requireString(
|
|
305
385
|
cmsRecord.cognitoDomainPrefix,
|
|
@@ -314,6 +394,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
314
394
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
315
395
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
316
396
|
deployStackInBuild: cmsRecord.deployStackInBuild as boolean | undefined,
|
|
397
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll as boolean | undefined,
|
|
317
398
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
318
399
|
prospectusAccountsApiStageArn: optionalString(
|
|
319
400
|
cmsRecord.prospectusAccountsApiStageArn,
|
|
@@ -329,10 +410,17 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
329
410
|
}
|
|
330
411
|
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
331
412
|
if (key in cms.environment) {
|
|
332
|
-
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
|
|
413
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn and the public site origin comes from the site host");
|
|
333
414
|
}
|
|
334
415
|
}
|
|
416
|
+
const privateChatEnabled = cms.environment.PAPYRUS_PRIVATE_CHAT_ENABLED;
|
|
417
|
+
if (privateChatEnabled !== undefined && privateChatEnabled !== "true" && privateChatEnabled !== "false") {
|
|
418
|
+
fail("cms.environment.PAPYRUS_PRIVATE_CHAT_ENABLED", "must be the string \"true\" or \"false\"");
|
|
419
|
+
}
|
|
335
420
|
|
|
421
|
+
if (cms.siteOrigin && cms.domainName) {
|
|
422
|
+
fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
|
|
423
|
+
}
|
|
336
424
|
if (cms.staging === false && cms.stagingDomainName) {
|
|
337
425
|
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
338
426
|
}
|
|
@@ -395,11 +483,22 @@ function validateOauthRedirectUrls(config: CmsSiteConfig): void {
|
|
|
395
483
|
}
|
|
396
484
|
}
|
|
397
485
|
|
|
486
|
+
function parseAws(value: unknown): SiteAwsConfig | undefined {
|
|
487
|
+
if (value === undefined) return undefined;
|
|
488
|
+
const record = requireObject(value, "aws");
|
|
489
|
+
rejectUnknownKeys(record, ["profile", "accountId", "region"], "aws");
|
|
490
|
+
return {
|
|
491
|
+
profile: optionalString(record.profile, "aws.profile", /^[A-Za-z0-9_.-]+$/, "AWS CLI profile name"),
|
|
492
|
+
accountId: optionalString(record.accountId, "aws.accountId", /^\d{12}$/, "12-digit account id"),
|
|
493
|
+
region: optionalString(record.region, "aws.region", /^[a-z]{2}(-[a-z]+)+-\d$/, "AWS region such as us-east-1"),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
398
497
|
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
399
498
|
const record = requireObject(raw, "site");
|
|
400
499
|
rejectUnknownKeys(
|
|
401
500
|
record,
|
|
402
|
-
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
501
|
+
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "email", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"],
|
|
403
502
|
"site",
|
|
404
503
|
);
|
|
405
504
|
|
|
@@ -415,6 +514,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
415
514
|
}
|
|
416
515
|
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
417
516
|
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
517
|
+
const aws = parseAws(record.aws);
|
|
418
518
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
419
519
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
420
520
|
|
|
@@ -452,11 +552,14 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
452
552
|
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
453
553
|
}
|
|
454
554
|
|
|
555
|
+
const autoDeploy = record.autoDeploy ?? true;
|
|
556
|
+
if (typeof autoDeploy !== "boolean") fail("autoDeploy", "must be a boolean");
|
|
557
|
+
|
|
455
558
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
456
559
|
if (record.github !== undefined) {
|
|
457
560
|
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
458
561
|
const githubRecord = requireObject(record.github, "github");
|
|
459
|
-
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
562
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
|
|
460
563
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
461
564
|
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
462
565
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
@@ -476,7 +579,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
476
579
|
}
|
|
477
580
|
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
478
581
|
if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
|
|
479
|
-
|
|
582
|
+
const optionalId = (value: unknown, name: string): number | undefined => {
|
|
583
|
+
if (value === undefined) return undefined;
|
|
584
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) fail(name, "must be a positive integer (GitHub numeric id)");
|
|
585
|
+
return value as number;
|
|
586
|
+
};
|
|
587
|
+
const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
|
|
588
|
+
const repoId = optionalId(githubRecord.repoId, "github.repoId");
|
|
589
|
+
if ((ownerId === undefined) !== (repoId === undefined)) fail("github", "ownerId and repoId must be set together");
|
|
590
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
|
|
480
591
|
}
|
|
481
592
|
|
|
482
593
|
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
@@ -495,14 +606,35 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
495
606
|
frontend,
|
|
496
607
|
hostedZoneId,
|
|
497
608
|
stackName,
|
|
609
|
+
aws,
|
|
498
610
|
cms,
|
|
611
|
+
email: parseCloudEmail(record.email, cms !== undefined, siteId),
|
|
499
612
|
reader,
|
|
500
613
|
papyrusVersion,
|
|
501
614
|
storagePreviewPrefix,
|
|
615
|
+
autoDeploy: autoDeploy as boolean,
|
|
502
616
|
github,
|
|
617
|
+
...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
|
|
503
618
|
};
|
|
504
619
|
|
|
505
620
|
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
|
506
621
|
|
|
507
622
|
return config;
|
|
508
623
|
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The CDK environment for a site's app-shell stack, taken only from the site's own `aws` block. There is
|
|
627
|
+
* deliberately no default account: a deploy that guesses an account can land in, or fail against, the wrong
|
|
628
|
+
* one.
|
|
629
|
+
*/
|
|
630
|
+
export function resolveDeployEnvironment(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "aws">): { account: string; region: string } {
|
|
631
|
+
const account = config.aws?.accountId;
|
|
632
|
+
const region = config.aws?.region;
|
|
633
|
+
const missing = [account ? "" : "aws.accountId", region ? "" : "aws.region"].filter(Boolean);
|
|
634
|
+
if (!account || !region) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`site "${config.siteId}" does not say where it deploys: add ${missing.join(" and ")} to its site.json, e.g. "aws": { "accountId": "<12-digit account>", "region": "us-east-1" }. No account is assumed.`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return { account, region };
|
|
640
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { AutomationResult, RunNowResult } from "./automation-overview";
|
|
5
|
+
|
|
6
|
+
type GraphQLResult = { data?: { automationOverview?: unknown }; errors?: { message?: string; errorType?: string }[] };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads the automations through the site's own authenticated API (user pool, editor or admin).
|
|
10
|
+
* The browser holds no table name, key or secret: the function behind the query does the reads.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadAutomationOverview(): Promise<AutomationResult> {
|
|
13
|
+
const client = generateClient({ authMode: "userPool" });
|
|
14
|
+
let result: GraphQLResult;
|
|
15
|
+
try {
|
|
16
|
+
result = (await client.graphql({
|
|
17
|
+
query: `query AutomationOverview($automationId: String) { automationOverview(automationId: $automationId) }`,
|
|
18
|
+
variables: {},
|
|
19
|
+
})) as GraphQLResult;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Amplify throws the response when AppSync returns errors.
|
|
22
|
+
result = (error && typeof error === "object" && "errors" in error ? error : { errors: [{ message: "failed" }] }) as GraphQLResult;
|
|
23
|
+
}
|
|
24
|
+
const refused = result.errors?.some((entry) => /unauthorized|not authorized|forbidden/i.test(`${entry.errorType ?? ""} ${entry.message ?? ""}`));
|
|
25
|
+
if (refused) return { error: "forbidden" };
|
|
26
|
+
if (result.errors?.length || result.data?.automationOverview === undefined || result.data.automationOverview === null) return { state: "unavailable" };
|
|
27
|
+
const raw = result.data.automationOverview;
|
|
28
|
+
return (typeof raw === "string" ? JSON.parse(raw) : raw) as AutomationResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RunGraphQLResult = { data?: { runAutomation?: unknown }; errors?: { message?: string; errorType?: string }[] };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Starts a run of the automation's procedure now, as the signed-in editor or admin. The function behind the
|
|
35
|
+
* mutation checks the groups again, refuses a disabled automation or one at its limit, and writes the row that
|
|
36
|
+
* links the run to the automation's history. `input` is sent as JSON text; null uses the record's own input.
|
|
37
|
+
*/
|
|
38
|
+
export async function runAutomationNow(automationId: string, input: Record<string, unknown> | null): Promise<RunNowResult> {
|
|
39
|
+
const client = generateClient({ authMode: "userPool" });
|
|
40
|
+
let result: RunGraphQLResult;
|
|
41
|
+
try {
|
|
42
|
+
result = (await client.graphql({
|
|
43
|
+
query: `mutation RunAutomation($automationId: String!, $input: AWSJSON) { runAutomation(automationId: $automationId, input: $input) }`,
|
|
44
|
+
variables: { automationId, input: input === null ? null : JSON.stringify(input) },
|
|
45
|
+
})) as RunGraphQLResult;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
result = (error && typeof error === "object" && "errors" in error ? error : { errors: [{ message: "failed" }] }) as RunGraphQLResult;
|
|
48
|
+
}
|
|
49
|
+
const refused = result.errors?.some((entry) => /unauthorized|not authorized|forbidden/i.test(`${entry.errorType ?? ""} ${entry.message ?? ""}`));
|
|
50
|
+
if (refused) return { error: "forbidden" };
|
|
51
|
+
if (result.errors?.length || result.data?.runAutomation === undefined || result.data.runAutomation === null) return { state: "unavailable" };
|
|
52
|
+
const raw = result.data.runAutomation;
|
|
53
|
+
return (typeof raw === "string" ? JSON.parse(raw) : raw) as RunNowResult;
|
|
54
|
+
}
|