@anthusai/papyrus 2.0.0-next.10 → 2.0.0-next.100

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 (230) hide show
  1. package/amplify/auth/resource.js +16 -1
  2. package/amplify/auth/resource.ts +19 -1
  3. package/amplify/data/function-handler.js +20 -0
  4. package/amplify/data/function-handler.ts +40 -0
  5. package/amplify/data/resource.js +152 -62
  6. package/amplify/data/resource.ts +158 -62
  7. package/amplify/data/schema.js +2102 -1986
  8. package/amplify/data/schema.ts +199 -70
  9. package/amplify/functions/assignment-action/data-access.js +2 -0
  10. package/amplify/functions/assignment-action/data-access.ts +2 -0
  11. package/amplify/functions/assignment-action/handler.ts +1 -1
  12. package/amplify/functions/automation-view/handler.py +17 -0
  13. package/amplify/functions/automation-view/resource.js +24 -0
  14. package/amplify/functions/automation-view/resource.ts +34 -0
  15. package/amplify/functions/automation-view/schema.js +26 -0
  16. package/amplify/functions/automation-view/schema.ts +28 -0
  17. package/amplify/functions/automation-view/wire.js +56 -0
  18. package/amplify/functions/automation-view/wire.ts +76 -0
  19. package/amplify/functions/content-actions/handler.py +64 -3
  20. package/amplify/functions/content-actions/requirements.txt +1 -0
  21. package/amplify/functions/content-actions/resource.js +1 -1
  22. package/amplify/functions/content-actions/resource.ts +1 -1
  23. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  25. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
  26. package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
  27. package/amplify/functions/editorial-checks/schema.js +122 -0
  28. package/amplify/{plugins → functions}/editorial-checks/schema.ts +40 -12
  29. package/amplify/functions/editorial-checks/wire.js +53 -0
  30. package/amplify/functions/editorial-checks/wire.ts +61 -0
  31. package/amplify/functions/monitoring-view/handler.py +11 -0
  32. package/amplify/functions/monitoring-view/resource.js +24 -0
  33. package/amplify/functions/monitoring-view/resource.ts +34 -0
  34. package/amplify/functions/monitoring-view/schema.js +29 -0
  35. package/amplify/functions/monitoring-view/schema.ts +31 -0
  36. package/amplify/functions/monitoring-view/wire.js +64 -0
  37. package/amplify/functions/monitoring-view/wire.ts +81 -0
  38. package/amplify/functions/public-forms/core.js +247 -0
  39. package/amplify/functions/public-forms/core.ts +310 -0
  40. package/amplify/functions/public-forms/handler.ts +41 -0
  41. package/amplify/functions/public-forms/origins.js +35 -0
  42. package/amplify/functions/public-forms/origins.ts +43 -0
  43. package/amplify/functions/public-forms/resource.js +9 -0
  44. package/amplify/functions/public-forms/resource.ts +10 -0
  45. package/amplify/functions/public-forms/wire.js +25 -0
  46. package/amplify/functions/public-forms/wire.ts +32 -0
  47. package/amplify/functions/shared/editorial-context.ts +63 -4
  48. package/amplify/functions/shared/python-bundle.js +20 -14
  49. package/amplify/functions/shared/python-bundle.ts +25 -18
  50. package/amplify/plugins/newsletter/config.js +245 -0
  51. package/amplify/plugins/newsletter/config.ts +305 -0
  52. package/amplify/plugins/newsletter/function/handler.py +6 -0
  53. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  54. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  55. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  56. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  57. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  58. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  59. package/amplify/plugins/newsletter/schema.js +57 -0
  60. package/amplify/plugins/newsletter/schema.ts +62 -0
  61. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  62. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  63. package/amplify/plugins/newsletter/sending.js +140 -0
  64. package/amplify/plugins/newsletter/sending.ts +186 -0
  65. package/amplify/plugins/reader-steering/config.js +182 -0
  66. package/amplify/plugins/reader-steering/config.ts +290 -0
  67. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  68. package/amplify/plugins/reader-steering/function/handler.py +1567 -0
  69. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  70. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  71. package/amplify/plugins/reader-steering/function/proposals.py +458 -0
  72. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  73. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  74. package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
  76. package/amplify/plugins/reader-steering/schema.js +229 -0
  77. package/amplify/plugins/reader-steering/schema.ts +231 -0
  78. package/amplify/plugins/registry.js +4 -2
  79. package/amplify/plugins/registry.ts +4 -2
  80. package/amplify/plugins/signup/schema.js +2 -1
  81. package/amplify/plugins/signup/schema.ts +2 -1
  82. package/amplify/plugins/types.ts +5 -0
  83. package/amplify/plugins/wire.js +1 -0
  84. package/amplify/plugins/wire.ts +1 -0
  85. package/amplify/publication/projection-lease.js +30 -0
  86. package/amplify/publication/projection-lease.ts +39 -0
  87. package/amplify/publication/redirect-and-index.js +105 -0
  88. package/amplify/publication/redirect-rules.js +73 -0
  89. package/amplify/publication/redirect-rules.ts +93 -0
  90. package/amplify/publication/s3-projection.js +7 -1
  91. package/amplify/publication/s3-projection.ts +9 -1
  92. package/amplify/site-backend-config.js +19 -0
  93. package/amplify/site-backend-config.ts +45 -1
  94. package/amplify/site-backend-names.js +4 -2
  95. package/amplify/site-backend-names.ts +4 -2
  96. package/amplify/site-backend.js +46 -4
  97. package/amplify/site-backend.ts +49 -4
  98. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  99. package/app/api/newsletter/confirm/route.ts +13 -0
  100. package/app/api/newsletter/preferences/route.ts +13 -0
  101. package/app/api/newsletter/state/route.ts +9 -0
  102. package/app/api/newsletter/subscribe/route.ts +9 -0
  103. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  104. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  105. package/app/api/newsroom/curation/context/route.ts +9 -0
  106. package/app/api/newsroom/curation/priming/route.ts +9 -0
  107. package/app/api/newsroom/curation/publication/route.ts +9 -0
  108. package/app/api/reader/comment/delete/route.ts +9 -0
  109. package/app/api/reader/comment/edit/route.ts +9 -0
  110. package/app/api/reader/comment/route.ts +9 -0
  111. package/app/api/reader/flag/route.ts +9 -0
  112. package/app/api/reader/handle/route.ts +9 -0
  113. package/app/api/reader/me/route.ts +9 -0
  114. package/app/api/reader/rating/route.ts +9 -0
  115. package/app/api/reader/report/route.ts +9 -0
  116. package/app/api/reader/retired/route.ts +9 -0
  117. package/app/api/reader/submit/route.ts +9 -0
  118. package/app/api/reader/vote/route.ts +9 -0
  119. package/app/layout.tsx +5 -1
  120. package/bin/papyrus-infra.mjs +20 -1
  121. package/components/automation-panel.tsx +49 -0
  122. package/components/automation-run-now.tsx +97 -0
  123. package/components/automation-view.tsx +195 -0
  124. package/components/editorial-checks-admin-panel.tsx +38 -13
  125. package/components/moderation-panel.tsx +248 -0
  126. package/components/monitoring-overview-view.tsx +414 -0
  127. package/components/monitoring-panel.tsx +60 -0
  128. package/components/news-desk-client-provider.tsx +18 -18
  129. package/components/news-desk-taxonomy-client.ts +28 -8
  130. package/components/newsletter-panel.tsx +474 -0
  131. package/components/newsletter-sections.tsx +163 -0
  132. package/components/newsroom-access-gate.tsx +16 -2
  133. package/components/newsroom-article-editor.tsx +11 -11
  134. package/components/newsroom-articles-view.tsx +6 -6
  135. package/components/newsroom-assignment-detail.tsx +6 -6
  136. package/components/newsroom-board-view.tsx +8 -3
  137. package/components/newsroom-curation-bootstrap.tsx +45 -0
  138. package/components/newsroom-ops-overview.tsx +26 -96
  139. package/components/newsroom-ops-shell.tsx +2 -2
  140. package/components/newsroom-organizer.css +33 -0
  141. package/components/newsroom-overview-skeleton.tsx +25 -0
  142. package/components/newsroom-references-view.tsx +192 -49
  143. package/components/notification-center.tsx +281 -0
  144. package/components/papyrus-console-conversation.css +14 -0
  145. package/components/papyrus-console-conversation.tsx +55 -0
  146. package/components/papyrus-console-shell.tsx +62 -420
  147. package/components/papyrus-private-chat-panel.tsx +134 -0
  148. package/components/publication-cyclotron-panel.tsx +15 -10
  149. package/components/reader-auth-state.ts +1 -1
  150. package/components/reference-add-entry.tsx +75 -0
  151. package/components/reference-curation-controls.tsx +31 -22
  152. package/components/reference-cyclotron-priming.tsx +58 -19
  153. package/components/reference-cyclotron-suggestion.tsx +10 -6
  154. package/components/reference-priming.tsx +14 -10
  155. package/components/reference-relevance-review.tsx +3 -3
  156. package/components/reference-section-suggestion.tsx +1 -1
  157. package/components/topic-steering-workspace.tsx +237 -158
  158. package/components/use-editorial-checks.ts +12 -10
  159. package/infra/amplify-app-shell.js +96 -16
  160. package/infra/amplify-app-shell.ts +108 -18
  161. package/infra/cloud-email-config.js +32 -0
  162. package/infra/cloud-email-config.ts +39 -0
  163. package/infra/cloud-email.js +57 -0
  164. package/infra/cloud-email.test.js +69 -0
  165. package/infra/cloud-email.test.ts +66 -0
  166. package/infra/cloud-email.ts +60 -0
  167. package/infra/index.js +1 -0
  168. package/infra/secure-string-parameter-handler.js +41 -0
  169. package/infra/secure-string-parameter-handler.ts +41 -0
  170. package/infra/secure-string-parameter.js +79 -0
  171. package/infra/secure-string-parameter.ts +91 -0
  172. package/infra/site-config.js +92 -6
  173. package/infra/site-config.ts +138 -6
  174. package/lib/automation-client.ts +54 -0
  175. package/lib/automation-fixture.ts +93 -0
  176. package/lib/automation-overview.ts +252 -0
  177. package/lib/category-repository.ts +14 -12
  178. package/lib/console-web-context.ts +1 -1
  179. package/lib/curation-routes.ts +345 -0
  180. package/lib/cyclotron-suggestions.ts +4 -4
  181. package/lib/editorial-checks-backend.ts +1 -1
  182. package/lib/editorial-checks-client.ts +19 -1
  183. package/lib/editorial-checks-demo.ts +2 -2
  184. package/lib/editorial-checks-settings.ts +3 -0
  185. package/lib/editorial-checks.ts +8 -11
  186. package/lib/editorial-preview.ts +5 -5
  187. package/lib/monitoring-client.ts +83 -0
  188. package/lib/monitoring-overview-fixture.ts +51 -0
  189. package/lib/monitoring-overview.ts +188 -0
  190. package/lib/news-desk-session.ts +12 -21
  191. package/lib/newsletter-api.ts +418 -0
  192. package/lib/newsletter-client.ts +46 -0
  193. package/lib/newsletter-fixture.ts +184 -0
  194. package/lib/newsletter-model.ts +202 -0
  195. package/lib/newsroom-demo-dashboard.ts +1 -1
  196. package/lib/newsroom-nav.ts +1 -1
  197. package/lib/newsroom-organizer-nav.ts +2 -1
  198. package/lib/papyrus-chat-admission.ts +65 -0
  199. package/lib/papyrus-hosted-chat.ts +217 -0
  200. package/lib/papyrus-private-chat-client.ts +81 -0
  201. package/lib/prospectus-session.ts +30 -14
  202. package/lib/prospectus-sign-in-routes.ts +106 -31
  203. package/lib/prospectus-site-identity.ts +46 -6
  204. package/lib/reader-api.ts +145 -0
  205. package/lib/reader-moderation.ts +220 -0
  206. package/lib/reader-retirement-route.ts +89 -0
  207. package/lib/reader-subject.md +29 -0
  208. package/lib/reader-subject.ts +53 -0
  209. package/lib/reference-review-details.ts +71 -0
  210. package/lib/reference-review-request.ts +14 -0
  211. package/lib/relevance-decisions.ts +42 -10
  212. package/lib/relevance-status.ts +2 -2
  213. package/lib/review-client.ts +120 -0
  214. package/lib/review-fixture.ts +45 -0
  215. package/lib/review-requests.ts +246 -0
  216. package/lib/semantic-graph.ts +2 -0
  217. package/lib/shared-chat-admission.ts +182 -0
  218. package/lib/site-brand-tenant.ts +11 -6
  219. package/lib/site-brand.ts +6 -3
  220. package/lib/site-origin-value.ts +50 -0
  221. package/lib/site-origin.ts +8 -0
  222. package/lib/use-reference-review-data.ts +44 -0
  223. package/middleware.ts +14 -3
  224. package/package.json +65 -3
  225. package/routes.manifest.json +227 -2
  226. package/src/with-papyrus.mjs +1 -1
  227. package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
  228. package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -71
  229. package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -87
  230. package/amplify/plugins/editorial-checks/schema.js +0 -94
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ /** One pattern redirect as the edge function reads it: regex, target with $1..$9, status, excluded paths. */
5
+ export type EdgeRedirectRule = { p: string; t: string; s: number; x: string[] };
6
+
7
+ /** Mirrors DIRECTORY_HTML_PATTERN and DIRECTORY_SLASH_PATTERN in standard/config.py. */
8
+ const DIRECTORY_HTML_PATTERN = "^/(?!(?:.*/)?index\\.html$)(.+)\\.html$";
9
+ const DIRECTORY_SLASH_PATTERN = "^/((?:[^/]+/)*[^/.]+)$";
10
+
11
+ const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
12
+ const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
13
+
14
+ type ProjectionRedirects = { patterns?: { pattern: string; to: string; status?: number; except?: string[] }[] };
15
+ type ProjectionSettings = { site?: unknown; urls?: { style?: string }; redirects?: ProjectionRedirects; agents?: { markdown?: boolean } };
16
+
17
+ /**
18
+ * The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
19
+ * `urls.style: "directory"`, `/x.html` to `/x/`. Mirrors `StandardSettings.edge_rules` in Python.
20
+ */
21
+ export function edgeRulesFromSettings(settings: ProjectionSettings): EdgeRedirectRule[] {
22
+ const rules = (settings.redirects?.patterns ?? []).map((rule) => ({
23
+ p: rule.pattern,
24
+ t: rule.to,
25
+ s: rule.status ?? 301,
26
+ x: rule.except ?? [],
27
+ }));
28
+ if (settings.urls?.style === "directory") {
29
+ // /x.html to /x/ (a directory index file such as /x/index.html is served as it is), then /x to /x/.
30
+ rules.push({ p: DIRECTORY_HTML_PATTERN, t: "/$1/", s: 301, x: ["/404.html"] });
31
+ rules.push({ p: DIRECTORY_SLASH_PATTERN, t: "/$1/", s: 301, x: ["/404"] });
32
+ }
33
+ return rules;
34
+ }
35
+
36
+ /** A standard-front-end site publishes Markdown twins unless it turns `agents.markdown` off. */
37
+ export function negotiatesMarkdown(settings: ProjectionSettings): boolean {
38
+ return settings.site !== undefined && settings.agents?.markdown !== false;
39
+ }
40
+
41
+ /** A directory-style site keeps a page's Markdown twin at `/x/index.md`; the edge then serves a bare `/x.md` from there. */
42
+ export function usesDirectoryTwins(settings: ProjectionSettings): boolean {
43
+ return negotiatesMarkdown(settings) && settings.urls?.style === "directory";
44
+ }
45
+
46
+ export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean; directoryTwins: boolean };
47
+
48
+ export function readEdgeConfig(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeConfig {
49
+ if (staticDir === undefined) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
50
+ const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
51
+ if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
52
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings;
53
+ return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings), directoryTwins: usesDirectoryTwins(settings) };
54
+ }
55
+
56
+ export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
57
+ return readEdgeConfig(staticDir, baseDirectory).rules;
58
+ }
59
+
60
+ const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
61
+ const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
62
+ const TWINS_START = "/*PAPYRUS_DIRECTORY_TWINS*/";
63
+ const TWINS_END = "/*END_PAPYRUS_DIRECTORY_TWINS*/";
64
+
65
+ function spliceFlag(source: string, start: string, end: string, label: string): string {
66
+ const from = source.indexOf(start);
67
+ const to = source.indexOf(end);
68
+ if (from === -1 || to === -1 || to < from) {
69
+ throw new Error(`redirect function source has no ${label} markers`);
70
+ }
71
+ return source.slice(0, from + start.length) + "true" + source.slice(to);
72
+ }
73
+
74
+ /** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
75
+ export function compileRedirectFunctionSource(
76
+ source: string,
77
+ rules: EdgeRedirectRule[],
78
+ options: { negotiateMarkdown?: boolean; directoryTwins?: boolean } = {},
79
+ ): string {
80
+ const start = source.indexOf(START_MARKER);
81
+ const end = source.indexOf(END_MARKER);
82
+ if (start === -1 || end === -1 || end < start) {
83
+ throw new Error("redirect function source has no pattern-rule markers");
84
+ }
85
+ let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
86
+ if (options.negotiateMarkdown === true) {
87
+ compiled = spliceFlag(compiled, NEGOTIATE_START, NEGOTIATE_END, "markdown negotiation");
88
+ }
89
+ if (options.directoryTwins === true) {
90
+ compiled = spliceFlag(compiled, TWINS_START, TWINS_END, "directory twin");
91
+ }
92
+ return compiled;
93
+ }
@@ -7,11 +7,13 @@ import { SqsDestination } from "aws-cdk-lib/aws-lambda-destinations";
7
7
  import * as route53 from "aws-cdk-lib/aws-route53";
8
8
  import * as s3 from "aws-cdk-lib/aws-s3";
9
9
  import * as sqs from "aws-cdk-lib/aws-sqs";
10
+ import { readFileSync } from "node:fs";
10
11
  import { dirname, resolve } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
13
  import { assertValidPublicationConfig } from "../site-backend-config.js";
13
14
  import { addRenditionsFunction } from "./renditions-function.js";
14
15
  import { RENDITIONS_FUNCTION_NAME_ENV } from "./renditions-settings.js";
16
+ import { compileRedirectFunctionSource, readEdgeConfig } from "./redirect-rules.js";
15
17
  import { addInvalidationPathAlarm } from "./invalidation-alarm.js";
16
18
  import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings.js";
17
19
  const redirectFunctionSourcePath = resolve(dirname(fileURLToPath(import.meta.url)), "redirect-and-index.js");
@@ -68,9 +70,13 @@ export function addS3ProjectionPublication(scope, props, media) {
68
70
  assertValidPublicationConfig({ kind: "s3-projection", ...props });
69
71
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
70
72
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
73
+ const edgeConfig = readEdgeConfig(props.staticDir);
71
74
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
72
75
  runtime: cloudfront.FunctionRuntime.JS_2_0,
73
- code: cloudfront.FunctionCode.fromFile({ filePath: redirectFunctionSourcePath }),
76
+ code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
77
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
78
+ directoryTwins: edgeConfig.directoryTwins,
79
+ })),
74
80
  keyValueStore,
75
81
  });
76
82
  const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
@@ -10,11 +10,13 @@ import * as route53 from "aws-cdk-lib/aws-route53";
10
10
  import * as s3 from "aws-cdk-lib/aws-s3";
11
11
  import * as sqs from "aws-cdk-lib/aws-sqs";
12
12
  import type { Construct } from "constructs";
13
+ import { readFileSync } from "node:fs";
13
14
  import { dirname, resolve } from "node:path";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import { assertValidPublicationConfig, type PapyrusPublicationConfig } from "../site-backend-config";
16
17
  import { addRenditionsFunction, type RenditionsFunction } from "./renditions-function";
17
18
  import { RENDITIONS_FUNCTION_NAME_ENV, type RenditionsPlan } from "./renditions-settings";
19
+ import { compileRedirectFunctionSource, readEdgeConfig } from "./redirect-rules";
18
20
  import { addInvalidationPathAlarm } from "./invalidation-alarm";
19
21
  import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings";
20
22
 
@@ -110,9 +112,15 @@ export function addS3ProjectionPublication(
110
112
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
111
113
 
112
114
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
115
+ const edgeConfig = readEdgeConfig(props.staticDir);
113
116
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
114
117
  runtime: cloudfront.FunctionRuntime.JS_2_0,
115
- code: cloudfront.FunctionCode.fromFile({ filePath: redirectFunctionSourcePath }),
118
+ code: cloudfront.FunctionCode.fromInline(
119
+ compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
120
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
121
+ directoryTwins: edgeConfig.directoryTwins,
122
+ }),
123
+ ),
116
124
  keyValueStore,
117
125
  });
118
126
  const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
@@ -1,4 +1,23 @@
1
1
  export const MAXIMUM_HTML_S_MAXAGE_SECONDS = 86400;
2
+ const MONITOR_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
3
+ export function validateMonitoringConfig(monitoring) {
4
+ if (monitoring === undefined)
5
+ return [];
6
+ if (typeof monitoring !== "object" || monitoring === null || Array.isArray(monitoring)) {
7
+ return ["monitoring must be an object"];
8
+ }
9
+ const config = monitoring;
10
+ if (config.emailMonitorId !== undefined
11
+ && (typeof config.emailMonitorId !== "string" || !MONITOR_ID_PATTERN.test(config.emailMonitorId))) {
12
+ return ["monitoring.emailMonitorId must be a lowercase monitor id (1-63 letters, digits, or hyphens)"];
13
+ }
14
+ return [];
15
+ }
16
+ export function assertValidMonitoringConfig(monitoring) {
17
+ const problems = validateMonitoringConfig(monitoring);
18
+ if (problems.length > 0)
19
+ throw new Error(`Invalid backend.monitoring: ${problems.join("; ")}`);
20
+ }
2
21
  const CLOUDFRONT_DISTRIBUTION_ARN = /^arn:aws[a-z-]*:cloudfront::\d{12}:distribution\/[A-Z0-9]+$/;
3
22
  function isRelativePathWithoutParentSegments(value) {
4
23
  if (value === "" || value.startsWith("/") || value.includes("\\"))
@@ -40,6 +40,31 @@ export type PapyrusPublicationCacheConfig = {
40
40
 
41
41
  export const MAXIMUM_HTML_S_MAXAGE_SECONDS = 86400;
42
42
 
43
+ export type PapyrusMonitoringConfig = {
44
+ /** Existing shared Monitoring partition that stores this site's private email receipts. */
45
+ emailMonitorId?: string;
46
+ };
47
+
48
+ const MONITOR_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
49
+
50
+ export function validateMonitoringConfig(monitoring: unknown): string[] {
51
+ if (monitoring === undefined) return [];
52
+ if (typeof monitoring !== "object" || monitoring === null || Array.isArray(monitoring)) {
53
+ return ["monitoring must be an object"];
54
+ }
55
+ const config = monitoring as { emailMonitorId?: unknown };
56
+ if (config.emailMonitorId !== undefined
57
+ && (typeof config.emailMonitorId !== "string" || !MONITOR_ID_PATTERN.test(config.emailMonitorId))) {
58
+ return ["monitoring.emailMonitorId must be a lowercase monitor id (1-63 letters, digits, or hyphens)"];
59
+ }
60
+ return [];
61
+ }
62
+
63
+ export function assertValidMonitoringConfig(monitoring: unknown): void {
64
+ const problems = validateMonitoringConfig(monitoring);
65
+ if (problems.length > 0) throw new Error(`Invalid backend.monitoring: ${problems.join("; ")}`);
66
+ }
67
+
43
68
  export type PapyrusPublicationConfig = {
44
69
  kind: "s3-projection";
45
70
  /** Site repo path (relative, `.py`) of the SiteProjector bundled into content-actions as `site_projector`. */
@@ -138,6 +163,8 @@ export type PapyrusSiteBackendConfig = {
138
163
  */
139
164
  productionAppId?: string;
140
165
  auth?: PapyrusAuthConfig;
166
+ /** Shared Monitoring partition used by private email intake for this publication. */
167
+ monitoring?: PapyrusMonitoringConfig;
141
168
  /**
142
169
  * Maximum number of data model nested stacks CloudFormation creates at once.
143
170
  * Stacks are chained in waves of this size so a new AWS account is not hit
@@ -145,6 +172,13 @@ export type PapyrusSiteBackendConfig = {
145
172
  * DependsOn ordering; no resource changes. Default 12; 0 turns it off.
146
173
  */
147
174
  resolverWaveSize?: number;
175
+ /**
176
+ * Optional model groups to leave out of the data API (see `OPTIONAL_MODEL_GROUPS`; today `editions`).
177
+ * CloudFormation allows 2,500 resources per stack operation, nested stacks included, and a fresh create of the full
178
+ * model set exceeds it. Deploy a new site once with a group excluded, then remove the exclusion and deploy again:
179
+ * the second deploy is an ordinary update that adds the models.
180
+ */
181
+ excludeModelGroups?: string[];
148
182
  /** Static reader app (Markus-static sites) that a publish rebuilds through IAM `amplify:StartJob`. */
149
183
  reader?: { amplifyAppId: string; branchName?: string };
150
184
  /** Lets the newsroom start the `staging` branch build of this CMS app. */
@@ -159,7 +193,7 @@ export type PapyrusSiteBackendConfig = {
159
193
  /**
160
194
  * SES receipt options for a site that is not the legacy p.apyr.us backend.
161
195
  * By default the site gets its own brand-named rule set that is never
162
- * activated by CloudFormation (SES allows one active rule set per region).
196
+ * activated by CloudFormation (SES allows one active rule set per account/region).
163
197
  */
164
198
  inboundEmailSes?: {
165
199
  /** Add this site's rule to a rule set another stack owns instead of creating one. */
@@ -178,6 +212,16 @@ export type PapyrusSiteBackendConfig = {
178
212
  * Unset: no plugin, and the synthesized backend is exactly what it was before plugins existed.
179
213
  */
180
214
  plugins?: PluginRequest[];
215
+ /**
216
+ * The public form receiving endpoint every site gets (docs/public-forms.md). It accepts posts only from the site's
217
+ * own public hosts (distribution aliases, reader domain, revalidate URL, sign-in redirect URLs) plus these.
218
+ */
219
+ publicForms?: {
220
+ /** Extra https origins (or http://localhost) allowed to post, for example `https://www.example.org`. */
221
+ allowedOrigins?: string[];
222
+ /** Extra honeypot field names besides `website`. */
223
+ honeypotFields?: string[];
224
+ };
181
225
  features?: {
182
226
  consoleResponder?: boolean;
183
227
  inboundEmail?: boolean;
@@ -68,8 +68,10 @@ export function deriveStorageBackupVaultName(identity, explicitName) {
68
68
  /**
69
69
  * The `papyrus-site-inbound-` prefix keeps a brand named `p-apyr-us` from
70
70
  * colliding with the legacy `papyrus-inbound-p-apyr-us` set.
71
- * SES allows one ACTIVE receipt rule set per region. A non-legacy site creates
72
- * its own brand-named rule set and never activates it from CloudFormation.
71
+ * SES allows one ACTIVE receipt rule set per account and region. A non-legacy
72
+ * site creates its own brand-named rule set and never activates it from
73
+ * CloudFormation. SES accounts that receive the same domain process their
74
+ * matching rules independently; the limit does not span accounts.
73
75
  */
74
76
  export function deriveReceiptRuleSetName(identity, inboundEmailDomain) {
75
77
  const name = isLegacyProductionApp(identity)
@@ -109,8 +109,10 @@ export function deriveStorageBackupVaultName(identity: SiteBackendIdentity, expl
109
109
  /**
110
110
  * The `papyrus-site-inbound-` prefix keeps a brand named `p-apyr-us` from
111
111
  * colliding with the legacy `papyrus-inbound-p-apyr-us` set.
112
- * SES allows one ACTIVE receipt rule set per region. A non-legacy site creates
113
- * its own brand-named rule set and never activates it from CloudFormation.
112
+ * SES allows one ACTIVE receipt rule set per account and region. A non-legacy
113
+ * site creates its own brand-named rule set and never activates it from
114
+ * CloudFormation. SES accounts that receive the same domain process their
115
+ * matching rules independently; the limit does not span accounts.
114
116
  */
115
117
  export function deriveReceiptRuleSetName(identity: SiteBackendIdentity, inboundEmailDomain: string): string {
116
118
  const name = isLegacyProductionApp(identity)
@@ -11,15 +11,22 @@ import { resolveSitePublication } from "./publication/default-publication.js";
11
11
  import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy.js";
12
12
  import { addProjectionRetries, addS3ProjectionPublication } from "./publication/s3-projection.js";
13
13
  import { planRenditions } from "./publication/renditions-settings.js";
14
+ import { addProjectionLeaseAccess } from "./publication/projection-lease.js";
14
15
  import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
15
16
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
16
- import { assertValidPublicationConfig } from "./site-backend-config.js";
17
+ import { assertValidMonitoringConfig, assertValidPublicationConfig } from "./site-backend-config.js";
17
18
  import { revalidateSecretReadStatement } from "./revalidate-secret-policy.js";
18
19
  import { revalidateDistributionEnvironment, revalidateInvalidationStatement, validateRevalidateDistributionIds, } from "./revalidate-invalidation-settings.js";
19
20
  import { addInvalidationPathAlarm } from "./publication/invalidation-alarm.js";
20
- import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource.js";
21
+ import { applyCognitoDomainPrefix, applyGoogleEmailVerifiedMapping, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource.js";
21
22
  import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client.js";
22
23
  import { defineSiteData } from "./data/resource.js";
24
+ import { editorialChecks } from "./functions/editorial-checks/resource.js";
25
+ import { automationView } from "./functions/automation-view/resource.js";
26
+ import { wireAutomationView } from "./functions/automation-view/wire.js";
27
+ import { monitoringView } from "./functions/monitoring-view/resource.js";
28
+ import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire.js";
29
+ import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
23
30
  import { resolvePlugins } from "./plugins/registry.js";
24
31
  import { wirePlugins } from "./plugins/wire.js";
25
32
  import { assignmentAction } from "./functions/assignment-action/resource.js";
@@ -31,6 +38,9 @@ import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
31
38
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
32
39
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
33
40
  import { newsroomSummary } from "./functions/newsroom-summary/resource.js";
41
+ import { derivePublicFormOrigins } from "./functions/public-forms/origins.js";
42
+ import { publicForms } from "./functions/public-forms/resource.js";
43
+ import { wirePublicForms } from "./functions/public-forms/wire.js";
34
44
  import { procedureAction } from "./functions/procedure-action/resource.js";
35
45
  import { readerSettings } from "./functions/reader-settings/resource.js";
36
46
  import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource.js";
@@ -77,6 +87,8 @@ export function defineSiteBackend(papyrusSite) {
77
87
  const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
78
88
  if (publication !== undefined)
79
89
  assertValidPublicationConfig(publication);
90
+ assertValidMonitoringConfig(site.monitoring);
91
+ const emailMonitorId = site.monitoring?.emailMonitorId ?? "hn";
80
92
  const publicationPlan = planS3ProjectionBackend(publication);
81
93
  const renditionsPlan = planRenditions(publication, process.cwd());
82
94
  setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
@@ -86,15 +98,19 @@ export function defineSiteBackend(papyrusSite) {
86
98
  const backend = defineBackend({
87
99
  ...pluginFunctions,
88
100
  assignmentAction,
101
+ automationView,
89
102
  auth: defineSiteAuth(authConfig),
90
103
  categoryAction,
91
104
  contentActions,
92
- data: defineSiteData(pluginDefinitions),
105
+ editorialChecks,
106
+ data: defineSiteData(pluginDefinitions, site.excludeModelGroups ?? []),
93
107
  knowledgeQuery,
94
108
  manageUserRole,
109
+ monitoringView,
95
110
  modelAttachmentUpload,
96
111
  newsroomSummary,
97
112
  procedureAction,
113
+ publicForms,
98
114
  readerSettings,
99
115
  emailSubmissionProcessor,
100
116
  sesInboundReceive,
@@ -102,6 +118,9 @@ export function defineSiteBackend(papyrusSite) {
102
118
  slackDelivery,
103
119
  storage,
104
120
  });
121
+ if (authConfig.disableGoogleOAuth !== true) {
122
+ applyGoogleEmailVerifiedMapping(backend.auth.stack);
123
+ }
105
124
  orderModelStacksInWaves(backend.data.resources.graphqlApi.node.scope, Object.keys(backend.data.resources.tables), site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE);
106
125
  const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
107
126
  if (authConfig.applyCognitoDomainPrefix === true
@@ -310,10 +329,16 @@ export function defineSiteBackend(papyrusSite) {
310
329
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
311
330
  const contentActionsLambda = backend.contentActions.resources.lambda;
312
331
  grantNewsroomReadWriteDelete(contentActionsLambda);
332
+ if ((activeBrand?.corpusKey ?? "").trim() !== "") {
333
+ // A deploy creates the publication's canonical corpus from the brand when the backend has none.
334
+ contentActionsLambda.addEnvironment("PAPYRUS_PUBLICATION_CORPUS_KEY", (activeBrand?.corpusKey ?? "").trim());
335
+ contentActionsLambda.addEnvironment("PAPYRUS_PUBLICATION_NAME", (activeBrand?.publicationName ?? activeBrand?.corpusKey ?? "").trim());
336
+ }
313
337
  // AppSync validates the token; pending intake additionally binds its actor
314
338
  // and publication to this deployment's pool and existing API endpoint.
315
339
  contentActionsLambda.addEnvironment("PAPYRUS_USER_POOL_ISSUER", `https://cognito-idp.${Stack.of(contentActionsLambda).region}.amazonaws.com/${backend.auth.resources.userPool.userPoolId}`);
316
340
  contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
341
+ wireEmailReferenceIntake(contentActionsLambda, emailMonitorId);
317
342
  contentActionsLambda.addToRolePolicy(new PolicyStatement({
318
343
  actions: ["appsync:GraphQL"],
319
344
  resources: ["*"],
@@ -345,6 +370,7 @@ export function defineSiteBackend(papyrusSite) {
345
370
  }
346
371
  if (s3ProjectionPublication !== undefined) {
347
372
  addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
373
+ addProjectionLeaseAccess(contentActionsLambda);
348
374
  }
349
375
  const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
350
376
  for (const [name, value] of Object.entries(publicationWiring.environment)) {
@@ -449,7 +475,7 @@ export function defineSiteBackend(papyrusSite) {
449
475
  resources: ["*"],
450
476
  }));
451
477
  const inboundEventStack = Stack.of(receiveLambda);
452
- // SES allows one active receipt rule set per region, so a non-legacy site
478
+ // SES allows one active receipt rule set per account and region, so a non-legacy site
453
479
  // creates its own brand-named rule set and never activates it here.
454
480
  const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
455
481
  if (inboundSesPlan.createReceiptRules) {
@@ -553,8 +579,21 @@ export function defineSiteBackend(papyrusSite) {
553
579
  : []),
554
580
  ],
555
581
  }));
582
+ const publicFormsEndpoint = wirePublicForms(backend, {
583
+ allowedOrigins: derivePublicFormOrigins({
584
+ explicit: site.publicForms?.allowedOrigins,
585
+ hosts: [activeBrand?.readerDeployment?.domain, ...(publication?.distribution?.aliases ?? [])],
586
+ urls: [site.revalidateBaseUrl, ...siteAuthRedirectUrls(authConfig)],
587
+ }),
588
+ honeypotFields: site.publicForms?.honeypotFields,
589
+ });
556
590
  backend.addOutput({
557
591
  custom: {
592
+ publicForms: { endpoint: publicFormsEndpoint },
593
+ // Lets the operator CLI re-project on publish without PAPYRUS_PUBLICATION_KIND / PAPYRUS_CONTENT_ACTIONS_FUNCTION (PPY-0c0b75).
594
+ publication: s3ProjectionPublication !== undefined
595
+ ? { kind: "s3-projection", contentActionsFunction: contentActionsLambda.functionName }
596
+ : null,
558
597
  prospectusSignIn: prospectusGoogleOnlyClient
559
598
  ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
560
599
  : null,
@@ -589,6 +628,9 @@ export function defineSiteBackend(papyrusSite) {
589
628
  : null,
590
629
  },
591
630
  });
631
+ wireEditorialChecks(backend);
632
+ wireMonitoringView(backend, emailMonitorId);
633
+ wireAutomationView(backend);
592
634
  wirePlugins(backend, plugins, contentActionsLambda);
593
635
  return backend;
594
636
  }
@@ -14,9 +14,10 @@ import { resolveSitePublication } from "./publication/default-publication";
14
14
  import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy";
15
15
  import { addProjectionRetries, addS3ProjectionPublication, type S3ProjectionPublication } from "./publication/s3-projection";
16
16
  import { planRenditions } from "./publication/renditions-settings";
17
+ import { addProjectionLeaseAccess } from "./publication/projection-lease";
17
18
  import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
18
19
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
19
- import { assertValidPublicationConfig } from "./site-backend-config";
20
+ import { assertValidMonitoringConfig, assertValidPublicationConfig } from "./site-backend-config";
20
21
  import { revalidateSecretReadStatement } from "./revalidate-secret-policy";
21
22
  import {
22
23
  revalidateDistributionEnvironment,
@@ -24,9 +25,15 @@ import {
24
25
  validateRevalidateDistributionIds,
25
26
  } from "./revalidate-invalidation-settings";
26
27
  import { addInvalidationPathAlarm } from "./publication/invalidation-alarm";
27
- import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource";
28
+ import { applyCognitoDomainPrefix, applyGoogleEmailVerifiedMapping, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource";
28
29
  import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client";
29
30
  import { defineSiteData } from "./data/resource";
31
+ import { editorialChecks } from "./functions/editorial-checks/resource";
32
+ import { automationView } from "./functions/automation-view/resource";
33
+ import { wireAutomationView } from "./functions/automation-view/wire";
34
+ import { monitoringView } from "./functions/monitoring-view/resource";
35
+ import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire";
36
+ import { wireEditorialChecks } from "./functions/editorial-checks/wire";
30
37
  import { resolvePlugins } from "./plugins/registry";
31
38
  import { wirePlugins } from "./plugins/wire";
32
39
  import { assignmentAction } from "./functions/assignment-action/resource";
@@ -38,6 +45,9 @@ import { knowledgeQuery } from "./functions/knowledge-query/resource";
38
45
  import { manageUserRole } from "./functions/manage-user-role/resource";
39
46
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
40
47
  import { newsroomSummary } from "./functions/newsroom-summary/resource";
48
+ import { derivePublicFormOrigins } from "./functions/public-forms/origins";
49
+ import { publicForms } from "./functions/public-forms/resource";
50
+ import { wirePublicForms } from "./functions/public-forms/wire";
41
51
  import { procedureAction } from "./functions/procedure-action/resource";
42
52
  import { readerSettings } from "./functions/reader-settings/resource";
43
53
  import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource";
@@ -98,6 +108,8 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
98
108
  };
99
109
  const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
100
110
  if (publication !== undefined) assertValidPublicationConfig(publication);
111
+ assertValidMonitoringConfig(site.monitoring);
112
+ const emailMonitorId = site.monitoring?.emailMonitorId ?? "hn";
101
113
  const publicationPlan = planS3ProjectionBackend(publication);
102
114
  const renditionsPlan = planRenditions(publication, process.cwd());
103
115
  setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
@@ -107,15 +119,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
107
119
  const backend = defineBackend({
108
120
  ...pluginFunctions,
109
121
  assignmentAction,
122
+ automationView,
110
123
  auth: defineSiteAuth(authConfig),
111
124
  categoryAction,
112
125
  contentActions,
113
- data: defineSiteData(pluginDefinitions),
126
+ editorialChecks,
127
+ data: defineSiteData(pluginDefinitions, site.excludeModelGroups ?? []),
114
128
  knowledgeQuery,
115
129
  manageUserRole,
130
+ monitoringView,
116
131
  modelAttachmentUpload,
117
132
  newsroomSummary,
118
133
  procedureAction,
134
+ publicForms,
119
135
  readerSettings,
120
136
  emailSubmissionProcessor,
121
137
  sesInboundReceive,
@@ -124,6 +140,10 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
124
140
  storage,
125
141
  });
126
142
 
143
+ if (authConfig.disableGoogleOAuth !== true) {
144
+ applyGoogleEmailVerifiedMapping(backend.auth.stack);
145
+ }
146
+
127
147
  orderModelStacksInWaves(
128
148
  backend.data.resources.graphqlApi.node.scope as Construct,
129
149
  Object.keys(backend.data.resources.tables),
@@ -396,6 +416,11 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
396
416
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
397
417
  const contentActionsLambda = backend.contentActions.resources.lambda as LambdaFunction;
398
418
  grantNewsroomReadWriteDelete(contentActionsLambda);
419
+ if ((activeBrand?.corpusKey ?? "").trim() !== "") {
420
+ // A deploy creates the publication's canonical corpus from the brand when the backend has none.
421
+ contentActionsLambda.addEnvironment("PAPYRUS_PUBLICATION_CORPUS_KEY", (activeBrand?.corpusKey ?? "").trim());
422
+ contentActionsLambda.addEnvironment("PAPYRUS_PUBLICATION_NAME", (activeBrand?.publicationName ?? activeBrand?.corpusKey ?? "").trim());
423
+ }
399
424
  // AppSync validates the token; pending intake additionally binds its actor
400
425
  // and publication to this deployment's pool and existing API endpoint.
401
426
  contentActionsLambda.addEnvironment(
@@ -406,6 +431,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
406
431
  "PAPYRUS_GRAPHQL_ENDPOINT",
407
432
  backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
408
433
  );
434
+ wireEmailReferenceIntake(contentActionsLambda, emailMonitorId);
409
435
  contentActionsLambda.addToRolePolicy(
410
436
  new PolicyStatement({
411
437
  actions: ["appsync:GraphQL"],
@@ -441,6 +467,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
441
467
  }
442
468
  if (s3ProjectionPublication !== undefined) {
443
469
  addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
470
+ addProjectionLeaseAccess(contentActionsLambda);
444
471
  }
445
472
  const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
446
473
  for (const [name, value] of Object.entries(publicationWiring.environment)) {
@@ -581,7 +608,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
581
608
 
582
609
  const inboundEventStack = Stack.of(receiveLambda);
583
610
 
584
- // SES allows one active receipt rule set per region, so a non-legacy site
611
+ // SES allows one active receipt rule set per account and region, so a non-legacy site
585
612
  // creates its own brand-named rule set and never activates it here.
586
613
  const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
587
614
  if (inboundSesPlan.createReceiptRules) {
@@ -696,8 +723,23 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
696
723
  }),
697
724
  );
698
725
 
726
+ const publicFormsEndpoint = wirePublicForms(backend, {
727
+ allowedOrigins: derivePublicFormOrigins({
728
+ explicit: site.publicForms?.allowedOrigins,
729
+ hosts: [activeBrand?.readerDeployment?.domain, ...(publication?.distribution?.aliases ?? [])],
730
+ urls: [site.revalidateBaseUrl, ...siteAuthRedirectUrls(authConfig)],
731
+ }),
732
+ honeypotFields: site.publicForms?.honeypotFields,
733
+ });
734
+
699
735
  backend.addOutput({
700
736
  custom: {
737
+ publicForms: { endpoint: publicFormsEndpoint },
738
+ // Lets the operator CLI re-project on publish without PAPYRUS_PUBLICATION_KIND / PAPYRUS_CONTENT_ACTIONS_FUNCTION (PPY-0c0b75).
739
+ publication:
740
+ s3ProjectionPublication !== undefined
741
+ ? { kind: "s3-projection", contentActionsFunction: contentActionsLambda.functionName }
742
+ : null,
701
743
  prospectusSignIn: prospectusGoogleOnlyClient
702
744
  ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
703
745
  : null,
@@ -732,6 +774,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
732
774
  : null,
733
775
  },
734
776
  });
777
+ wireEditorialChecks(backend);
778
+ wireMonitoringView(backend, emailMonitorId);
779
+ wireAutomationView(backend);
735
780
  wirePlugins(backend, plugins, contentActionsLambda);
736
781
 
737
782
  return backend;
@@ -7,3 +7,7 @@ export const runtime = "nodejs";
7
7
  export function GET(request: NextRequest) {
8
8
  return handleProspectusSignInStart(request);
9
9
  }
10
+
11
+ export function POST(request: NextRequest) {
12
+ return handleProspectusSignInStart(request);
13
+ }
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleNewsletterConfirm } from "../../../../lib/newsletter-api";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleNewsletterConfirm(request);
9
+ }
10
+
11
+ export function POST(request: NextRequest) {
12
+ return handleNewsletterConfirm(request);
13
+ }
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleNewsletterPreferences } from "../../../../lib/newsletter-api";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleNewsletterPreferences(request);
9
+ }
10
+
11
+ export function POST(request: NextRequest) {
12
+ return handleNewsletterPreferences(request);
13
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleNewsletterState } from "../../../../lib/newsletter-api";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleNewsletterState(request);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleNewsletterSubscribe } from "../../../../lib/newsletter-api";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function POST(request: NextRequest) {
8
+ return handleNewsletterSubscribe(request);
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleNewsletterUnsubscribe } from "../../../../lib/newsletter-api";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleNewsletterUnsubscribe(request);
9
+ }
10
+
11
+ export function POST(request: NextRequest) {
12
+ return handleNewsletterUnsubscribe(request);
13
+ }