@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.40
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/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +45 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +212 -50
- package/infra/amplify-app-shell.ts +240 -53
- package/infra/build-specs.js +14 -5
- package/infra/build-specs.ts +17 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +167 -10
- package/infra/site-config.ts +196 -14
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
package/infra/site-config.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
1
2
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2
3
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
3
4
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
4
5
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
6
|
+
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
5
7
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
8
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
6
9
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
7
10
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
11
|
+
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
12
|
+
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
13
|
+
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
14
|
+
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
15
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
|
|
16
|
+
function originOfRedirectUrl(value) {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(value).origin;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function resolveRevalidateSecretParameterName(config) {
|
|
25
|
+
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
26
|
+
}
|
|
8
27
|
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
9
28
|
function fail(field, problem) {
|
|
10
29
|
throw new Error(`site.json: ${field} ${problem}`);
|
|
@@ -43,18 +62,55 @@ function rejectUnknownKeys(record, allowed, field) {
|
|
|
43
62
|
fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
|
|
44
63
|
}
|
|
45
64
|
}
|
|
65
|
+
function parseRedirects(value) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
69
|
+
fail("cms.redirects", "must be a non-empty array");
|
|
70
|
+
return value.map((entry, index) => {
|
|
71
|
+
const field = `cms.redirects[${index}]`;
|
|
72
|
+
const record = requireObject(entry, field);
|
|
73
|
+
rejectUnknownKeys(record, ["source", "status"], field);
|
|
74
|
+
const source = requireString(record.source, `${field}.source`, HOST_NAME_PATTERN, "host name without scheme or path");
|
|
75
|
+
const status = record.status ?? 301;
|
|
76
|
+
if (status !== 301 && status !== 302)
|
|
77
|
+
fail(`${field}.status`, `must be 301 or 302, got ${JSON.stringify(status)}`);
|
|
78
|
+
return { source, status };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function resolveRedirectSubDomainPrefix(rootDomain, source) {
|
|
82
|
+
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
83
|
+
}
|
|
84
|
+
export function isStagingEnabled(config) {
|
|
85
|
+
return config.cms.staging !== false;
|
|
86
|
+
}
|
|
87
|
+
export function resolveCmsHostName(config) {
|
|
88
|
+
if (!config.cms.domainName)
|
|
89
|
+
return undefined;
|
|
90
|
+
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
91
|
+
}
|
|
46
92
|
export function resolveStagingDomainName(config) {
|
|
93
|
+
if (!isStagingEnabled(config))
|
|
94
|
+
return undefined;
|
|
95
|
+
if (config.cms.domainPrefix) {
|
|
96
|
+
return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
|
|
97
|
+
}
|
|
47
98
|
if (config.cms.stagingDomainName)
|
|
48
99
|
return config.cms.stagingDomainName;
|
|
100
|
+
if (!config.cms.domainName)
|
|
101
|
+
return undefined;
|
|
49
102
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
50
103
|
return ["staging", ...rest].join(".");
|
|
51
104
|
}
|
|
105
|
+
export function resolveStackName(config) {
|
|
106
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
107
|
+
}
|
|
52
108
|
export function resolveStoragePreviewPrefix(config) {
|
|
53
109
|
return config.storagePreviewPrefix ?? "preview/";
|
|
54
110
|
}
|
|
55
111
|
export function parseSiteConfig(raw) {
|
|
56
112
|
const record = requireObject(raw, "site");
|
|
57
|
-
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix"], "site");
|
|
113
|
+
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
58
114
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
59
115
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
60
116
|
const brand = requireString(record.brand, "brand");
|
|
@@ -62,22 +118,86 @@ export function parseSiteConfig(raw) {
|
|
|
62
118
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
63
119
|
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
64
120
|
}
|
|
65
|
-
const hostedZoneId =
|
|
121
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
122
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
66
123
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
67
124
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
68
125
|
const cmsRecord = requireObject(record.cms, "cms");
|
|
69
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
126
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
70
127
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
71
128
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
72
129
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
73
130
|
}
|
|
131
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
132
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
133
|
+
}
|
|
134
|
+
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
135
|
+
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
136
|
+
}
|
|
74
137
|
const cms = {
|
|
75
138
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
76
|
-
domainName:
|
|
139
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
140
|
+
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
141
|
+
staging: cmsRecord.staging,
|
|
142
|
+
cognitoDomainPrefix: requireString(cmsRecord.cognitoDomainPrefix, "cms.cognitoDomainPrefix", COGNITO_DOMAIN_PREFIX_PATTERN, "lowercase letters, digits and hyphens; globally unique per region"),
|
|
143
|
+
applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix,
|
|
77
144
|
buildComputeType: buildComputeType,
|
|
78
145
|
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
79
146
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
147
|
+
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
148
|
+
redirects: parseRedirects(cmsRecord.redirects),
|
|
80
149
|
};
|
|
150
|
+
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
151
|
+
if (reservedWord) {
|
|
152
|
+
fail("cms.cognitoDomainPrefix", `must not contain "${reservedWord}" (Cognito reserves it): ${JSON.stringify(cms.cognitoDomainPrefix)}`);
|
|
153
|
+
}
|
|
154
|
+
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
155
|
+
if (key in cms.environment) {
|
|
156
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix and the revalidation secret parameter is derived from siteId");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
160
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
161
|
+
}
|
|
162
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
163
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
164
|
+
}
|
|
165
|
+
if (cms.domainPrefix && !cms.domainName) {
|
|
166
|
+
fail("cms.domainPrefix", "requires cms.domainName (the root domain)");
|
|
167
|
+
}
|
|
168
|
+
if (cms.stagingDomainPrefix && !cms.domainPrefix) {
|
|
169
|
+
fail("cms.stagingDomainPrefix", "requires cms.domainPrefix");
|
|
170
|
+
}
|
|
171
|
+
if (cms.domainPrefix && cms.stagingDomainName) {
|
|
172
|
+
fail("cms.stagingDomainName", "must not be set with cms.domainPrefix: use cms.stagingDomainPrefix");
|
|
173
|
+
}
|
|
174
|
+
if (cms.staging === false && cms.stagingDomainPrefix) {
|
|
175
|
+
fail("cms.stagingDomainPrefix", "must not be set when cms.staging is false");
|
|
176
|
+
}
|
|
177
|
+
if (cms.domainPrefix && cms.staging !== false && !cms.stagingDomainPrefix) {
|
|
178
|
+
fail("cms.stagingDomainPrefix", "is required when cms.domainPrefix is set and staging is enabled: the staging host is never derived under a shared root domain");
|
|
179
|
+
}
|
|
180
|
+
if (cms.domainPrefix && cms.stagingDomainPrefix === cms.domainPrefix) {
|
|
181
|
+
fail("cms.stagingDomainPrefix", "must differ from cms.domainPrefix");
|
|
182
|
+
}
|
|
183
|
+
if (cms.redirects) {
|
|
184
|
+
if (!cms.domainPrefix)
|
|
185
|
+
fail("cms.redirects", "requires cms.domainPrefix (redirects target the primary host under a shared root domain)");
|
|
186
|
+
cms.redirects.forEach((redirect, index) => {
|
|
187
|
+
const field = `cms.redirects[${index}].source`;
|
|
188
|
+
const root = cms.domainName;
|
|
189
|
+
if (redirect.source !== root && !(redirect.source.endsWith(`.${root}`) && DOMAIN_PREFIX_PATTERN.test(redirect.source.slice(0, -root.length - 1)))) {
|
|
190
|
+
fail(field, `must be the root domain ${root} or a single label under it: ${JSON.stringify(redirect.source)}`);
|
|
191
|
+
}
|
|
192
|
+
if (redirect.source === resolveCmsHostName({ cms }))
|
|
193
|
+
fail(field, "must differ from the primary host");
|
|
194
|
+
if (redirect.source === resolveStagingDomainName({ cms }))
|
|
195
|
+
fail(field, "must differ from the staging host");
|
|
196
|
+
});
|
|
197
|
+
if (new Set(cms.redirects.map((redirect) => redirect.source)).size !== cms.redirects.length) {
|
|
198
|
+
fail("cms.redirects", "must not repeat a source");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
81
201
|
let reader;
|
|
82
202
|
if (record.reader !== undefined) {
|
|
83
203
|
if (frontend !== "markus-static")
|
|
@@ -86,7 +206,7 @@ export function parseSiteConfig(raw) {
|
|
|
86
206
|
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
87
207
|
reader = {
|
|
88
208
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
89
|
-
domainName:
|
|
209
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
90
210
|
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
91
211
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
92
212
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
@@ -96,29 +216,66 @@ export function parseSiteConfig(raw) {
|
|
|
96
216
|
else if (frontend === "markus-static") {
|
|
97
217
|
fail("reader", `is required when frontend is "markus-static"`);
|
|
98
218
|
}
|
|
219
|
+
let github;
|
|
220
|
+
if (record.github !== undefined) {
|
|
221
|
+
const githubRecord = requireObject(record.github, "github");
|
|
222
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
223
|
+
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
224
|
+
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
225
|
+
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
226
|
+
fail("github", `owner/repo must match repository ${repository}`);
|
|
227
|
+
}
|
|
228
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
229
|
+
if (githubRecord.branches !== undefined) {
|
|
230
|
+
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
231
|
+
fail("github.branches", "must be a non-empty array of branch names");
|
|
232
|
+
}
|
|
233
|
+
branches = githubRecord.branches.map((branch, index) => {
|
|
234
|
+
if (typeof branch === "string" && /[*?]/.test(branch)) {
|
|
235
|
+
fail(`github.branches[${index}]`, `must not contain wildcards: ${JSON.stringify(branch)}`);
|
|
236
|
+
}
|
|
237
|
+
return requireString(branch, `github.branches[${index}]`, BRANCH_NAME_PATTERN);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
241
|
+
if (typeof ciCanDeployInfra !== "boolean")
|
|
242
|
+
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
243
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
|
|
244
|
+
}
|
|
245
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
246
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
247
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
248
|
+
}
|
|
249
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
250
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
251
|
+
}
|
|
99
252
|
const config = {
|
|
100
253
|
siteId,
|
|
101
254
|
repository,
|
|
102
255
|
brand,
|
|
103
256
|
frontend,
|
|
104
257
|
hostedZoneId,
|
|
258
|
+
stackName,
|
|
105
259
|
cms,
|
|
106
260
|
reader,
|
|
107
261
|
papyrusVersion,
|
|
108
262
|
storagePreviewPrefix,
|
|
263
|
+
github,
|
|
109
264
|
};
|
|
110
265
|
const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
111
266
|
if (redirectValue === undefined)
|
|
112
267
|
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
113
268
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
269
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
270
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
114
271
|
const requiredOrigins = [
|
|
115
|
-
`https://${
|
|
116
|
-
`https://${
|
|
117
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
272
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
273
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
274
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
118
275
|
];
|
|
119
276
|
for (const origin of requiredOrigins) {
|
|
120
|
-
if (!
|
|
121
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
277
|
+
if (!listedOrigins.has(origin)) {
|
|
278
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
122
279
|
}
|
|
123
280
|
}
|
|
124
281
|
return config;
|
package/infra/site-config.ts
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
|
+
export type CmsRedirect = {
|
|
2
|
+
source: string;
|
|
3
|
+
status: 301 | 302;
|
|
4
|
+
};
|
|
5
|
+
|
|
1
6
|
export type AmplifyAppShellSiteConfig = {
|
|
2
7
|
siteId: string;
|
|
3
8
|
repository: string;
|
|
4
9
|
brand: string;
|
|
5
10
|
frontend: "pretext" | "markus-static";
|
|
6
|
-
hostedZoneId
|
|
11
|
+
hostedZoneId?: string;
|
|
12
|
+
stackName?: string;
|
|
7
13
|
cms: {
|
|
8
14
|
appName?: string;
|
|
9
|
-
domainName
|
|
15
|
+
domainName?: string;
|
|
16
|
+
domainPrefix?: string;
|
|
17
|
+
staging?: boolean;
|
|
18
|
+
cognitoDomainPrefix: string;
|
|
19
|
+
applyCognitoDomainPrefix?: boolean;
|
|
10
20
|
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
11
21
|
environment: Record<string, string>;
|
|
12
22
|
stagingDomainName?: string;
|
|
23
|
+
stagingDomainPrefix?: string;
|
|
24
|
+
redirects?: CmsRedirect[];
|
|
13
25
|
};
|
|
14
26
|
reader?: {
|
|
15
27
|
appName?: string;
|
|
16
|
-
domainName
|
|
28
|
+
domainName?: string;
|
|
17
29
|
branchName?: string;
|
|
18
30
|
buildCommand: string;
|
|
19
31
|
baseDirectory: string;
|
|
@@ -21,15 +33,42 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
21
33
|
};
|
|
22
34
|
papyrusVersion: string;
|
|
23
35
|
storagePreviewPrefix?: string;
|
|
36
|
+
github?: {
|
|
37
|
+
owner: string;
|
|
38
|
+
repo: string;
|
|
39
|
+
branches: string[];
|
|
40
|
+
ciCanDeployInfra: boolean;
|
|
41
|
+
};
|
|
24
42
|
};
|
|
25
43
|
|
|
44
|
+
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
45
|
+
|
|
26
46
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
27
47
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
28
48
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
29
49
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
50
|
+
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
30
51
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
52
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
31
53
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
32
54
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
55
|
+
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
56
|
+
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
57
|
+
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
58
|
+
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
59
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER"];
|
|
60
|
+
function originOfRedirectUrl(value: string): string {
|
|
61
|
+
try {
|
|
62
|
+
return new URL(value).origin;
|
|
63
|
+
} catch {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
69
|
+
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
70
|
+
}
|
|
71
|
+
|
|
33
72
|
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
34
73
|
|
|
35
74
|
function fail(field: string, problem: string): never {
|
|
@@ -73,12 +112,48 @@ function rejectUnknownKeys(record: Record<string, unknown>, allowed: string[], f
|
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
114
|
|
|
76
|
-
|
|
115
|
+
function parseRedirects(value: unknown): CmsRedirect[] | undefined {
|
|
116
|
+
if (value === undefined) return undefined;
|
|
117
|
+
if (!Array.isArray(value) || value.length === 0) fail("cms.redirects", "must be a non-empty array");
|
|
118
|
+
return (value as unknown[]).map((entry, index) => {
|
|
119
|
+
const field = `cms.redirects[${index}]`;
|
|
120
|
+
const record = requireObject(entry, field);
|
|
121
|
+
rejectUnknownKeys(record, ["source", "status"], field);
|
|
122
|
+
const source = requireString(record.source, `${field}.source`, HOST_NAME_PATTERN, "host name without scheme or path");
|
|
123
|
+
const status = record.status ?? 301;
|
|
124
|
+
if (status !== 301 && status !== 302) fail(`${field}.status`, `must be 301 or 302, got ${JSON.stringify(status)}`);
|
|
125
|
+
return { source, status };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveRedirectSubDomainPrefix(rootDomain: string, source: string): string {
|
|
130
|
+
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isStagingEnabled(config: Pick<AmplifyAppShellSiteConfig, "cms">): boolean {
|
|
134
|
+
return config.cms.staging !== false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolveCmsHostName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
|
|
138
|
+
if (!config.cms.domainName) return undefined;
|
|
139
|
+
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolveStagingDomainName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
|
|
143
|
+
if (!isStagingEnabled(config)) return undefined;
|
|
144
|
+
if (config.cms.domainPrefix) {
|
|
145
|
+
return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
|
|
146
|
+
}
|
|
77
147
|
if (config.cms.stagingDomainName) return config.cms.stagingDomainName;
|
|
148
|
+
if (!config.cms.domainName) return undefined;
|
|
78
149
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
79
150
|
return ["staging", ...rest].join(".");
|
|
80
151
|
}
|
|
81
152
|
|
|
153
|
+
export function resolveStackName(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "stackName">): string {
|
|
154
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
82
157
|
export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConfig, "storagePreviewPrefix">): string {
|
|
83
158
|
return config.storagePreviewPrefix ?? "preview/";
|
|
84
159
|
}
|
|
@@ -87,7 +162,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
87
162
|
const record = requireObject(raw, "site");
|
|
88
163
|
rejectUnknownKeys(
|
|
89
164
|
record,
|
|
90
|
-
["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix"],
|
|
165
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
91
166
|
"site",
|
|
92
167
|
);
|
|
93
168
|
|
|
@@ -98,24 +173,93 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
98
173
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
99
174
|
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
100
175
|
}
|
|
101
|
-
const hostedZoneId =
|
|
176
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
177
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
102
178
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
103
179
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
104
180
|
|
|
105
181
|
const cmsRecord = requireObject(record.cms, "cms");
|
|
106
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
182
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
107
183
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
108
184
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
109
185
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
110
186
|
}
|
|
187
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
188
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
189
|
+
}
|
|
190
|
+
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
191
|
+
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
192
|
+
}
|
|
111
193
|
const cms: AmplifyAppShellSiteConfig["cms"] = {
|
|
112
194
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
113
|
-
domainName:
|
|
195
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
196
|
+
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
197
|
+
staging: cmsRecord.staging as boolean | undefined,
|
|
198
|
+
cognitoDomainPrefix: requireString(
|
|
199
|
+
cmsRecord.cognitoDomainPrefix,
|
|
200
|
+
"cms.cognitoDomainPrefix",
|
|
201
|
+
COGNITO_DOMAIN_PREFIX_PATTERN,
|
|
202
|
+
"lowercase letters, digits and hyphens; globally unique per region",
|
|
203
|
+
),
|
|
204
|
+
applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix as boolean | undefined,
|
|
114
205
|
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
115
206
|
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
116
207
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
208
|
+
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
209
|
+
redirects: parseRedirects(cmsRecord.redirects),
|
|
117
210
|
};
|
|
118
211
|
|
|
212
|
+
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
213
|
+
if (reservedWord) {
|
|
214
|
+
fail("cms.cognitoDomainPrefix", `must not contain "${reservedWord}" (Cognito reserves it): ${JSON.stringify(cms.cognitoDomainPrefix)}`);
|
|
215
|
+
}
|
|
216
|
+
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
217
|
+
if (key in cms.environment) {
|
|
218
|
+
fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix and the revalidation secret parameter is derived from siteId");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
223
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
224
|
+
}
|
|
225
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
226
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
227
|
+
}
|
|
228
|
+
if (cms.domainPrefix && !cms.domainName) {
|
|
229
|
+
fail("cms.domainPrefix", "requires cms.domainName (the root domain)");
|
|
230
|
+
}
|
|
231
|
+
if (cms.stagingDomainPrefix && !cms.domainPrefix) {
|
|
232
|
+
fail("cms.stagingDomainPrefix", "requires cms.domainPrefix");
|
|
233
|
+
}
|
|
234
|
+
if (cms.domainPrefix && cms.stagingDomainName) {
|
|
235
|
+
fail("cms.stagingDomainName", "must not be set with cms.domainPrefix: use cms.stagingDomainPrefix");
|
|
236
|
+
}
|
|
237
|
+
if (cms.staging === false && cms.stagingDomainPrefix) {
|
|
238
|
+
fail("cms.stagingDomainPrefix", "must not be set when cms.staging is false");
|
|
239
|
+
}
|
|
240
|
+
if (cms.domainPrefix && cms.staging !== false && !cms.stagingDomainPrefix) {
|
|
241
|
+
fail("cms.stagingDomainPrefix", "is required when cms.domainPrefix is set and staging is enabled: the staging host is never derived under a shared root domain");
|
|
242
|
+
}
|
|
243
|
+
if (cms.domainPrefix && cms.stagingDomainPrefix === cms.domainPrefix) {
|
|
244
|
+
fail("cms.stagingDomainPrefix", "must differ from cms.domainPrefix");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (cms.redirects) {
|
|
248
|
+
if (!cms.domainPrefix) fail("cms.redirects", "requires cms.domainPrefix (redirects target the primary host under a shared root domain)");
|
|
249
|
+
cms.redirects.forEach((redirect, index) => {
|
|
250
|
+
const field = `cms.redirects[${index}].source`;
|
|
251
|
+
const root = cms.domainName as string;
|
|
252
|
+
if (redirect.source !== root && !(redirect.source.endsWith(`.${root}`) && DOMAIN_PREFIX_PATTERN.test(redirect.source.slice(0, -root.length - 1)))) {
|
|
253
|
+
fail(field, `must be the root domain ${root} or a single label under it: ${JSON.stringify(redirect.source)}`);
|
|
254
|
+
}
|
|
255
|
+
if (redirect.source === resolveCmsHostName({ cms })) fail(field, "must differ from the primary host");
|
|
256
|
+
if (redirect.source === resolveStagingDomainName({ cms })) fail(field, "must differ from the staging host");
|
|
257
|
+
});
|
|
258
|
+
if (new Set(cms.redirects.map((redirect) => redirect.source)).size !== cms.redirects.length) {
|
|
259
|
+
fail("cms.redirects", "must not repeat a source");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
119
263
|
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
120
264
|
if (record.reader !== undefined) {
|
|
121
265
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
@@ -123,7 +267,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
123
267
|
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
124
268
|
reader = {
|
|
125
269
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
126
|
-
domainName:
|
|
270
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
127
271
|
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
128
272
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
129
273
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
@@ -133,29 +277,67 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
133
277
|
fail("reader", `is required when frontend is "markus-static"`);
|
|
134
278
|
}
|
|
135
279
|
|
|
280
|
+
let github: AmplifyAppShellSiteConfig["github"];
|
|
281
|
+
if (record.github !== undefined) {
|
|
282
|
+
const githubRecord = requireObject(record.github, "github");
|
|
283
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
284
|
+
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
285
|
+
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
286
|
+
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
287
|
+
fail("github", `owner/repo must match repository ${repository}`);
|
|
288
|
+
}
|
|
289
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
290
|
+
if (githubRecord.branches !== undefined) {
|
|
291
|
+
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
292
|
+
fail("github.branches", "must be a non-empty array of branch names");
|
|
293
|
+
}
|
|
294
|
+
branches = (githubRecord.branches as unknown[]).map((branch, index) => {
|
|
295
|
+
if (typeof branch === "string" && /[*?]/.test(branch)) {
|
|
296
|
+
fail(`github.branches[${index}]`, `must not contain wildcards: ${JSON.stringify(branch)}`);
|
|
297
|
+
}
|
|
298
|
+
return requireString(branch, `github.branches[${index}]`, BRANCH_NAME_PATTERN);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
302
|
+
if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
|
|
303
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
307
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
308
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
309
|
+
}
|
|
310
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
311
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
312
|
+
}
|
|
313
|
+
|
|
136
314
|
const config: AmplifyAppShellSiteConfig = {
|
|
137
315
|
siteId,
|
|
138
316
|
repository,
|
|
139
317
|
brand,
|
|
140
318
|
frontend,
|
|
141
319
|
hostedZoneId,
|
|
320
|
+
stackName,
|
|
142
321
|
cms,
|
|
143
322
|
reader,
|
|
144
323
|
papyrusVersion,
|
|
145
324
|
storagePreviewPrefix,
|
|
325
|
+
github,
|
|
146
326
|
};
|
|
147
327
|
|
|
148
328
|
const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
149
329
|
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
150
330
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
331
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
332
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
151
333
|
const requiredOrigins = [
|
|
152
|
-
`https://${
|
|
153
|
-
`https://${
|
|
154
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
334
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
335
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
336
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
155
337
|
];
|
|
156
338
|
for (const origin of requiredOrigins) {
|
|
157
|
-
if (!
|
|
158
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
339
|
+
if (!listedOrigins.has(origin)) {
|
|
340
|
+
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
159
341
|
}
|
|
160
342
|
}
|
|
161
343
|
|
|
@@ -6,7 +6,11 @@ export function getAmplifyOutputsPath(): string {
|
|
|
6
6
|
if (explicit) {
|
|
7
7
|
return path.isAbsolute(explicit) ? explicit : path.join(process.cwd(), explicit);
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
const checkoutRootPath = path.join(process.cwd(), "amplify_outputs.json");
|
|
10
|
+
if (fs.existsSync(checkoutRootPath)) return checkoutRootPath;
|
|
11
|
+
const shippedBundlePath = path.join(process.cwd(), ".next", "amplify_outputs.json");
|
|
12
|
+
if (fs.existsSync(shippedBundlePath)) return shippedBundlePath;
|
|
13
|
+
return checkoutRootPath;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function readAmplifyOutputsFile(): Record<string, unknown> | null {
|