@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41

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