@anthusai/papyrus 2.0.0-next.7 → 2.0.0-next.71

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 (165) hide show
  1. package/amplify/data/resource.js +23 -3
  2. package/amplify/data/resource.ts +25 -3
  3. package/amplify/data/schema.js +2028 -1979
  4. package/amplify/data/schema.ts +72 -14
  5. package/amplify/functions/assignment-action/data-access.js +90 -0
  6. package/amplify/functions/assignment-action/data-access.ts +89 -0
  7. package/amplify/functions/assignment-action/handler.ts +1 -1
  8. package/amplify/functions/content-actions/handler.py +61 -2
  9. package/amplify/functions/content-actions/requirements.txt +1 -0
  10. package/amplify/functions/content-actions/resource.js +1 -1
  11. package/amplify/functions/content-actions/resource.ts +1 -1
  12. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
  13. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  14. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  15. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  16. package/amplify/functions/editorial-checks/schema.js +121 -0
  17. package/amplify/{plugins → functions}/editorial-checks/schema.ts +33 -6
  18. package/amplify/functions/editorial-checks/wire.js +53 -0
  19. package/amplify/functions/editorial-checks/wire.ts +61 -0
  20. package/amplify/functions/monitoring-view/handler.py +11 -0
  21. package/amplify/functions/monitoring-view/resource.js +24 -0
  22. package/amplify/functions/monitoring-view/resource.ts +34 -0
  23. package/amplify/functions/monitoring-view/schema.js +16 -0
  24. package/amplify/functions/monitoring-view/schema.ts +18 -0
  25. package/amplify/functions/monitoring-view/wire.js +37 -0
  26. package/amplify/functions/monitoring-view/wire.ts +47 -0
  27. package/amplify/functions/public-forms/core.js +247 -0
  28. package/amplify/functions/public-forms/core.ts +310 -0
  29. package/amplify/functions/public-forms/handler.ts +41 -0
  30. package/amplify/functions/public-forms/origins.js +35 -0
  31. package/amplify/functions/public-forms/origins.ts +43 -0
  32. package/amplify/functions/public-forms/resource.js +9 -0
  33. package/amplify/functions/public-forms/resource.ts +10 -0
  34. package/amplify/functions/public-forms/wire.js +25 -0
  35. package/amplify/functions/public-forms/wire.ts +32 -0
  36. package/amplify/functions/shared/editorial-context.ts +63 -4
  37. package/amplify/functions/shared/python-bundle.js +20 -14
  38. package/amplify/functions/shared/python-bundle.ts +25 -18
  39. package/amplify/plugins/newsletter/config.js +245 -0
  40. package/amplify/plugins/newsletter/config.ts +305 -0
  41. package/amplify/plugins/newsletter/function/handler.py +6 -0
  42. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  43. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  44. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  45. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  46. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  47. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  48. package/amplify/plugins/newsletter/schema.js +56 -0
  49. package/amplify/plugins/newsletter/schema.ts +61 -0
  50. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  51. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  52. package/amplify/plugins/newsletter/sending.js +140 -0
  53. package/amplify/plugins/newsletter/sending.ts +186 -0
  54. package/amplify/plugins/reader-steering/config.js +180 -0
  55. package/amplify/plugins/reader-steering/config.ts +280 -0
  56. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  57. package/amplify/plugins/reader-steering/function/handler.py +1554 -0
  58. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  59. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  60. package/amplify/plugins/reader-steering/function/proposals.py +255 -0
  61. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  62. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  63. package/amplify/plugins/reader-steering/papyrus.plugin.js +184 -0
  64. package/amplify/plugins/reader-steering/papyrus.plugin.ts +210 -0
  65. package/amplify/plugins/reader-steering/schema.js +204 -0
  66. package/amplify/plugins/reader-steering/schema.ts +206 -0
  67. package/amplify/plugins/registry.js +4 -2
  68. package/amplify/plugins/registry.ts +4 -2
  69. package/amplify/plugins/types.ts +5 -0
  70. package/amplify/plugins/wire.js +1 -0
  71. package/amplify/plugins/wire.ts +1 -0
  72. package/amplify/publication/projection-lease.js +30 -0
  73. package/amplify/publication/projection-lease.ts +39 -0
  74. package/amplify/publication/redirect-and-index.js +105 -0
  75. package/amplify/publication/redirect-rules.js +73 -0
  76. package/amplify/publication/redirect-rules.ts +93 -0
  77. package/amplify/publication/s3-projection.js +7 -1
  78. package/amplify/publication/s3-projection.ts +9 -1
  79. package/amplify/site-backend-config.ts +17 -0
  80. package/amplify/site-backend.js +38 -1
  81. package/amplify/site-backend.ts +46 -1
  82. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  83. package/app/api/newsletter/confirm/route.ts +13 -0
  84. package/app/api/newsletter/preferences/route.ts +13 -0
  85. package/app/api/newsletter/state/route.ts +9 -0
  86. package/app/api/newsletter/subscribe/route.ts +9 -0
  87. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  88. package/app/api/newsroom/curation/context/route.ts +9 -0
  89. package/app/api/newsroom/curation/priming/route.ts +9 -0
  90. package/app/api/newsroom/curation/publication/route.ts +9 -0
  91. package/app/api/reader/comment/delete/route.ts +9 -0
  92. package/app/api/reader/comment/edit/route.ts +9 -0
  93. package/app/api/reader/comment/route.ts +9 -0
  94. package/app/api/reader/flag/route.ts +9 -0
  95. package/app/api/reader/handle/route.ts +9 -0
  96. package/app/api/reader/me/route.ts +9 -0
  97. package/app/api/reader/rating/route.ts +9 -0
  98. package/app/api/reader/report/route.ts +9 -0
  99. package/app/api/reader/retired/route.ts +9 -0
  100. package/app/api/reader/submit/route.ts +9 -0
  101. package/app/api/reader/vote/route.ts +9 -0
  102. package/bin/papyrus-infra.mjs +20 -1
  103. package/components/editorial-checks-admin-panel.tsx +36 -11
  104. package/components/moderation-panel.tsx +248 -0
  105. package/components/monitoring-overview-view.tsx +241 -0
  106. package/components/monitoring-panel.tsx +40 -0
  107. package/components/newsletter-panel.tsx +474 -0
  108. package/components/newsletter-sections.tsx +163 -0
  109. package/components/newsroom-article-editor.tsx +2 -4
  110. package/components/newsroom-curation-bootstrap.tsx +45 -0
  111. package/components/newsroom-organizer.css +31 -0
  112. package/components/newsroom-references-view.tsx +14 -14
  113. package/components/publication-cyclotron-panel.tsx +15 -10
  114. package/components/reference-add-entry.tsx +75 -0
  115. package/components/reference-curation-controls.tsx +31 -22
  116. package/components/reference-cyclotron-priming.tsx +58 -19
  117. package/components/reference-cyclotron-suggestion.tsx +1 -1
  118. package/components/reference-priming.tsx +14 -10
  119. package/components/reference-section-suggestion.tsx +1 -1
  120. package/components/topic-steering-workspace.tsx +64 -20
  121. package/components/use-editorial-checks.ts +12 -10
  122. package/infra/amplify-app-shell.js +90 -16
  123. package/infra/amplify-app-shell.ts +102 -18
  124. package/infra/index.js +1 -0
  125. package/infra/secure-string-parameter-handler.js +41 -0
  126. package/infra/secure-string-parameter-handler.ts +41 -0
  127. package/infra/secure-string-parameter.js +79 -0
  128. package/infra/secure-string-parameter.ts +91 -0
  129. package/infra/site-config.js +86 -6
  130. package/infra/site-config.ts +129 -6
  131. package/lib/category-repository.ts +11 -9
  132. package/lib/curation-routes.ts +345 -0
  133. package/lib/editorial-checks-backend.ts +1 -1
  134. package/lib/editorial-checks-client.ts +19 -1
  135. package/lib/editorial-checks-demo.ts +2 -2
  136. package/lib/editorial-checks-settings.ts +3 -0
  137. package/lib/editorial-checks.ts +8 -11
  138. package/lib/editorial-preview.ts +5 -5
  139. package/lib/monitoring-client.ts +29 -0
  140. package/lib/monitoring-overview-fixture.ts +51 -0
  141. package/lib/monitoring-overview.ts +145 -0
  142. package/lib/newsletter-api.ts +418 -0
  143. package/lib/newsletter-client.ts +46 -0
  144. package/lib/newsletter-fixture.ts +184 -0
  145. package/lib/newsletter-model.ts +202 -0
  146. package/lib/prospectus-session.ts +15 -14
  147. package/lib/prospectus-sign-in-routes.ts +84 -19
  148. package/lib/prospectus-site-identity.ts +24 -1
  149. package/lib/reader-api.ts +145 -0
  150. package/lib/reader-moderation.ts +220 -0
  151. package/lib/reader-retirement-route.ts +89 -0
  152. package/lib/reader-subject.md +29 -0
  153. package/lib/reader-subject.ts +53 -0
  154. package/lib/relevance-status.ts +2 -2
  155. package/lib/semantic-graph.ts +2 -0
  156. package/lib/site-brand-tenant.ts +11 -6
  157. package/lib/site-brand.ts +6 -3
  158. package/lib/site-origin.ts +67 -0
  159. package/middleware.ts +14 -3
  160. package/package.json +42 -3
  161. package/routes.manifest.json +215 -2
  162. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  163. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -69
  164. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -85
  165. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -32,6 +32,8 @@ export type CmsConfig = {
32
32
  appName?: string;
33
33
  domainName?: string;
34
34
  domainPrefix?: string;
35
+ /** Public https origin of a site whose host is served by its own edge (no Amplify domain association). */
36
+ siteOrigin?: string;
35
37
  staging?: boolean;
36
38
  stagingPreview?: StagingPreviewConfig;
37
39
  cognitoDomainPrefix: string;
@@ -42,6 +44,12 @@ export type CmsConfig = {
42
44
  stagingDomainPrefix?: string;
43
45
  redirects?: CmsRedirect[];
44
46
  prospectusAccountsApiStageArn?: string;
47
+ /**
48
+ * The site is a public-reader site that polls Prospectus for retired reader subjects. The template then generates a
49
+ * shared secret that the scheduled snapshot run presents to the CMS route `/api/reader/retired` (requires
50
+ * `prospectusAccountsApiStageArn`; docs/reader-steering.md).
51
+ */
52
+ readerRetirementPoll?: boolean;
45
53
  deployStackInBuild?: boolean;
46
54
  };
47
55
 
@@ -53,6 +61,7 @@ export type AmplifyAppShellSiteConfig = {
53
61
  frontend: "pretext" | "markus-static";
54
62
  hostedZoneId?: string;
55
63
  stackName?: string;
64
+ aws?: SiteAwsConfig;
56
65
  cms?: CmsConfig;
57
66
  reader?: {
58
67
  appName?: string;
@@ -69,20 +78,67 @@ export type AmplifyAppShellSiteConfig = {
69
78
  };
70
79
  papyrusVersion: string;
71
80
  storagePreviewPrefix?: string;
81
+ /**
82
+ * The scheduled cloud knowledge run for this publication. The app-shell stack ignores it; it is validated by
83
+ * infra/scheduled-jobs/lib/knowledge-run-config.ts, which owns the schema.
84
+ */
85
+ knowledgeRun?: unknown;
86
+ /**
87
+ * Merge-to-main deploy from GitHub Actions through OIDC. When true (the default) the stack creates the
88
+ * `<siteId>-github-deploy` role and turns off Amplify's own auto-build on the production branches, so the
89
+ * public repository's workflow is the only trigger. Set false for a site whose repository is private:
90
+ * private repositories get no Actions minutes, so Amplify's own repository-connected auto-build stays on.
91
+ */
92
+ autoDeploy: boolean;
72
93
  github?: {
73
94
  owner: string;
74
95
  repo: string;
75
96
  branches: string[];
76
97
  ciCanDeployInfra: boolean;
98
+ /**
99
+ * GitHub's numeric ids of the owner and the repository. Repositories with the immutable OIDC subject
100
+ * (`repo:<owner>@<ownerId>/<repo>@<repoId>:ref:...`, see `gh api repos/<owner>/<repo>/actions/oidc/customization/sub`)
101
+ * present that form instead of `repo:<owner>/<repo>:ref:...`; with both ids set, roles trust both forms.
102
+ */
103
+ ownerId?: number;
104
+ repoId?: number;
77
105
  };
78
106
  };
79
107
 
108
+ /**
109
+ * Where the site's AWS resources live. `bin/app-shell.ts` deploys the stack into exactly this account and
110
+ * region (see `resolveDeployEnvironment`), and `papyrus site google-secrets` asserts `accountId` with STS
111
+ * before it writes anything.
112
+ */
113
+ export type SiteAwsConfig = {
114
+ profile?: string;
115
+ accountId?: string;
116
+ region?: string;
117
+ };
118
+
80
119
  export type CmsSiteConfig = AmplifyAppShellSiteConfig & { cms: CmsConfig };
81
120
 
82
121
  export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteConfig {
83
122
  return config.cms !== undefined;
84
123
  }
85
124
 
125
+ export function resolveGithubRepository(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">): { owner: string; repo: string } {
126
+ if (config.github) return { owner: config.github.owner, repo: config.github.repo };
127
+ const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
128
+ return { owner, repo };
129
+ }
130
+
131
+ /** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
132
+ export function githubOidcSubjects(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">, ref: string): string[] {
133
+ const { owner, repo } = resolveGithubRepository(config);
134
+ const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
135
+ const { ownerId, repoId } = config.github ?? {};
136
+ if (ownerId !== undefined && repoId !== undefined) {
137
+ subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
138
+ }
139
+ return subjects;
140
+ }
141
+
86
142
  export const DEFAULT_CI_BRANCHES = ["main", "staging"];
87
143
 
88
144
  const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -90,6 +146,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
90
146
  const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
91
147
  const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
92
148
  const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
149
+ const SITE_ORIGIN_PATTERN = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(:\d{1,5})?$/;
93
150
  const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
94
151
  const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
95
152
  const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
@@ -100,7 +157,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
100
157
  const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
101
158
  const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
102
159
  const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
103
- const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_ARN", "PAPYRUS_PROSPECTUS_SITE_PRODUCT"];
160
+ 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_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
104
161
 
105
162
  /** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
106
163
  export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
@@ -127,6 +184,14 @@ function originOfRedirectUrl(value: string): string {
127
184
  }
128
185
  }
129
186
 
187
+ export function resolveProspectusSessionSealingParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
188
+ return `/papyrus/${config.siteId}/prospectus-session-sealing`;
189
+ }
190
+
191
+ export function resolveReaderPollSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
192
+ return `/papyrus/${config.siteId}/reader-poll-secret`;
193
+ }
194
+
130
195
  export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
131
196
  return `/papyrus/${config.siteId}/revalidate-secret`;
132
197
  }
@@ -251,6 +316,10 @@ export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string |
251
316
  return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
252
317
  }
253
318
 
319
+ export function resolveSiteOrigin(config: Pick<CmsSiteConfig, "cms">): string | undefined {
320
+ return config.cms.siteOrigin;
321
+ }
322
+
254
323
  export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
255
324
  if (!isStagingEnabled(config)) return undefined;
256
325
  if (config.cms.domainPrefix) {
@@ -282,7 +351,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
282
351
 
283
352
  function parseCms(value: unknown): CmsConfig {
284
353
  const cmsRecord = requireObject(value, "cms");
285
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
354
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
286
355
  const buildComputeType = cmsRecord.buildComputeType;
287
356
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
288
357
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -293,6 +362,12 @@ function parseCms(value: unknown): CmsConfig {
293
362
  if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
294
363
  fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
295
364
  }
365
+ if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
366
+ fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
367
+ }
368
+ if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
369
+ fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
370
+ }
296
371
  if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
297
372
  fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
298
373
  }
@@ -300,6 +375,7 @@ function parseCms(value: unknown): CmsConfig {
300
375
  appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
301
376
  domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
302
377
  domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
378
+ siteOrigin: optionalString(cmsRecord.siteOrigin, "cms.siteOrigin", SITE_ORIGIN_PATTERN, "https origin with no path, query or trailing slash, such as https://shipped.anth.us"),
303
379
  staging: cmsRecord.staging as boolean | undefined,
304
380
  cognitoDomainPrefix: requireString(
305
381
  cmsRecord.cognitoDomainPrefix,
@@ -314,6 +390,7 @@ function parseCms(value: unknown): CmsConfig {
314
390
  stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
315
391
  redirects: parseRedirects(cmsRecord.redirects),
316
392
  deployStackInBuild: cmsRecord.deployStackInBuild as boolean | undefined,
393
+ readerRetirementPoll: cmsRecord.readerRetirementPoll as boolean | undefined,
317
394
  stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
318
395
  prospectusAccountsApiStageArn: optionalString(
319
396
  cmsRecord.prospectusAccountsApiStageArn,
@@ -329,10 +406,13 @@ function parseCms(value: unknown): CmsConfig {
329
406
  }
330
407
  for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
331
408
  if (key in cms.environment) {
332
- fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
409
+ fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn and the public site origin comes from the site host");
333
410
  }
334
411
  }
335
412
 
413
+ if (cms.siteOrigin && cms.domainName) {
414
+ fail("cms.siteOrigin", "is for sites whose host is served by their own edge; remove it when cms.domainName is set");
415
+ }
336
416
  if (cms.staging === false && cms.stagingDomainName) {
337
417
  fail("cms.stagingDomainName", "must not be set when cms.staging is false");
338
418
  }
@@ -395,11 +475,22 @@ function validateOauthRedirectUrls(config: CmsSiteConfig): void {
395
475
  }
396
476
  }
397
477
 
478
+ function parseAws(value: unknown): SiteAwsConfig | undefined {
479
+ if (value === undefined) return undefined;
480
+ const record = requireObject(value, "aws");
481
+ rejectUnknownKeys(record, ["profile", "accountId", "region"], "aws");
482
+ return {
483
+ profile: optionalString(record.profile, "aws.profile", /^[A-Za-z0-9_.-]+$/, "AWS CLI profile name"),
484
+ accountId: optionalString(record.accountId, "aws.accountId", /^\d{12}$/, "12-digit account id"),
485
+ region: optionalString(record.region, "aws.region", /^[a-z]{2}(-[a-z]+)+-\d$/, "AWS region such as us-east-1"),
486
+ };
487
+ }
488
+
398
489
  export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
399
490
  const record = requireObject(raw, "site");
400
491
  rejectUnknownKeys(
401
492
  record,
402
- ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
493
+ ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "aws", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "knowledgeRun", "autoDeploy", "github"],
403
494
  "site",
404
495
  );
405
496
 
@@ -415,6 +506,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
415
506
  }
416
507
  const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
417
508
  const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
509
+ const aws = parseAws(record.aws);
418
510
  const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
419
511
  const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
420
512
 
@@ -452,11 +544,14 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
452
544
  fail("cms.stagingPreview", "must not be set when cms.staging is false");
453
545
  }
454
546
 
547
+ const autoDeploy = record.autoDeploy ?? true;
548
+ if (typeof autoDeploy !== "boolean") fail("autoDeploy", "must be a boolean");
549
+
455
550
  let github: AmplifyAppShellSiteConfig["github"];
456
551
  if (record.github !== undefined) {
457
552
  if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
458
553
  const githubRecord = requireObject(record.github, "github");
459
- rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
554
+ rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
460
555
  const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
461
556
  const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
462
557
  if (repository !== `https://github.com/${owner}/${repo}`) {
@@ -476,7 +571,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
476
571
  }
477
572
  const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
478
573
  if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
479
- github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
574
+ const optionalId = (value: unknown, name: string): number | undefined => {
575
+ if (value === undefined) return undefined;
576
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) fail(name, "must be a positive integer (GitHub numeric id)");
577
+ return value as number;
578
+ };
579
+ const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
580
+ const repoId = optionalId(githubRecord.repoId, "github.repoId");
581
+ if ((ownerId === undefined) !== (repoId === undefined)) fail("github", "ownerId and repoId must be set together");
582
+ github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
480
583
  }
481
584
 
482
585
  const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
@@ -495,14 +598,34 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
495
598
  frontend,
496
599
  hostedZoneId,
497
600
  stackName,
601
+ aws,
498
602
  cms,
499
603
  reader,
500
604
  papyrusVersion,
501
605
  storagePreviewPrefix,
606
+ autoDeploy: autoDeploy as boolean,
502
607
  github,
608
+ ...(record.knowledgeRun !== undefined ? { knowledgeRun: record.knowledgeRun } : {}),
503
609
  };
504
610
 
505
611
  if (hasCmsApp(config)) validateOauthRedirectUrls(config);
506
612
 
507
613
  return config;
508
614
  }
615
+
616
+ /**
617
+ * The CDK environment for a site's app-shell stack, taken only from the site's own `aws` block. There is
618
+ * deliberately no default account: a deploy that guesses an account can land in, or fail against, the wrong
619
+ * one.
620
+ */
621
+ export function resolveDeployEnvironment(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "aws">): { account: string; region: string } {
622
+ const account = config.aws?.accountId;
623
+ const region = config.aws?.region;
624
+ const missing = [account ? "" : "aws.accountId", region ? "" : "aws.region"].filter(Boolean);
625
+ if (!account || !region) {
626
+ throw new Error(
627
+ `site "${config.siteId}" does not say where it deploys: add ${missing.join(" and ")} to its site.json, e.g. "aws": { "accountId": "<12-digit account>", "region": "us-east-1" }. No account is assumed.`,
628
+ );
629
+ }
630
+ return { account, region };
631
+ }
@@ -200,7 +200,6 @@ export type ProcedureRunRecord = {
200
200
  newsroomFeedKey?: string | null;
201
201
  };
202
202
 
203
- const DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-newsroom-sections.yml");
204
203
  const NEWSROOM_SECTION_TYPES = new Set<NewsroomSectionType>(["canonical", "floating", "rotating"]);
205
204
  type NewsroomSectionSeedRow = Omit<NewsroomSectionRecord, "sortOrder" | "enabled" | "enabledStatus" | "createdAt" | "updatedAt"> & {
206
205
  enabled: boolean;
@@ -690,10 +689,10 @@ export type CategorySteeringDashboard = {
690
689
  loadError?: string | null;
691
690
  };
692
691
 
693
- const DEFAULT_ANALYSIS_PROFILES_PATH = path.join(process.cwd(), "corpora", "papyrus-analysis-profiles.yml");
694
- const DEFAULT_STEERING_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-steering.yml");
695
692
 
696
- export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_PROFILES_PATH): Promise<AnalysisProfileSummary[]> {
693
+ /** Analysis profiles the site keeps in a file; a site that configures none (data-driven sites) has none. No default file is looked up. */
694
+ export async function loadAnalysisProfileSummaries(filepath?: string): Promise<AnalysisProfileSummary[]> {
695
+ if (!filepath) return [];
697
696
  try {
698
697
  const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
699
698
  schemaVersion?: number;
@@ -722,7 +721,9 @@ export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_P
722
721
  }
723
722
  }
724
723
 
725
- export async function loadConfiguredCorpusSummaries(filepath = DEFAULT_STEERING_CONFIG_PATH): Promise<CategorySteeringCorpus[]> {
724
+ /** Corpora a site lists in a steering file; no configured file means none. No default file is looked up. */
725
+ export async function loadConfiguredCorpusSummaries(filepath?: string): Promise<CategorySteeringCorpus[]> {
726
+ if (!filepath) return [];
726
727
  try {
727
728
  const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
728
729
  corpora?: Array<Record<string, unknown>>;
@@ -818,7 +819,8 @@ function sortCategorys(categorys: CategorySteeringCategory[]): CategorySteeringC
818
819
  });
819
820
  }
820
821
 
821
- function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH): NewsroomSectionRecord[] {
822
+ function defaultNewsroomSections(importedAt: string, sectionsConfigPath?: string): NewsroomSectionRecord[] {
823
+ if (!sectionsConfigPath) return [];
822
824
  return loadNewsroomSectionSeedRows(sectionsConfigPath).map((row, index) => ({
823
825
  ...row,
824
826
  enabledStatus: "enabled",
@@ -828,7 +830,7 @@ function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAUL
828
830
  }));
829
831
  }
830
832
 
831
- function loadNewsroomSectionSeedRows(sectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH) {
833
+ function loadNewsroomSectionSeedRows(sectionsConfigPath: string) {
832
834
  const cached = newsroomSectionSeedRowsCache.get(sectionsConfigPath);
833
835
  if (cached) return cached;
834
836
  const parsed = YAML.parse(fs.readFileSync(sectionsConfigPath, "utf8")) as {
@@ -883,7 +885,7 @@ function normalizeNewsroomSectionSeedRow(
883
885
 
884
886
  export function createDemoCategorySteeringDashboard(
885
887
  brandId: SiteBrandId = resolveSiteBrandId(),
886
- newsroomSectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH,
888
+ newsroomSectionsConfigPath?: string,
887
889
  ): CategorySteeringDashboard {
888
890
  const importedAt = "2026-05-16T12:00:00.000Z";
889
891
  const profile = getNewsroomDemoProfile(brandId);
@@ -1698,7 +1700,7 @@ export function createDemoCategorySteeringDashboard(
1698
1700
  question: "Should this candidate source become a reference for this publication?",
1699
1701
  reviewControl: "thumbs",
1700
1702
  reviewReason: "program",
1701
- reviewDetail: "Every decision is reviewed while the cyclotron is onboarding.",
1703
+ reviewDetail: "Every suggestion is reviewed while suggestions are getting started.",
1702
1704
  version: 1,
1703
1705
  }),
1704
1706
  },