@anthusai/papyrus 1.0.0-next.9 → 1.1.0
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/prospectus-google-client.js +66 -0
- package/amplify/auth/prospectus-google-client.ts +71 -0
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +23 -5
- package/amplify/auth/resource.ts +31 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -62
- package/amplify/data/schema.ts +47 -62
- 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 +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- 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/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +150 -176
- package/amplify/site-backend.ts +189 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/auth/prospectus/callback/route.ts +9 -0
- package/app/api/auth/prospectus/selection/route.ts +9 -0
- package/app/api/auth/prospectus/session/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +9 -0
- package/app/api/auth/prospectus/sign-out/route.ts +9 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -6
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -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/priming-proof-page.tsx +94 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -8
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +166 -27
- package/infra/amplify-app-shell.ts +180 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- 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 +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-publication-access.ts +53 -0
- package/lib/prospectus-session.ts +196 -0
- package/lib/prospectus-sign-in-routes.ts +284 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- 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-prewarm.ts +53 -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/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +73 -8
- 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 +123 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- 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/components/ai-elements/message.tsx +0 -360
package/infra/site-config.ts
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
export type CmsRedirect = {
|
|
2
|
+
source: string;
|
|
3
|
+
status: 301 | 302;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ReaderRedirectStatus = "301" | "302" | "200" | "404" | "404-200";
|
|
7
|
+
|
|
8
|
+
export type ReaderRedirect = {
|
|
9
|
+
source: string;
|
|
10
|
+
target: string;
|
|
11
|
+
status: ReaderRedirectStatus;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ReaderCustomHeaderRule = {
|
|
15
|
+
pattern: string;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const READER_REDIRECT_STATUSES: ReaderRedirectStatus[] = ["301", "302", "200", "404", "404-200"];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What the CMS `staging` branch builds to preview drafts of a markus-static site: the site build
|
|
23
|
+
* command and the directory it writes. Without it, and without a `reader` to borrow them from,
|
|
24
|
+
* the staging branch builds the newsroom only.
|
|
25
|
+
*/
|
|
26
|
+
export type StagingPreviewConfig = {
|
|
27
|
+
buildCommand: string;
|
|
28
|
+
baseDirectory: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CmsConfig = {
|
|
32
|
+
appName?: string;
|
|
33
|
+
domainName?: string;
|
|
34
|
+
domainPrefix?: string;
|
|
35
|
+
staging?: boolean;
|
|
36
|
+
stagingPreview?: StagingPreviewConfig;
|
|
37
|
+
cognitoDomainPrefix: string;
|
|
38
|
+
applyCognitoDomainPrefix?: boolean;
|
|
39
|
+
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
40
|
+
environment: Record<string, string>;
|
|
41
|
+
stagingDomainName?: string;
|
|
42
|
+
stagingDomainPrefix?: string;
|
|
43
|
+
redirects?: CmsRedirect[];
|
|
44
|
+
prospectusAccountsApiStageArn?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
1
47
|
export type AmplifyAppShellSiteConfig = {
|
|
2
48
|
siteId: string;
|
|
3
49
|
repository: string;
|
|
@@ -5,21 +51,19 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
5
51
|
frontend: "pretext" | "markus-static";
|
|
6
52
|
hostedZoneId?: string;
|
|
7
53
|
stackName?: string;
|
|
8
|
-
cms
|
|
9
|
-
appName?: string;
|
|
10
|
-
domainName?: string;
|
|
11
|
-
staging?: boolean;
|
|
12
|
-
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
13
|
-
environment: Record<string, string>;
|
|
14
|
-
stagingDomainName?: string;
|
|
15
|
-
};
|
|
54
|
+
cms?: CmsConfig;
|
|
16
55
|
reader?: {
|
|
17
56
|
appName?: string;
|
|
18
57
|
domainName?: string;
|
|
58
|
+
domainPrefixes?: string[];
|
|
19
59
|
branchName?: string;
|
|
20
60
|
buildCommand: string;
|
|
21
61
|
baseDirectory: string;
|
|
22
62
|
environment?: Record<string, string>;
|
|
63
|
+
redirects?: ReaderRedirect[];
|
|
64
|
+
customHeaders?: ReaderCustomHeaderRule[];
|
|
65
|
+
preBuildCommands?: string[];
|
|
66
|
+
skipContentExport?: boolean;
|
|
23
67
|
};
|
|
24
68
|
papyrusVersion: string;
|
|
25
69
|
storagePreviewPrefix?: string;
|
|
@@ -31,18 +75,59 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
31
75
|
};
|
|
32
76
|
};
|
|
33
77
|
|
|
78
|
+
export type CmsSiteConfig = AmplifyAppShellSiteConfig & { cms: CmsConfig };
|
|
79
|
+
|
|
80
|
+
export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteConfig {
|
|
81
|
+
return config.cms !== undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
34
84
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
35
85
|
|
|
36
86
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
37
87
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
38
88
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
39
89
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
90
|
+
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
40
91
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
41
92
|
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
42
93
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
43
94
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
44
95
|
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
45
96
|
const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
97
|
+
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
98
|
+
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
99
|
+
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
100
|
+
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", "PAPYRUS_PROSPECTUS_SESSION_SECRET_ARN", "PAPYRUS_PROSPECTUS_SITE_PRODUCT"];
|
|
101
|
+
|
|
102
|
+
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
103
|
+
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
104
|
+
"GET/v1/identities/me",
|
|
105
|
+
"POST/v1/identities/google/challenge",
|
|
106
|
+
"POST/v1/identities/google/resolve",
|
|
107
|
+
"GET/v1/accounts",
|
|
108
|
+
"GET/v1/accounts/*",
|
|
109
|
+
"POST/v1/accounts/*/authorize",
|
|
110
|
+
"GET/v1/account-directory",
|
|
111
|
+
"GET/v1/account-directory/*",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn: string): string {
|
|
115
|
+
const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
|
|
116
|
+
const [apiId, stageName] = apiPath.split("/");
|
|
117
|
+
return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
|
|
118
|
+
}
|
|
119
|
+
function originOfRedirectUrl(value: string): string {
|
|
120
|
+
try {
|
|
121
|
+
return new URL(value).origin;
|
|
122
|
+
} catch {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
128
|
+
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
129
|
+
}
|
|
130
|
+
|
|
46
131
|
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
47
132
|
|
|
48
133
|
function fail(field: string, problem: string): never {
|
|
@@ -86,12 +171,88 @@ function rejectUnknownKeys(record: Record<string, unknown>, allowed: string[], f
|
|
|
86
171
|
}
|
|
87
172
|
}
|
|
88
173
|
|
|
89
|
-
|
|
174
|
+
function parseRedirects(value: unknown): CmsRedirect[] | undefined {
|
|
175
|
+
if (value === undefined) return undefined;
|
|
176
|
+
if (!Array.isArray(value) || value.length === 0) fail("cms.redirects", "must be a non-empty array");
|
|
177
|
+
return (value as unknown[]).map((entry, index) => {
|
|
178
|
+
const field = `cms.redirects[${index}]`;
|
|
179
|
+
const record = requireObject(entry, field);
|
|
180
|
+
rejectUnknownKeys(record, ["source", "status"], field);
|
|
181
|
+
const source = requireString(record.source, `${field}.source`, HOST_NAME_PATTERN, "host name without scheme or path");
|
|
182
|
+
const status = record.status ?? 301;
|
|
183
|
+
if (status !== 301 && status !== 302) fail(`${field}.status`, `must be 301 or 302, got ${JSON.stringify(status)}`);
|
|
184
|
+
return { source, status };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseReaderDomainPrefixes(value: unknown): string[] | undefined {
|
|
189
|
+
if (value === undefined) return undefined;
|
|
190
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.domainPrefixes", "must be a non-empty array");
|
|
191
|
+
const prefixes = (value as unknown[]).map((entry, index) => {
|
|
192
|
+
const field = `reader.domainPrefixes[${index}]`;
|
|
193
|
+
if (entry === "") return "";
|
|
194
|
+
return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
|
|
195
|
+
});
|
|
196
|
+
if (new Set(prefixes).size !== prefixes.length) fail("reader.domainPrefixes", "must not repeat a prefix");
|
|
197
|
+
return prefixes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseReaderRedirects(value: unknown): ReaderRedirect[] | undefined {
|
|
201
|
+
if (value === undefined) return undefined;
|
|
202
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.redirects", "must be a non-empty array");
|
|
203
|
+
return (value as unknown[]).map((entry, index) => {
|
|
204
|
+
const field = `reader.redirects[${index}]`;
|
|
205
|
+
const record = requireObject(entry, field);
|
|
206
|
+
rejectUnknownKeys(record, ["source", "target", "status"], field);
|
|
207
|
+
const status = record.status ?? "301";
|
|
208
|
+
if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status as ReaderRedirectStatus)) {
|
|
209
|
+
fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
source: requireString(record.source, `${field}.source`),
|
|
213
|
+
target: requireString(record.target, `${field}.target`),
|
|
214
|
+
status: status as ReaderRedirectStatus,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseReaderCustomHeaders(value: unknown): ReaderCustomHeaderRule[] | undefined {
|
|
220
|
+
if (value === undefined) return undefined;
|
|
221
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.customHeaders", "must be a non-empty array");
|
|
222
|
+
return (value as unknown[]).map((entry, index) => {
|
|
223
|
+
const field = `reader.customHeaders[${index}]`;
|
|
224
|
+
const record = requireObject(entry, field);
|
|
225
|
+
rejectUnknownKeys(record, ["pattern", "headers"], field);
|
|
226
|
+
const headers = requireStringRecord(record.headers, `${field}.headers`);
|
|
227
|
+
if (Object.keys(headers).length === 0) fail(`${field}.headers`, "must not be empty");
|
|
228
|
+
return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseReaderPreBuildCommands(value: unknown): string[] | undefined {
|
|
233
|
+
if (value === undefined) return undefined;
|
|
234
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.preBuildCommands", "must be a non-empty array of commands");
|
|
235
|
+
return (value as unknown[]).map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function resolveRedirectSubDomainPrefix(rootDomain: string, source: string): string {
|
|
239
|
+
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function isStagingEnabled(config: Pick<CmsSiteConfig, "cms">): boolean {
|
|
90
243
|
return config.cms.staging !== false;
|
|
91
244
|
}
|
|
92
245
|
|
|
93
|
-
export function
|
|
246
|
+
export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
247
|
+
if (!config.cms.domainName) return undefined;
|
|
248
|
+
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
94
252
|
if (!isStagingEnabled(config)) return undefined;
|
|
253
|
+
if (config.cms.domainPrefix) {
|
|
254
|
+
return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
|
|
255
|
+
}
|
|
95
256
|
if (config.cms.stagingDomainName) return config.cms.stagingDomainName;
|
|
96
257
|
if (!config.cms.domainName) return undefined;
|
|
97
258
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
@@ -106,28 +267,19 @@ export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConf
|
|
|
106
267
|
return config.storagePreviewPrefix ?? "preview/";
|
|
107
268
|
}
|
|
108
269
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
119
|
-
const brand = requireString(record.brand, "brand");
|
|
120
|
-
const frontend = requireString(record.frontend, "frontend");
|
|
121
|
-
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
122
|
-
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
123
|
-
}
|
|
124
|
-
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
125
|
-
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
126
|
-
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
127
|
-
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
270
|
+
function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
271
|
+
if (value === undefined) return undefined;
|
|
272
|
+
const record = requireObject(value, "cms.stagingPreview");
|
|
273
|
+
rejectUnknownKeys(record, ["buildCommand", "baseDirectory"], "cms.stagingPreview");
|
|
274
|
+
return {
|
|
275
|
+
buildCommand: requireString(record.buildCommand, "cms.stagingPreview.buildCommand"),
|
|
276
|
+
baseDirectory: requireString(record.baseDirectory, "cms.stagingPreview.baseDirectory"),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
128
279
|
|
|
129
|
-
|
|
130
|
-
|
|
280
|
+
function parseCms(value: unknown): CmsConfig {
|
|
281
|
+
const cmsRecord = requireObject(value, "cms");
|
|
282
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
|
|
131
283
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
132
284
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
133
285
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -135,27 +287,137 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
135
287
|
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
136
288
|
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
137
289
|
}
|
|
138
|
-
|
|
290
|
+
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
291
|
+
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
292
|
+
}
|
|
293
|
+
const cms: CmsConfig = {
|
|
139
294
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
140
295
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
296
|
+
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
141
297
|
staging: cmsRecord.staging as boolean | undefined,
|
|
298
|
+
cognitoDomainPrefix: requireString(
|
|
299
|
+
cmsRecord.cognitoDomainPrefix,
|
|
300
|
+
"cms.cognitoDomainPrefix",
|
|
301
|
+
COGNITO_DOMAIN_PREFIX_PATTERN,
|
|
302
|
+
"lowercase letters, digits and hyphens; globally unique per region",
|
|
303
|
+
),
|
|
304
|
+
applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix as boolean | undefined,
|
|
142
305
|
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
143
306
|
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
144
307
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
308
|
+
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
309
|
+
redirects: parseRedirects(cmsRecord.redirects),
|
|
310
|
+
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
311
|
+
prospectusAccountsApiStageArn: optionalString(
|
|
312
|
+
cmsRecord.prospectusAccountsApiStageArn,
|
|
313
|
+
"cms.prospectusAccountsApiStageArn",
|
|
314
|
+
PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN,
|
|
315
|
+
"arn:aws:execute-api:<region>:<account>:<api id>/api",
|
|
316
|
+
),
|
|
145
317
|
};
|
|
146
318
|
|
|
319
|
+
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
320
|
+
if (reservedWord) {
|
|
321
|
+
fail("cms.cognitoDomainPrefix", `must not contain "${reservedWord}" (Cognito reserves it): ${JSON.stringify(cms.cognitoDomainPrefix)}`);
|
|
322
|
+
}
|
|
323
|
+
for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
|
|
324
|
+
if (key in cms.environment) {
|
|
325
|
+
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");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
147
329
|
if (cms.staging === false && cms.stagingDomainName) {
|
|
148
330
|
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
149
331
|
}
|
|
150
332
|
if (cms.stagingDomainName && !cms.domainName) {
|
|
151
333
|
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
152
334
|
}
|
|
335
|
+
if (cms.domainPrefix && !cms.domainName) {
|
|
336
|
+
fail("cms.domainPrefix", "requires cms.domainName (the root domain)");
|
|
337
|
+
}
|
|
338
|
+
if (cms.stagingDomainPrefix && !cms.domainPrefix) {
|
|
339
|
+
fail("cms.stagingDomainPrefix", "requires cms.domainPrefix");
|
|
340
|
+
}
|
|
341
|
+
if (cms.domainPrefix && cms.stagingDomainName) {
|
|
342
|
+
fail("cms.stagingDomainName", "must not be set with cms.domainPrefix: use cms.stagingDomainPrefix");
|
|
343
|
+
}
|
|
344
|
+
if (cms.staging === false && cms.stagingDomainPrefix) {
|
|
345
|
+
fail("cms.stagingDomainPrefix", "must not be set when cms.staging is false");
|
|
346
|
+
}
|
|
347
|
+
if (cms.domainPrefix && cms.staging !== false && !cms.stagingDomainPrefix) {
|
|
348
|
+
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");
|
|
349
|
+
}
|
|
350
|
+
if (cms.domainPrefix && cms.stagingDomainPrefix === cms.domainPrefix) {
|
|
351
|
+
fail("cms.stagingDomainPrefix", "must differ from cms.domainPrefix");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (cms.redirects) {
|
|
355
|
+
if (!cms.domainPrefix) fail("cms.redirects", "requires cms.domainPrefix (redirects target the primary host under a shared root domain)");
|
|
356
|
+
cms.redirects.forEach((redirect, index) => {
|
|
357
|
+
const field = `cms.redirects[${index}].source`;
|
|
358
|
+
const root = cms.domainName as string;
|
|
359
|
+
if (redirect.source !== root && !(redirect.source.endsWith(`.${root}`) && DOMAIN_PREFIX_PATTERN.test(redirect.source.slice(0, -root.length - 1)))) {
|
|
360
|
+
fail(field, `must be the root domain ${root} or a single label under it: ${JSON.stringify(redirect.source)}`);
|
|
361
|
+
}
|
|
362
|
+
if (redirect.source === resolveCmsHostName({ cms })) fail(field, "must differ from the primary host");
|
|
363
|
+
if (redirect.source === resolveStagingDomainName({ cms })) fail(field, "must differ from the staging host");
|
|
364
|
+
});
|
|
365
|
+
if (new Set(cms.redirects.map((redirect) => redirect.source)).size !== cms.redirects.length) {
|
|
366
|
+
fail("cms.redirects", "must not repeat a source");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return cms;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function validateOauthRedirectUrls(config: CmsSiteConfig): void {
|
|
374
|
+
const redirectValue = config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
375
|
+
if (redirectValue === undefined) fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
376
|
+
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
377
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
378
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
379
|
+
const requiredOrigins = [
|
|
380
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
381
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
382
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
383
|
+
];
|
|
384
|
+
for (const origin of requiredOrigins) {
|
|
385
|
+
if (!listedOrigins.has(origin)) {
|
|
386
|
+
fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
392
|
+
const record = requireObject(raw, "site");
|
|
393
|
+
rejectUnknownKeys(
|
|
394
|
+
record,
|
|
395
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
396
|
+
"site",
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
400
|
+
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
401
|
+
const brand = requireString(record.brand, "brand");
|
|
402
|
+
const frontend = requireString(record.frontend, "frontend");
|
|
403
|
+
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
404
|
+
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
405
|
+
}
|
|
406
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
407
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
408
|
+
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
409
|
+
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
410
|
+
|
|
411
|
+
const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
|
|
153
412
|
|
|
154
413
|
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
155
414
|
if (record.reader !== undefined) {
|
|
156
415
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
157
416
|
const readerRecord = requireObject(record.reader, "reader");
|
|
158
|
-
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
417
|
+
rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
|
|
418
|
+
if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
|
|
419
|
+
fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
|
|
420
|
+
}
|
|
159
421
|
reader = {
|
|
160
422
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
161
423
|
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
@@ -163,13 +425,26 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
163
425
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
164
426
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
165
427
|
environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
|
|
428
|
+
domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
|
|
429
|
+
redirects: parseReaderRedirects(readerRecord.redirects),
|
|
430
|
+
customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
|
|
431
|
+
preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
|
|
432
|
+
skipContentExport: readerRecord.skipContentExport as boolean | undefined,
|
|
166
433
|
};
|
|
167
|
-
|
|
168
|
-
|
|
434
|
+
if (reader.domainPrefixes && !reader.domainName) fail("reader.domainPrefixes", "requires reader.domainName");
|
|
435
|
+
} else if (frontend === "markus-static" && cms === undefined) {
|
|
436
|
+
fail("reader", `is required when frontend is "markus-static" and there is no cms section`);
|
|
437
|
+
}
|
|
438
|
+
if (cms?.stagingPreview !== undefined && frontend !== "markus-static") {
|
|
439
|
+
fail("cms.stagingPreview", `is only allowed when frontend is "markus-static"`);
|
|
440
|
+
}
|
|
441
|
+
if (cms?.stagingPreview !== undefined && cms.staging === false) {
|
|
442
|
+
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
169
443
|
}
|
|
170
444
|
|
|
171
445
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
172
446
|
if (record.github !== undefined) {
|
|
447
|
+
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
173
448
|
const githubRecord = requireObject(record.github, "github");
|
|
174
449
|
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
175
450
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
@@ -177,7 +452,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
177
452
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
178
453
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
179
454
|
}
|
|
180
|
-
let branches = cms
|
|
455
|
+
let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
181
456
|
if (githubRecord.branches !== undefined) {
|
|
182
457
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
183
458
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -194,7 +469,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
194
469
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
195
470
|
}
|
|
196
471
|
|
|
197
|
-
const hasDomain = cms
|
|
472
|
+
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
198
473
|
if (hasDomain && hostedZoneId === undefined) {
|
|
199
474
|
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
200
475
|
}
|
|
@@ -216,20 +491,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
216
491
|
github,
|
|
217
492
|
};
|
|
218
493
|
|
|
219
|
-
|
|
220
|
-
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
221
|
-
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
222
|
-
const stagingDomainName = resolveStagingDomainName(config);
|
|
223
|
-
const requiredOrigins = [
|
|
224
|
-
...(cms.domainName ? [`https://${cms.domainName}/`] : []),
|
|
225
|
-
...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
|
|
226
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
227
|
-
];
|
|
228
|
-
for (const origin of requiredOrigins) {
|
|
229
|
-
if (!redirectUrls.includes(origin)) {
|
|
230
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
494
|
+
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
|
233
495
|
|
|
234
496
|
return config;
|
|
235
497
|
}
|
|
@@ -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 {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
|
|
3
3
|
import { Amplify, type ResourcesConfig } from "aws-amplify";
|
|
4
|
+
import {
|
|
5
|
+
createAWSCredentialsAndIdentityIdProvider,
|
|
6
|
+
createUserPoolsTokenProvider,
|
|
7
|
+
runWithAmplifyServerContext as runWithAmplifyContext,
|
|
8
|
+
} from "aws-amplify/adapter-core";
|
|
9
|
+
import { sharedInMemoryStorage } from "aws-amplify/utils";
|
|
10
|
+
import { createSharedCredentialsProvider } from "./shared-credentials-provider";
|
|
4
11
|
import { assertSandboxAmplifyOutputsForDev } from "./amplify-outputs-guard";
|
|
5
12
|
import { getAmplifyOutputsPath } from "./amplify-outputs-path";
|
|
6
13
|
|
|
@@ -14,14 +21,42 @@ export function getAmplifyServerRuntime(): AmplifyServerRuntime {
|
|
|
14
21
|
if (cachedRuntime) return cachedRuntime;
|
|
15
22
|
|
|
16
23
|
const config = loadAmplifyOutputs();
|
|
17
|
-
Amplify.configure(config, { ssr: true });
|
|
18
24
|
cachedRuntime = {
|
|
19
25
|
...createServerRunner({ config }),
|
|
20
26
|
config,
|
|
21
27
|
};
|
|
28
|
+
Amplify.configure(config, config.Auth ? { ssr: true, Auth: getGuestProviders(config) } : { ssr: true });
|
|
22
29
|
return cachedRuntime;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
type GuestProviders = {
|
|
33
|
+
credentialsProvider: ReturnType<typeof createAWSCredentialsAndIdentityIdProvider>;
|
|
34
|
+
tokenProvider: ReturnType<typeof createUserPoolsTokenProvider>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const GUEST_PROVIDERS_KEY = Symbol.for("papyrus.guestAmplifyProviders");
|
|
38
|
+
|
|
39
|
+
function getGuestProviders(config: ResourcesConfig): GuestProviders {
|
|
40
|
+
const holder = globalThis as unknown as Record<symbol, GuestProviders | undefined>;
|
|
41
|
+
const existing = holder[GUEST_PROVIDERS_KEY];
|
|
42
|
+
if (existing) return existing;
|
|
43
|
+
const auth = config.Auth as NonNullable<ResourcesConfig["Auth"]>;
|
|
44
|
+
const created: GuestProviders = {
|
|
45
|
+
credentialsProvider: createSharedCredentialsProvider(
|
|
46
|
+
createAWSCredentialsAndIdentityIdProvider(auth, sharedInMemoryStorage),
|
|
47
|
+
),
|
|
48
|
+
tokenProvider: createUserPoolsTokenProvider(auth, sharedInMemoryStorage),
|
|
49
|
+
};
|
|
50
|
+
holder[GUEST_PROVIDERS_KEY] = created;
|
|
51
|
+
return created;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runWithGuestAmplifyContext<T>(operation: (contextSpec: Parameters<Parameters<typeof runWithAmplifyContext>[2]>[0]) => Promise<T>): Promise<T> {
|
|
55
|
+
const { config } = getAmplifyServerRuntime();
|
|
56
|
+
if (!config.Auth) return runWithAmplifyContext(config, {}, operation) as Promise<T>;
|
|
57
|
+
return runWithAmplifyContext(config, { Auth: getGuestProviders(config) }, operation) as Promise<T>;
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
export function hasAmplifyOutputs(): boolean {
|
|
26
61
|
return fs.existsSync(getAmplifyOutputsPath());
|
|
27
62
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Priming proof reports a publication owner approved for visitors, keyed by site brand id.
|
|
3
|
+
* A report reaches this map only through a reviewed commit of its
|
|
4
|
+
* `publications/priming-proofs/<brand id>.json` file; see docs/priming-proof.md.
|
|
5
|
+
*/
|
|
6
|
+
export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {};
|