@anthusai/papyrus 0.0.0 → 1.0.0-next.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,587 @@
1
+ import { defineBackend, secret } from "@aws-amplify/backend";
2
+ import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
3
+ import * as backup from "aws-cdk-lib/aws-backup";
4
+ import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
5
+ import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
6
+ import * as events from "aws-cdk-lib/aws-events";
7
+ import * as route53 from "aws-cdk-lib/aws-route53";
8
+ import * as ses from "aws-cdk-lib/aws-ses";
9
+ import * as sesActions from "aws-cdk-lib/aws-ses-actions";
10
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
11
+ import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
+ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
13
+ import { dirname, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
16
+ import { data } from "./data/resource.js";
17
+ import { assignmentAction } from "./functions/assignment-action/resource.js";
18
+ import { categoryAction } from "./functions/category-action/resource.js";
19
+ import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
20
+ import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
21
+ import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
22
+ import { manageUserRole } from "./functions/manage-user-role/resource.js";
23
+ import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
24
+ import { newsroomSummary } from "./functions/newsroom-summary/resource.js";
25
+ import { procedureAction } from "./functions/procedure-action/resource.js";
26
+ import { readerSettings } from "./functions/reader-settings/resource.js";
27
+ import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource.js";
28
+ import { sesInboundReceive } from "./functions/ses-inbound-receive/resource.js";
29
+ import { slackDelivery } from "./functions/slack-delivery/resource.js";
30
+ import { slackEvents } from "./functions/slack-events/resource.js";
31
+ import { InboundEmailStack } from "./inbound-email/stack.js";
32
+ import { storage } from "./storage/resource.js";
33
+ export function defineSiteBackend(papyrusSite) {
34
+ // `papyrus.config.ts`'s default export. Backend options live under `backend`;
35
+ // the default brand id namespaces account-global resources.
36
+ const site = { brandId: papyrusSite.defaultBrand, ...papyrusSite.backend };
37
+ const knowledgeVectorDimension = 1536;
38
+ const knowledgeEmbeddingModel = "text-embedding-3-small";
39
+ const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
40
+ const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
41
+ // Amplify app id whose `main` branch is this site's production pipeline (owns
42
+ // account-global resources: SES rule set, backup vault, legacy S3 Vectors name).
43
+ const productionAppId = (site.productionAppId ?? "").trim();
44
+ const isAmplifyProductionPipeline = amplifyBranch === "main" && productionAppId !== "" && amplifyAppId === productionAppId;
45
+ function readEnvFlag(name, defaultValue) {
46
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
47
+ if (["1", "true", "yes", "on"].includes(raw))
48
+ return true;
49
+ if (["0", "false", "no", "off"].includes(raw))
50
+ return false;
51
+ return defaultValue;
52
+ }
53
+ function sanitizeAwsName(value, maxLength = 50) {
54
+ return value
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9-]/g, "-")
57
+ .replace(/-+/g, "-")
58
+ .replace(/^-|-$/g, "")
59
+ .slice(0, maxLength);
60
+ }
61
+ // S3 Vectors index names are account-global. Keep the original name for the
62
+ // p.apyr.us production app so its existing index + data are not orphaned;
63
+ // namespace every other app's index by site brand so a second CMS app can
64
+ // deploy into the same account without colliding.
65
+ const papyrusSiteBrand = (site.brandId
66
+ ?? process.env.PAPYRUS_SITE_BRAND
67
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
68
+ ?? "papyrus").trim().toLowerCase();
69
+ const knowledgeVectorIndexName = productionAppId !== "" && amplifyAppId === productionAppId
70
+ ? "papyrus-knowledge"
71
+ : `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
72
+ // SES active receipt rule sets and shared backup vault names are account-global.
73
+ // Keep them on the production pipeline only unless explicitly opted in.
74
+ // A feature flag in the site config wins over the environment.
75
+ const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
76
+ const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
77
+ const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
78
+ const backend = defineBackend({
79
+ assignmentAction,
80
+ auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
81
+ categoryAction,
82
+ data,
83
+ graphqlJwtAuthorizer,
84
+ knowledgeQuery,
85
+ manageUserRole,
86
+ modelAttachmentUpload,
87
+ newsroomSummary,
88
+ procedureAction,
89
+ readerSettings,
90
+ emailSubmissionProcessor,
91
+ sesInboundReceive,
92
+ slackEvents,
93
+ slackDelivery,
94
+ storage,
95
+ });
96
+ const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
97
+ // Repo root in a Papyrus checkout; package root when installed from npm.
98
+ const projectRoot = resolve(amplifyBackendDir, "..");
99
+ const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
100
+ if (enableConsoleResponder
101
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
102
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
103
+ && !isAmplifyProductionPipeline) {
104
+ process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD = "true";
105
+ }
106
+ const inboundEmailDomain = (process.env.PAPYRUS_INBOUND_EMAIL_DOMAIN ?? "p.apyr.us").trim().toLowerCase();
107
+ const inboundEmailLocalParts = (process.env.PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS ?? "submissions,suggestions")
108
+ .split(",")
109
+ .map((entry) => entry.trim().toLowerCase())
110
+ .filter(Boolean);
111
+ const inboundEmailCorpusKey = (process.env.PAPYRUS_INBOUND_EMAIL_CORPUS_KEY ?? "AI-ML-research").trim();
112
+ const inboundDnsZoneId = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_ID ?? "Z10285921B1G7MVRV06W9").trim();
113
+ const inboundDnsZoneName = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_NAME ?? "apyr.us").trim().replace(/\.$/, "");
114
+ const inboundDnsRecordName = inboundEmailDomain.endsWith(`.${inboundDnsZoneName}`)
115
+ ? inboundEmailDomain.slice(0, -(inboundDnsZoneName.length + 1))
116
+ : inboundEmailDomain;
117
+ let slackEventsUrl;
118
+ if (enableConsoleResponder || enableSlackAgent) {
119
+ const messageTable = backend.data.resources.tables.Message;
120
+ const cfnTables = backend.data.resources.cfnResources.cfnTables;
121
+ const messageCfnTable = cfnTables.Message
122
+ ?? cfnTables.MessageTable
123
+ ?? Object.entries(cfnTables).find(([key]) => {
124
+ const normalized = key.toLowerCase();
125
+ return normalized.includes("message") && !normalized.includes("thread");
126
+ })?.[1];
127
+ if (messageCfnTable) {
128
+ messageCfnTable.streamSpecification = {
129
+ streamViewType: dynamodb.StreamViewType.NEW_IMAGE,
130
+ };
131
+ }
132
+ const messageStreamArn = messageTable.tableStreamArn ?? messageCfnTable?.attrStreamArn;
133
+ if (!messageStreamArn) {
134
+ throw new Error(`Message agents require Message table stream ARN. cfnTables=${Object.keys(cfnTables).join(",")}`);
135
+ }
136
+ const messageThreadTable = backend.data.resources.tables.MessageThread;
137
+ const dataStack = Stack.of(messageTable);
138
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
139
+ const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild;
140
+ const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
141
+ const jwtAuthorizerVariables = jwtAuthorizerEnvironment
142
+ && typeof jwtAuthorizerEnvironment === "object"
143
+ && "variables" in jwtAuthorizerEnvironment
144
+ ? jwtAuthorizerEnvironment.variables
145
+ : undefined;
146
+ const jwtSsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
147
+ || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
148
+ || "";
149
+ if (enableConsoleResponder) {
150
+ const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
151
+ messageTable,
152
+ messageStreamArn,
153
+ threadTable: messageThreadTable,
154
+ projectRoot,
155
+ graphqlEndpoint,
156
+ amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
157
+ responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
158
+ model: process.env.PAPYRUS_CONSOLE_MODEL,
159
+ prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
160
+ });
161
+ if (jwtSsmEnvConfig) {
162
+ consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
163
+ }
164
+ const jwtSecretSsmParam = process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
165
+ || (productionAppId !== "" && amplifyAppId === productionAppId
166
+ ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
167
+ : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
168
+ consoleChatResponder.responderFunction.addEnvironment("PAPYRUS_JWT_SECRET_SSM_PARAM", jwtSecretSsmParam);
169
+ }
170
+ if (enableSlackAgent) {
171
+ const slackEventsLambda = backend.slackEvents.resources.lambda;
172
+ const slackDeliveryLambda = backend.slackDelivery.resources.lambda;
173
+ slackEventsLambda.addToRolePolicy(new PolicyStatement({
174
+ actions: ["appsync:GraphQL"],
175
+ resources: ["*"],
176
+ }));
177
+ backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
178
+ backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
179
+ backend.slackEvents.addEnvironment("PAPYRUS_SLACK_ALLOWED_USER_IDS", (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim());
180
+ if (jwtSsmEnvConfig) {
181
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
182
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
183
+ }
184
+ backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
185
+ backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
186
+ const slackBotTokenName = (process.env.PAPYRUS_SLACK_BOT_TOKEN_NAME ?? "PAPYRUS_SLACK_BOT_TOKEN").trim();
187
+ backend.slackDelivery.addEnvironment("PAPYRUS_SLACK_BOT_TOKEN", secret(slackBotTokenName));
188
+ const slackStack = Stack.of(slackEventsLambda);
189
+ const slackSsmResources = amplifyAppId
190
+ ? [
191
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/${amplifyAppId}/*`,
192
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/shared/${amplifyAppId}/*`,
193
+ ]
194
+ : [`arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/*`];
195
+ const slackSsmPolicy = new PolicyStatement({
196
+ effect: Effect.ALLOW,
197
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
198
+ resources: slackSsmResources,
199
+ });
200
+ slackEventsLambda.addToRolePolicy(slackSsmPolicy);
201
+ slackDeliveryLambda.addToRolePolicy(slackSsmPolicy);
202
+ // Wire Slack on the data stack (not a nested SlackAgent stack) to avoid CloudFormation
203
+ // circular dependencies between Message and slack-delivery in the data resource group.
204
+ const slackEventsFunctionUrl = slackEventsLambda.addFunctionUrl({
205
+ authType: FunctionUrlAuthType.NONE,
206
+ });
207
+ slackEventsUrl = slackEventsFunctionUrl.url;
208
+ const slackConsumerStack = backend.createStack("slack-consumer");
209
+ new CfnEventSourceMapping(slackConsumerStack, "SlackDeliveryMessageStreamMapping", {
210
+ batchSize: 1,
211
+ eventSourceArn: messageStreamArn,
212
+ functionName: slackDeliveryLambda.functionArn,
213
+ functionResponseTypes: ["ReportBatchItemFailures"],
214
+ maximumRetryAttempts: 2,
215
+ startingPosition: "LATEST",
216
+ });
217
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
218
+ effect: Effect.ALLOW,
219
+ actions: [
220
+ "dynamodb:DescribeStream",
221
+ "dynamodb:GetRecords",
222
+ "dynamodb:GetShardIterator",
223
+ "dynamodb:ListStreams",
224
+ ],
225
+ resources: [messageStreamArn],
226
+ }));
227
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
228
+ effect: Effect.ALLOW,
229
+ actions: ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:BatchGetItem"],
230
+ resources: [messageTable.tableArn, `${messageTable.tableArn}/index/*`],
231
+ }));
232
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
233
+ effect: Effect.ALLOW,
234
+ actions: ["appsync:GraphQL"],
235
+ resources: ["*"],
236
+ }));
237
+ }
238
+ }
239
+ const storageBucket = backend.storage.resources.bucket;
240
+ const storageStack = Stack.of(storageBucket);
241
+ let storageBackupPlan;
242
+ let storageBackupVault;
243
+ const storageBucketCfn = storageBucket.node.defaultChild;
244
+ if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
245
+ // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
246
+ storageBucketCfn.addPropertyOverride("NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled", true);
247
+ }
248
+ if (enableStorageBackups) {
249
+ const storageBackupsStack = backend.createStack("storage-backups");
250
+ const storageBackupVaultName = sanitizeAwsName(process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME?.trim()
251
+ || (isAmplifyProductionPipeline
252
+ ? `papyrus-${productionAppId}-main-media-backup-vault`
253
+ : `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`));
254
+ storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
255
+ backupVaultName: storageBackupVaultName,
256
+ removalPolicy: RemovalPolicy.RETAIN,
257
+ });
258
+ storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
259
+ backupVault: storageBackupVault,
260
+ });
261
+ storageBackupPlan.addRule(new backup.BackupPlanRule({
262
+ ruleName: "papyrus-storage-pitr-35d",
263
+ enableContinuousBackup: true,
264
+ deleteAfter: Duration.days(35),
265
+ }));
266
+ storageBackupPlan.addRule(new backup.BackupPlanRule({
267
+ ruleName: "papyrus-storage-daily-365d",
268
+ scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
269
+ deleteAfter: Duration.days(365),
270
+ }));
271
+ storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
272
+ allowRestores: true,
273
+ resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
274
+ });
275
+ }
276
+ // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
277
+ // storage ↔ data nested-stack circular dependencies from allow.resource().
278
+ const corporaObjectArn = `${storageBucket.bucketArn}/corpora/*`;
279
+ const mediaObjectArn = `${storageBucket.bucketArn}/media/*`;
280
+ const newsroomObjectArn = `${storageBucket.bucketArn}/newsroom/*`;
281
+ const grantCorporaRead = (lambda) => {
282
+ lambda.addToRolePolicy(new PolicyStatement({
283
+ actions: ["s3:GetObject"],
284
+ resources: [corporaObjectArn],
285
+ }));
286
+ };
287
+ const grantNewsroomReadWrite = (lambda) => {
288
+ lambda.addToRolePolicy(new PolicyStatement({
289
+ actions: ["s3:GetObject", "s3:PutObject"],
290
+ resources: [newsroomObjectArn],
291
+ }));
292
+ };
293
+ const grantNewsroomReadWriteDelete = (lambda) => {
294
+ lambda.addToRolePolicy(new PolicyStatement({
295
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
296
+ resources: [newsroomObjectArn],
297
+ }));
298
+ };
299
+ const grantMediaReadWriteDelete = (lambda) => {
300
+ lambda.addToRolePolicy(new PolicyStatement({
301
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
302
+ resources: [mediaObjectArn],
303
+ }));
304
+ };
305
+ const mediaBucketName = storageBucket.bucketName;
306
+ const withMediaBucketEnv = (resource) => {
307
+ resource.addEnvironment("PAPYRUS_MEDIA_BUCKET_NAME", mediaBucketName);
308
+ resource.addEnvironment("papyrusMedia_BUCKET_NAME", mediaBucketName);
309
+ };
310
+ for (const resource of [
311
+ backend.assignmentAction,
312
+ backend.categoryAction,
313
+ backend.emailSubmissionProcessor,
314
+ backend.knowledgeQuery,
315
+ backend.modelAttachmentUpload,
316
+ backend.newsroomSummary,
317
+ backend.procedureAction,
318
+ backend.sesInboundReceive,
319
+ ]) {
320
+ withMediaBucketEnv(resource);
321
+ }
322
+ grantCorporaRead(backend.knowledgeQuery.resources.lambda);
323
+ grantNewsroomReadWrite(backend.assignmentAction.resources.lambda);
324
+ grantNewsroomReadWrite(backend.categoryAction.resources.lambda);
325
+ grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
326
+ grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
327
+ grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
328
+ if (backend.sesInboundReceive) {
329
+ grantNewsroomReadWrite(backend.sesInboundReceive.resources.lambda);
330
+ }
331
+ if (enableInboundEmail) {
332
+ if (!backend.sesInboundReceive || !backend.emailSubmissionProcessor) {
333
+ throw new Error("Inbound email is enabled but sesInboundReceive/emailSubmissionProcessor were not registered.");
334
+ }
335
+ const inboundReceive = backend.sesInboundReceive;
336
+ const inboundProcessor = backend.emailSubmissionProcessor;
337
+ const receiveLambda = inboundReceive.resources.lambda;
338
+ const processorLambda = inboundProcessor.resources.lambda;
339
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
340
+ const inboundRecipients = inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`);
341
+ inboundReceive.addEnvironment("PAPYRUS_EMAIL_SUBMISSION_PROCESSOR_FUNCTION_NAME", processorLambda.functionName);
342
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
343
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS", inboundEmailLocalParts.join(","));
344
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
345
+ inboundReceive.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
346
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
347
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
348
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_EMAIL_ENABLED", "true");
349
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_FROM_EMAIL", `Papyrus Submissions <submissions@${inboundEmailDomain}>`);
350
+ inboundProcessor.addEnvironment("PAPYRUS_PUBLIC_SITE_BASE_URL", "https://p.apyr.us");
351
+ inboundProcessor.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
352
+ receiveLambda.addToRolePolicy(new PolicyStatement({
353
+ actions: ["lambda:InvokeFunction"],
354
+ resources: [processorLambda.functionArn],
355
+ }));
356
+ receiveLambda.addToRolePolicy(new PolicyStatement({
357
+ actions: ["s3:GetObject", "s3:ListBucket"],
358
+ resources: [
359
+ storageBucket.bucketArn,
360
+ `${storageBucket.bucketArn}/inbound-email/*`,
361
+ ],
362
+ }));
363
+ processorLambda.addToRolePolicy(new PolicyStatement({
364
+ actions: ["s3:ListBucket"],
365
+ resources: [storageBucket.bucketArn],
366
+ }));
367
+ processorLambda.addToRolePolicy(new PolicyStatement({
368
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
369
+ resources: [
370
+ `${storageBucket.bucketArn}/inbound-email/*`,
371
+ `${storageBucket.bucketArn}/inbound-email-archived/*`,
372
+ ],
373
+ }));
374
+ processorLambda.addToRolePolicy(new PolicyStatement({
375
+ actions: ["s3:GetObject", "s3:PutObject"],
376
+ resources: [corporaObjectArn],
377
+ }));
378
+ receiveLambda.addToRolePolicy(new PolicyStatement({
379
+ actions: ["appsync:GraphQL"],
380
+ resources: ["*"],
381
+ }));
382
+ processorLambda.addToRolePolicy(new PolicyStatement({
383
+ actions: ["appsync:GraphQL"],
384
+ resources: ["*"],
385
+ }));
386
+ processorLambda.addToRolePolicy(new PolicyStatement({
387
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
388
+ resources: ["*"],
389
+ }));
390
+ const inboundEventStack = Stack.of(receiveLambda);
391
+ // SES receipt rule sets are account-global; only provision on the production pipeline.
392
+ if (isAmplifyProductionPipeline) {
393
+ const inboundDnsZone = route53.HostedZone.fromHostedZoneAttributes(storageStack, "PapyrusInboundDnsZone", {
394
+ hostedZoneId: inboundDnsZoneId,
395
+ zoneName: inboundDnsZoneName,
396
+ });
397
+ const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
398
+ onCreate: {
399
+ service: "SES",
400
+ action: "verifyDomainIdentity",
401
+ parameters: {
402
+ Domain: inboundEmailDomain,
403
+ },
404
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
405
+ },
406
+ onUpdate: {
407
+ service: "SES",
408
+ action: "verifyDomainIdentity",
409
+ parameters: {
410
+ Domain: inboundEmailDomain,
411
+ },
412
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
413
+ },
414
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
415
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
416
+ }),
417
+ timeout: Duration.minutes(2),
418
+ });
419
+ new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
420
+ zone: inboundDnsZone,
421
+ recordName: `_amazonses.${inboundDnsRecordName}`,
422
+ ttl: Duration.minutes(5),
423
+ values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
424
+ });
425
+ const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
426
+ receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
427
+ });
428
+ inboundRuleSet.addRule("PapyrusInboundSubmissions", {
429
+ recipients: inboundRecipients,
430
+ enabled: true,
431
+ scanEnabled: true,
432
+ tlsPolicy: ses.TlsPolicy.REQUIRE,
433
+ actions: [
434
+ new sesActions.S3({
435
+ bucket: storageBucket,
436
+ objectKeyPrefix: "inbound-email/",
437
+ }),
438
+ ],
439
+ });
440
+ new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
441
+ onCreate: {
442
+ service: "SES",
443
+ action: "setActiveReceiptRuleSet",
444
+ parameters: {
445
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
446
+ },
447
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
448
+ },
449
+ onUpdate: {
450
+ service: "SES",
451
+ action: "setActiveReceiptRuleSet",
452
+ parameters: {
453
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
454
+ },
455
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
456
+ },
457
+ onDelete: {
458
+ service: "SES",
459
+ action: "setActiveReceiptRuleSet",
460
+ parameters: {
461
+ RuleSetName: null,
462
+ },
463
+ },
464
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
465
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
466
+ }),
467
+ timeout: Duration.minutes(2),
468
+ });
469
+ }
470
+ new InboundEmailStack(inboundEventStack, "InboundEmail", {
471
+ storageBucket,
472
+ receiveFunctionArn: receiveLambda.functionArn,
473
+ });
474
+ }
475
+ const knowledgeVectorsStack = backend.createStack("knowledge-vectors");
476
+ const knowledgeVectorBucket = new CfnVectorBucket(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucket", {
477
+ encryptionConfiguration: {
478
+ sseType: "AES256",
479
+ },
480
+ });
481
+ const knowledgeVectorIndex = new CfnIndex(knowledgeVectorsStack, "PapyrusKnowledgeVectorIndex", {
482
+ dataType: "float32",
483
+ dimension: knowledgeVectorDimension,
484
+ distanceMetric: "cosine",
485
+ indexName: knowledgeVectorIndexName,
486
+ metadataConfiguration: {
487
+ nonFilterableMetadataKeys: ["text", "summary", "sourceUri", "title"],
488
+ },
489
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
490
+ });
491
+ knowledgeVectorIndex.node.addDependency(knowledgeVectorBucket);
492
+ const knowledgeVectorBucketPolicy = new CfnVectorBucketPolicy(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucketPolicy", {
493
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
494
+ policy: {
495
+ Version: "2012-10-17",
496
+ Statement: [
497
+ {
498
+ Sid: "AllowSameAccountPapyrusKnowledgeVectorAccess",
499
+ Effect: "Allow",
500
+ Principal: {
501
+ AWS: `arn:aws:iam::${knowledgeVectorsStack.account}:root`,
502
+ },
503
+ Action: [
504
+ "s3vectors:GetIndex",
505
+ "s3vectors:GetVectors",
506
+ "s3vectors:ListVectors",
507
+ "s3vectors:PutVectors",
508
+ "s3vectors:QueryVectors",
509
+ "s3vectors:DeleteVectors",
510
+ ],
511
+ Resource: [
512
+ knowledgeVectorBucket.attrVectorBucketArn,
513
+ knowledgeVectorIndex.attrIndexArn,
514
+ ],
515
+ },
516
+ ],
517
+ },
518
+ });
519
+ knowledgeVectorBucketPolicy.node.addDependency(knowledgeVectorIndex);
520
+ const knowledgeQueryLambda = backend.knowledgeQuery.resources.lambda;
521
+ backend.knowledgeQuery.addEnvironment("OPENAI_API_KEY", secret("OPENAI_API_KEY"));
522
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_STORAGE_BUCKET_NAME", storageBucket.bucketName);
523
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
524
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_ARN", knowledgeVectorIndex.attrIndexArn);
525
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_NAME", knowledgeVectorIndexName);
526
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_MODEL", knowledgeEmbeddingModel);
527
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_DIMENSIONS", String(knowledgeVectorDimension));
528
+ knowledgeQueryLambda.addToRolePolicy(new PolicyStatement({
529
+ actions: [
530
+ "s3vectors:GetIndex",
531
+ "s3vectors:GetVectors",
532
+ "s3vectors:ListVectors",
533
+ "s3vectors:QueryVectors",
534
+ ],
535
+ resources: [knowledgeVectorIndex.attrIndexArn],
536
+ }));
537
+ knowledgeQueryLambda.addToRolePolicy(new PolicyStatement({
538
+ actions: ["ssm:GetParameter"],
539
+ resources: [
540
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/papyrus/*/OPENAI_API_KEY`,
541
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/papyrus/OPENAI_API_KEY`,
542
+ // Pipeline-deployed apps store secrets under amplify/<appId>/... rather
543
+ // than the sandbox's amplify/papyrus/... path. Grant the app's own
544
+ // secret path so a second CMS app (e.g. pilobol-us) can read its key.
545
+ ...(amplifyAppId
546
+ ? [
547
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/${amplifyAppId}/*/OPENAI_API_KEY`,
548
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/${amplifyAppId}/OPENAI_API_KEY`,
549
+ ]
550
+ : []),
551
+ ],
552
+ }));
553
+ backend.addOutput({
554
+ custom: {
555
+ inboundEmail: enableInboundEmail
556
+ ? {
557
+ domain: inboundEmailDomain,
558
+ localParts: inboundEmailLocalParts,
559
+ addresses: inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`),
560
+ corpusKey: inboundEmailCorpusKey,
561
+ }
562
+ : null,
563
+ knowledgeQuery: {
564
+ s3VectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
565
+ s3VectorIndexArn: knowledgeVectorIndex.attrIndexArn,
566
+ s3VectorIndexName: knowledgeVectorIndexName,
567
+ embeddingModel: knowledgeEmbeddingModel,
568
+ embeddingDimensions: knowledgeVectorDimension,
569
+ },
570
+ storageBackups: storageBackupPlan && storageBackupVault
571
+ ? {
572
+ backupPlanId: storageBackupPlan.backupPlanId,
573
+ backupVaultArn: storageBackupVault.backupVaultArn,
574
+ backupVaultName: storageBackupVault.backupVaultName,
575
+ protectedBucketArn: storageBucket.bucketArn,
576
+ protectedBucketName: storageBucket.bucketName,
577
+ }
578
+ : null,
579
+ slack: enableSlackAgent && slackEventsUrl
580
+ ? {
581
+ eventsUrl: slackEventsUrl,
582
+ }
583
+ : null,
584
+ },
585
+ });
586
+ return backend;
587
+ }