@anthusai/papyrus 1.0.0-next.8 → 1.0.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/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 +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -60
- package/amplify/data/schema.ts +47 -60
- 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/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- 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 +143 -176
- package/amplify/site-backend.ts +181 -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/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 -0
- package/bin/papyrus-infra.mjs +3 -3
- 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 +92 -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 -6
- 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 +219 -71
- package/infra/amplify-app-shell.ts +231 -77
- package/infra/build-specs.js +23 -8
- package/infra/build-specs.ts +27 -11
- package/infra/site-config.js +277 -34
- package/infra/site-config.ts +340 -43
- 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 +89 -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 +64 -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 +68 -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/infra/site-config.ts
CHANGED
|
@@ -1,23 +1,69 @@
|
|
|
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;
|
|
4
50
|
brand: string;
|
|
5
51
|
frontend: "pretext" | "markus-static";
|
|
6
|
-
hostedZoneId
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
domainName: string;
|
|
10
|
-
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
11
|
-
environment: Record<string, string>;
|
|
12
|
-
stagingDomainName?: string;
|
|
13
|
-
};
|
|
52
|
+
hostedZoneId?: string;
|
|
53
|
+
stackName?: string;
|
|
54
|
+
cms?: CmsConfig;
|
|
14
55
|
reader?: {
|
|
15
56
|
appName?: string;
|
|
16
|
-
domainName
|
|
57
|
+
domainName?: string;
|
|
58
|
+
domainPrefixes?: string[];
|
|
17
59
|
branchName?: string;
|
|
18
60
|
buildCommand: string;
|
|
19
61
|
baseDirectory: string;
|
|
20
62
|
environment?: Record<string, string>;
|
|
63
|
+
redirects?: ReaderRedirect[];
|
|
64
|
+
customHeaders?: ReaderCustomHeaderRule[];
|
|
65
|
+
preBuildCommands?: string[];
|
|
66
|
+
skipContentExport?: boolean;
|
|
21
67
|
};
|
|
22
68
|
papyrusVersion: string;
|
|
23
69
|
storagePreviewPrefix?: string;
|
|
@@ -29,17 +75,59 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
29
75
|
};
|
|
30
76
|
};
|
|
31
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
|
+
|
|
32
84
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
33
85
|
|
|
34
86
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
35
87
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
36
88
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
37
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])?$/;
|
|
38
91
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
92
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
39
93
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
40
94
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
41
95
|
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
42
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"];
|
|
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
|
+
|
|
43
131
|
const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
|
|
44
132
|
|
|
45
133
|
function fail(field: string, problem: string): never {
|
|
@@ -83,21 +171,228 @@ function rejectUnknownKeys(record: Record<string, unknown>, allowed: string[], f
|
|
|
83
171
|
}
|
|
84
172
|
}
|
|
85
173
|
|
|
86
|
-
|
|
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 {
|
|
243
|
+
return config.cms.staging !== false;
|
|
244
|
+
}
|
|
245
|
+
|
|
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 {
|
|
252
|
+
if (!isStagingEnabled(config)) return undefined;
|
|
253
|
+
if (config.cms.domainPrefix) {
|
|
254
|
+
return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
|
|
255
|
+
}
|
|
87
256
|
if (config.cms.stagingDomainName) return config.cms.stagingDomainName;
|
|
257
|
+
if (!config.cms.domainName) return undefined;
|
|
88
258
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
89
259
|
return ["staging", ...rest].join(".");
|
|
90
260
|
}
|
|
91
261
|
|
|
262
|
+
export function resolveStackName(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "stackName">): string {
|
|
263
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
92
266
|
export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConfig, "storagePreviewPrefix">): string {
|
|
93
267
|
return config.storagePreviewPrefix ?? "preview/";
|
|
94
268
|
}
|
|
95
269
|
|
|
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
|
+
}
|
|
279
|
+
|
|
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");
|
|
283
|
+
const buildComputeType = cmsRecord.buildComputeType;
|
|
284
|
+
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
285
|
+
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
286
|
+
}
|
|
287
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
288
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
289
|
+
}
|
|
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 = {
|
|
294
|
+
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
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"),
|
|
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,
|
|
305
|
+
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
306
|
+
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
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
|
+
),
|
|
317
|
+
};
|
|
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
|
+
|
|
329
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
330
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
331
|
+
}
|
|
332
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
333
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
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
|
+
|
|
96
391
|
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
97
392
|
const record = requireObject(raw, "site");
|
|
98
393
|
rejectUnknownKeys(
|
|
99
394
|
record,
|
|
100
|
-
["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
395
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
101
396
|
"site",
|
|
102
397
|
);
|
|
103
398
|
|
|
@@ -108,43 +403,48 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
108
403
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
109
404
|
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
110
405
|
}
|
|
111
|
-
const hostedZoneId =
|
|
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");
|
|
112
408
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
113
409
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
114
410
|
|
|
115
|
-
const
|
|
116
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
117
|
-
const buildComputeType = cmsRecord.buildComputeType;
|
|
118
|
-
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
119
|
-
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
120
|
-
}
|
|
121
|
-
const cms: AmplifyAppShellSiteConfig["cms"] = {
|
|
122
|
-
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
123
|
-
domainName: requireString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
124
|
-
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
125
|
-
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
126
|
-
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
127
|
-
};
|
|
411
|
+
const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
|
|
128
412
|
|
|
129
413
|
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
130
414
|
if (record.reader !== undefined) {
|
|
131
415
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
132
416
|
const readerRecord = requireObject(record.reader, "reader");
|
|
133
|
-
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
|
+
}
|
|
134
421
|
reader = {
|
|
135
422
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
136
|
-
domainName:
|
|
423
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
137
424
|
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
138
425
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
139
426
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
140
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,
|
|
141
433
|
};
|
|
142
|
-
|
|
143
|
-
|
|
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");
|
|
144
443
|
}
|
|
145
444
|
|
|
146
445
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
147
446
|
if (record.github !== undefined) {
|
|
447
|
+
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
148
448
|
const githubRecord = requireObject(record.github, "github");
|
|
149
449
|
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
150
450
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
@@ -152,7 +452,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
152
452
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
153
453
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
154
454
|
}
|
|
155
|
-
let branches = DEFAULT_CI_BRANCHES;
|
|
455
|
+
let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
156
456
|
if (githubRecord.branches !== undefined) {
|
|
157
457
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
158
458
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -169,12 +469,21 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
169
469
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
170
470
|
}
|
|
171
471
|
|
|
472
|
+
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
473
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
474
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
475
|
+
}
|
|
476
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
477
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
478
|
+
}
|
|
479
|
+
|
|
172
480
|
const config: AmplifyAppShellSiteConfig = {
|
|
173
481
|
siteId,
|
|
174
482
|
repository,
|
|
175
483
|
brand,
|
|
176
484
|
frontend,
|
|
177
485
|
hostedZoneId,
|
|
486
|
+
stackName,
|
|
178
487
|
cms,
|
|
179
488
|
reader,
|
|
180
489
|
papyrusVersion,
|
|
@@ -182,19 +491,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
182
491
|
github,
|
|
183
492
|
};
|
|
184
493
|
|
|
185
|
-
|
|
186
|
-
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
187
|
-
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
188
|
-
const requiredOrigins = [
|
|
189
|
-
`https://${cms.domainName}/`,
|
|
190
|
-
`https://${resolveStagingDomainName(config)}/`,
|
|
191
|
-
LOCAL_DEVELOPMENT_ORIGIN,
|
|
192
|
-
];
|
|
193
|
-
for (const origin of requiredOrigins) {
|
|
194
|
-
if (!redirectUrls.includes(origin)) {
|
|
195
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
494
|
+
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
|
198
495
|
|
|
199
496
|
return config;
|
|
200
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> = {};
|