@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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,704 @@
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, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
+ import * as s3 from "aws-cdk-lib/aws-s3";
13
+ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
14
+ import { dirname, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
17
+ import { data } from "./data/resource";
18
+ import { assignmentAction } from "./functions/assignment-action/resource";
19
+ import { categoryAction } from "./functions/category-action/resource";
20
+ import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
21
+ import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
22
+ import { knowledgeQuery } from "./functions/knowledge-query/resource";
23
+ import { manageUserRole } from "./functions/manage-user-role/resource";
24
+ import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
25
+ import { newsroomSummary } from "./functions/newsroom-summary/resource";
26
+ import { procedureAction } from "./functions/procedure-action/resource";
27
+ import { readerSettings } from "./functions/reader-settings/resource";
28
+ import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource";
29
+ import { sesInboundReceive } from "./functions/ses-inbound-receive/resource";
30
+ import { slackDelivery } from "./functions/slack-delivery/resource";
31
+ import { slackEvents } from "./functions/slack-events/resource";
32
+ import { InboundEmailStack } from "./inbound-email/stack";
33
+ import { storage } from "./storage/resource";
34
+ import type { PapyrusSite } from "../lib/define-site";
35
+
36
+ export function defineSiteBackend(papyrusSite: PapyrusSite) {
37
+ // `papyrus.config.ts`'s default export. Backend options live under `backend`;
38
+ // the default brand id namespaces account-global resources.
39
+ const site = { brandId: papyrusSite.defaultBrand, ...papyrusSite.backend };
40
+ const knowledgeVectorDimension = 1536;
41
+ const knowledgeEmbeddingModel = "text-embedding-3-small";
42
+
43
+ const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
44
+ const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
45
+ // Amplify app id whose `main` branch is this site's production pipeline (owns
46
+ // account-global resources: SES rule set, backup vault, legacy S3 Vectors name).
47
+ const productionAppId = (site.productionAppId ?? "").trim();
48
+ const isAmplifyProductionPipeline =
49
+ amplifyBranch === "main" && productionAppId !== "" && amplifyAppId === productionAppId;
50
+
51
+ function readEnvFlag(name: string, defaultValue: boolean): boolean {
52
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
53
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
54
+ if (["0", "false", "no", "off"].includes(raw)) return false;
55
+ return defaultValue;
56
+ }
57
+
58
+ function sanitizeAwsName(value: string, maxLength = 50): string {
59
+ return value
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9-]/g, "-")
62
+ .replace(/-+/g, "-")
63
+ .replace(/^-|-$/g, "")
64
+ .slice(0, maxLength);
65
+ }
66
+
67
+ // S3 Vectors index names are account-global. Keep the original name for the
68
+ // p.apyr.us production app so its existing index + data are not orphaned;
69
+ // namespace every other app's index by site brand so a second CMS app can
70
+ // deploy into the same account without colliding.
71
+ const papyrusSiteBrand = (
72
+ site.brandId
73
+ ?? process.env.PAPYRUS_SITE_BRAND
74
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
75
+ ?? "papyrus"
76
+ ).trim().toLowerCase();
77
+ const knowledgeVectorIndexName =
78
+ productionAppId !== "" && amplifyAppId === productionAppId
79
+ ? "papyrus-knowledge"
80
+ : `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
81
+
82
+ // SES active receipt rule sets and shared backup vault names are account-global.
83
+ // Keep them on the production pipeline only unless explicitly opted in.
84
+ // A feature flag in the site config wins over the environment.
85
+ const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
86
+ const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
87
+ const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
88
+
89
+ const backend = defineBackend({
90
+ assignmentAction,
91
+ auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
92
+ categoryAction,
93
+ data,
94
+ graphqlJwtAuthorizer,
95
+ knowledgeQuery,
96
+ manageUserRole,
97
+ modelAttachmentUpload,
98
+ newsroomSummary,
99
+ procedureAction,
100
+ readerSettings,
101
+ emailSubmissionProcessor,
102
+ sesInboundReceive,
103
+ slackEvents,
104
+ slackDelivery,
105
+ storage,
106
+ });
107
+
108
+ const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
109
+ // Repo root in a Papyrus checkout; package root when installed from npm.
110
+ const projectRoot = resolve(amplifyBackendDir, "..");
111
+ const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
112
+
113
+ if (
114
+ enableConsoleResponder
115
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
116
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
117
+ && !isAmplifyProductionPipeline
118
+ ) {
119
+ process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD = "true";
120
+ }
121
+ const inboundEmailDomain = (process.env.PAPYRUS_INBOUND_EMAIL_DOMAIN ?? "p.apyr.us").trim().toLowerCase();
122
+ const inboundEmailLocalParts = (process.env.PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS ?? "submissions,suggestions")
123
+ .split(",")
124
+ .map((entry) => entry.trim().toLowerCase())
125
+ .filter(Boolean);
126
+ const inboundEmailCorpusKey = (process.env.PAPYRUS_INBOUND_EMAIL_CORPUS_KEY ?? "AI-ML-research").trim();
127
+ const inboundDnsZoneId = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_ID ?? "Z10285921B1G7MVRV06W9").trim();
128
+ const inboundDnsZoneName = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_NAME ?? "apyr.us").trim().replace(/\.$/, "");
129
+ const inboundDnsRecordName = inboundEmailDomain.endsWith(`.${inboundDnsZoneName}`)
130
+ ? inboundEmailDomain.slice(0, -(inboundDnsZoneName.length + 1))
131
+ : inboundEmailDomain;
132
+
133
+ let slackEventsUrl: string | undefined;
134
+
135
+ if (enableConsoleResponder || enableSlackAgent) {
136
+ const messageTable = backend.data.resources.tables.Message;
137
+ const cfnTables = backend.data.resources.cfnResources.cfnTables;
138
+ const messageCfnTable =
139
+ cfnTables.Message
140
+ ?? cfnTables.MessageTable
141
+ ?? Object.entries(cfnTables).find(([key]) => {
142
+ const normalized = key.toLowerCase();
143
+ return normalized.includes("message") && !normalized.includes("thread");
144
+ })?.[1];
145
+
146
+ if (messageCfnTable) {
147
+ messageCfnTable.streamSpecification = {
148
+ streamViewType: dynamodb.StreamViewType.NEW_IMAGE,
149
+ };
150
+ }
151
+
152
+ const messageStreamArn = messageTable.tableStreamArn ?? messageCfnTable?.attrStreamArn;
153
+ if (!messageStreamArn) {
154
+ throw new Error(`Message agents require Message table stream ARN. cfnTables=${Object.keys(cfnTables).join(",")}`);
155
+ }
156
+
157
+ const messageThreadTable = backend.data.resources.tables.MessageThread;
158
+ const dataStack = Stack.of(messageTable);
159
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
160
+ const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild as CfnFunction | undefined;
161
+ const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
162
+ const jwtAuthorizerVariables =
163
+ jwtAuthorizerEnvironment
164
+ && typeof jwtAuthorizerEnvironment === "object"
165
+ && "variables" in jwtAuthorizerEnvironment
166
+ ? (jwtAuthorizerEnvironment as { variables?: Record<string, string> }).variables
167
+ : undefined;
168
+ const jwtSsmEnvConfig =
169
+ process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
170
+ || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
171
+ || "";
172
+
173
+ if (enableConsoleResponder) {
174
+ const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
175
+ messageTable,
176
+ messageStreamArn,
177
+ threadTable: messageThreadTable,
178
+ projectRoot,
179
+ graphqlEndpoint,
180
+ amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
181
+ responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
182
+ model: process.env.PAPYRUS_CONSOLE_MODEL,
183
+ prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
184
+ });
185
+ if (jwtSsmEnvConfig) {
186
+ consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
187
+ }
188
+ const jwtSecretSsmParam =
189
+ process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
190
+ || (productionAppId !== "" && amplifyAppId === productionAppId
191
+ ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
192
+ : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
193
+ consoleChatResponder.responderFunction.addEnvironment(
194
+ "PAPYRUS_JWT_SECRET_SSM_PARAM",
195
+ jwtSecretSsmParam,
196
+ );
197
+ }
198
+
199
+ if (enableSlackAgent) {
200
+ const slackEventsLambda = backend.slackEvents.resources.lambda as LambdaFunction;
201
+ const slackDeliveryLambda = backend.slackDelivery.resources.lambda as LambdaFunction;
202
+ slackEventsLambda.addToRolePolicy(
203
+ new PolicyStatement({
204
+ actions: ["appsync:GraphQL"],
205
+ resources: ["*"],
206
+ }),
207
+ );
208
+ backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
209
+ backend.slackEvents.addEnvironment(
210
+ "PAPYRUS_CONSOLE_RESPONSE_TARGET",
211
+ (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud",
212
+ );
213
+ backend.slackEvents.addEnvironment(
214
+ "PAPYRUS_SLACK_ALLOWED_USER_IDS",
215
+ (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
216
+ );
217
+ if (jwtSsmEnvConfig) {
218
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
219
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
220
+ }
221
+ backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
222
+ backend.slackDelivery.addEnvironment(
223
+ "PAPYRUS_CONSOLE_RESPONSE_TARGET",
224
+ (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud",
225
+ );
226
+ const slackBotTokenName = (process.env.PAPYRUS_SLACK_BOT_TOKEN_NAME ?? "PAPYRUS_SLACK_BOT_TOKEN").trim();
227
+ backend.slackDelivery.addEnvironment("PAPYRUS_SLACK_BOT_TOKEN", secret(slackBotTokenName));
228
+
229
+ const slackStack = Stack.of(slackEventsLambda);
230
+ const slackSsmResources = amplifyAppId
231
+ ? [
232
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/${amplifyAppId}/*`,
233
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/shared/${amplifyAppId}/*`,
234
+ ]
235
+ : [`arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/*`];
236
+ const slackSsmPolicy = new PolicyStatement({
237
+ effect: Effect.ALLOW,
238
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
239
+ resources: slackSsmResources,
240
+ });
241
+ slackEventsLambda.addToRolePolicy(slackSsmPolicy);
242
+ slackDeliveryLambda.addToRolePolicy(slackSsmPolicy);
243
+
244
+ // Wire Slack on the data stack (not a nested SlackAgent stack) to avoid CloudFormation
245
+ // circular dependencies between Message and slack-delivery in the data resource group.
246
+ const slackEventsFunctionUrl = slackEventsLambda.addFunctionUrl({
247
+ authType: FunctionUrlAuthType.NONE,
248
+ });
249
+ slackEventsUrl = slackEventsFunctionUrl.url;
250
+
251
+ const slackConsumerStack = backend.createStack("slack-consumer");
252
+ new CfnEventSourceMapping(slackConsumerStack, "SlackDeliveryMessageStreamMapping", {
253
+ batchSize: 1,
254
+ eventSourceArn: messageStreamArn,
255
+ functionName: slackDeliveryLambda.functionArn,
256
+ functionResponseTypes: ["ReportBatchItemFailures"],
257
+ maximumRetryAttempts: 2,
258
+ startingPosition: "LATEST",
259
+ });
260
+
261
+ slackDeliveryLambda.addToRolePolicy(
262
+ new PolicyStatement({
263
+ effect: Effect.ALLOW,
264
+ actions: [
265
+ "dynamodb:DescribeStream",
266
+ "dynamodb:GetRecords",
267
+ "dynamodb:GetShardIterator",
268
+ "dynamodb:ListStreams",
269
+ ],
270
+ resources: [messageStreamArn],
271
+ }),
272
+ );
273
+ slackDeliveryLambda.addToRolePolicy(
274
+ new PolicyStatement({
275
+ effect: Effect.ALLOW,
276
+ actions: ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:BatchGetItem"],
277
+ resources: [messageTable.tableArn, `${messageTable.tableArn}/index/*`],
278
+ }),
279
+ );
280
+ slackDeliveryLambda.addToRolePolicy(
281
+ new PolicyStatement({
282
+ effect: Effect.ALLOW,
283
+ actions: ["appsync:GraphQL"],
284
+ resources: ["*"],
285
+ }),
286
+ );
287
+ }
288
+ }
289
+
290
+ const storageBucket = backend.storage.resources.bucket;
291
+ const storageStack = Stack.of(storageBucket);
292
+
293
+ let storageBackupPlan: backup.BackupPlan | undefined;
294
+ let storageBackupVault: backup.BackupVault | undefined;
295
+
296
+ const storageBucketCfn = storageBucket.node.defaultChild as s3.CfnBucket | undefined;
297
+ if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
298
+ // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
299
+ storageBucketCfn.addPropertyOverride(
300
+ "NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled",
301
+ true,
302
+ );
303
+ }
304
+
305
+ if (enableStorageBackups) {
306
+ const storageBackupsStack = backend.createStack("storage-backups");
307
+ const storageBackupVaultName = sanitizeAwsName(
308
+ process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME?.trim()
309
+ || (isAmplifyProductionPipeline
310
+ ? `papyrus-${productionAppId}-main-media-backup-vault`
311
+ : `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`),
312
+ );
313
+
314
+ storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
315
+ backupVaultName: storageBackupVaultName,
316
+ removalPolicy: RemovalPolicy.RETAIN,
317
+ });
318
+ storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
319
+ backupVault: storageBackupVault,
320
+ });
321
+
322
+ storageBackupPlan.addRule(
323
+ new backup.BackupPlanRule({
324
+ ruleName: "papyrus-storage-pitr-35d",
325
+ enableContinuousBackup: true,
326
+ deleteAfter: Duration.days(35),
327
+ }),
328
+ );
329
+
330
+ storageBackupPlan.addRule(
331
+ new backup.BackupPlanRule({
332
+ ruleName: "papyrus-storage-daily-365d",
333
+ scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
334
+ deleteAfter: Duration.days(365),
335
+ }),
336
+ );
337
+
338
+ storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
339
+ allowRestores: true,
340
+ resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
341
+ });
342
+ }
343
+
344
+ // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
345
+ // storage ↔ data nested-stack circular dependencies from allow.resource().
346
+ const corporaObjectArn = `${storageBucket.bucketArn}/corpora/*`;
347
+ const mediaObjectArn = `${storageBucket.bucketArn}/media/*`;
348
+ const newsroomObjectArn = `${storageBucket.bucketArn}/newsroom/*`;
349
+ const grantCorporaRead = (lambda: LambdaFunction) => {
350
+ lambda.addToRolePolicy(
351
+ new PolicyStatement({
352
+ actions: ["s3:GetObject"],
353
+ resources: [corporaObjectArn],
354
+ }),
355
+ );
356
+ };
357
+ const grantNewsroomReadWrite = (lambda: LambdaFunction) => {
358
+ lambda.addToRolePolicy(
359
+ new PolicyStatement({
360
+ actions: ["s3:GetObject", "s3:PutObject"],
361
+ resources: [newsroomObjectArn],
362
+ }),
363
+ );
364
+ };
365
+ const grantNewsroomReadWriteDelete = (lambda: LambdaFunction) => {
366
+ lambda.addToRolePolicy(
367
+ new PolicyStatement({
368
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
369
+ resources: [newsroomObjectArn],
370
+ }),
371
+ );
372
+ };
373
+ const grantMediaReadWriteDelete = (lambda: LambdaFunction) => {
374
+ lambda.addToRolePolicy(
375
+ new PolicyStatement({
376
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
377
+ resources: [mediaObjectArn],
378
+ }),
379
+ );
380
+ };
381
+ const mediaBucketName = storageBucket.bucketName;
382
+ const withMediaBucketEnv = (resource: { addEnvironment: (key: string, value: string) => void }) => {
383
+ resource.addEnvironment("PAPYRUS_MEDIA_BUCKET_NAME", mediaBucketName);
384
+ resource.addEnvironment("papyrusMedia_BUCKET_NAME", mediaBucketName);
385
+ };
386
+ for (const resource of [
387
+ backend.assignmentAction,
388
+ backend.categoryAction,
389
+ backend.emailSubmissionProcessor,
390
+ backend.knowledgeQuery,
391
+ backend.modelAttachmentUpload,
392
+ backend.newsroomSummary,
393
+ backend.procedureAction,
394
+ backend.sesInboundReceive,
395
+ ]) {
396
+ withMediaBucketEnv(resource);
397
+ }
398
+ grantCorporaRead(backend.knowledgeQuery.resources.lambda as LambdaFunction);
399
+ grantNewsroomReadWrite(backend.assignmentAction.resources.lambda as LambdaFunction);
400
+ grantNewsroomReadWrite(backend.categoryAction.resources.lambda as LambdaFunction);
401
+ grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
402
+ grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
403
+ grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
404
+ if (backend.sesInboundReceive) {
405
+ grantNewsroomReadWrite(backend.sesInboundReceive.resources.lambda as LambdaFunction);
406
+ }
407
+
408
+ if (enableInboundEmail) {
409
+ if (!backend.sesInboundReceive || !backend.emailSubmissionProcessor) {
410
+ throw new Error("Inbound email is enabled but sesInboundReceive/emailSubmissionProcessor were not registered.");
411
+ }
412
+ const inboundReceive = backend.sesInboundReceive;
413
+ const inboundProcessor = backend.emailSubmissionProcessor;
414
+ const receiveLambda = inboundReceive.resources.lambda as LambdaFunction;
415
+ const processorLambda = inboundProcessor.resources.lambda as LambdaFunction;
416
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
417
+ const inboundRecipients = inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`);
418
+
419
+ inboundReceive.addEnvironment(
420
+ "PAPYRUS_EMAIL_SUBMISSION_PROCESSOR_FUNCTION_NAME",
421
+ processorLambda.functionName,
422
+ );
423
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
424
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS", inboundEmailLocalParts.join(","));
425
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
426
+ inboundReceive.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
427
+
428
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
429
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
430
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_EMAIL_ENABLED", "true");
431
+ inboundProcessor.addEnvironment(
432
+ "PAPYRUS_INBOUND_FEEDBACK_FROM_EMAIL",
433
+ `Papyrus Submissions <submissions@${inboundEmailDomain}>`,
434
+ );
435
+ inboundProcessor.addEnvironment("PAPYRUS_PUBLIC_SITE_BASE_URL", "https://p.apyr.us");
436
+ inboundProcessor.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
437
+
438
+ receiveLambda.addToRolePolicy(
439
+ new PolicyStatement({
440
+ actions: ["lambda:InvokeFunction"],
441
+ resources: [processorLambda.functionArn],
442
+ }),
443
+ );
444
+ receiveLambda.addToRolePolicy(
445
+ new PolicyStatement({
446
+ actions: ["s3:GetObject", "s3:ListBucket"],
447
+ resources: [
448
+ storageBucket.bucketArn,
449
+ `${storageBucket.bucketArn}/inbound-email/*`,
450
+ ],
451
+ }),
452
+ );
453
+ processorLambda.addToRolePolicy(
454
+ new PolicyStatement({
455
+ actions: ["s3:ListBucket"],
456
+ resources: [storageBucket.bucketArn],
457
+ }),
458
+ );
459
+ processorLambda.addToRolePolicy(
460
+ new PolicyStatement({
461
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
462
+ resources: [
463
+ `${storageBucket.bucketArn}/inbound-email/*`,
464
+ `${storageBucket.bucketArn}/inbound-email-archived/*`,
465
+ ],
466
+ }),
467
+ );
468
+ processorLambda.addToRolePolicy(
469
+ new PolicyStatement({
470
+ actions: ["s3:GetObject", "s3:PutObject"],
471
+ resources: [corporaObjectArn],
472
+ }),
473
+ );
474
+ receiveLambda.addToRolePolicy(
475
+ new PolicyStatement({
476
+ actions: ["appsync:GraphQL"],
477
+ resources: ["*"],
478
+ }),
479
+ );
480
+ processorLambda.addToRolePolicy(
481
+ new PolicyStatement({
482
+ actions: ["appsync:GraphQL"],
483
+ resources: ["*"],
484
+ }),
485
+ );
486
+ processorLambda.addToRolePolicy(
487
+ new PolicyStatement({
488
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
489
+ resources: ["*"],
490
+ }),
491
+ );
492
+
493
+ const inboundEventStack = Stack.of(receiveLambda);
494
+
495
+ // SES receipt rule sets are account-global; only provision on the production pipeline.
496
+ if (isAmplifyProductionPipeline) {
497
+ const inboundDnsZone = route53.HostedZone.fromHostedZoneAttributes(storageStack, "PapyrusInboundDnsZone", {
498
+ hostedZoneId: inboundDnsZoneId,
499
+ zoneName: inboundDnsZoneName,
500
+ });
501
+ const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
502
+ onCreate: {
503
+ service: "SES",
504
+ action: "verifyDomainIdentity",
505
+ parameters: {
506
+ Domain: inboundEmailDomain,
507
+ },
508
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
509
+ },
510
+ onUpdate: {
511
+ service: "SES",
512
+ action: "verifyDomainIdentity",
513
+ parameters: {
514
+ Domain: inboundEmailDomain,
515
+ },
516
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
517
+ },
518
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
519
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
520
+ }),
521
+ timeout: Duration.minutes(2),
522
+ });
523
+ new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
524
+ zone: inboundDnsZone,
525
+ recordName: `_amazonses.${inboundDnsRecordName}`,
526
+ ttl: Duration.minutes(5),
527
+ values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
528
+ });
529
+
530
+ const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
531
+ receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
532
+ });
533
+ inboundRuleSet.addRule("PapyrusInboundSubmissions", {
534
+ recipients: inboundRecipients,
535
+ enabled: true,
536
+ scanEnabled: true,
537
+ tlsPolicy: ses.TlsPolicy.REQUIRE,
538
+ actions: [
539
+ new sesActions.S3({
540
+ bucket: storageBucket,
541
+ objectKeyPrefix: "inbound-email/",
542
+ }),
543
+ ],
544
+ });
545
+
546
+ new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
547
+ onCreate: {
548
+ service: "SES",
549
+ action: "setActiveReceiptRuleSet",
550
+ parameters: {
551
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
552
+ },
553
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
554
+ },
555
+ onUpdate: {
556
+ service: "SES",
557
+ action: "setActiveReceiptRuleSet",
558
+ parameters: {
559
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
560
+ },
561
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
562
+ },
563
+ onDelete: {
564
+ service: "SES",
565
+ action: "setActiveReceiptRuleSet",
566
+ parameters: {
567
+ RuleSetName: null,
568
+ },
569
+ },
570
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
571
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
572
+ }),
573
+ timeout: Duration.minutes(2),
574
+ });
575
+ }
576
+
577
+ new InboundEmailStack(inboundEventStack, "InboundEmail", {
578
+ storageBucket,
579
+ receiveFunctionArn: receiveLambda.functionArn,
580
+ });
581
+ }
582
+
583
+ const knowledgeVectorsStack = backend.createStack("knowledge-vectors");
584
+ const knowledgeVectorBucket = new CfnVectorBucket(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucket", {
585
+ encryptionConfiguration: {
586
+ sseType: "AES256",
587
+ },
588
+ });
589
+ const knowledgeVectorIndex = new CfnIndex(knowledgeVectorsStack, "PapyrusKnowledgeVectorIndex", {
590
+ dataType: "float32",
591
+ dimension: knowledgeVectorDimension,
592
+ distanceMetric: "cosine",
593
+ indexName: knowledgeVectorIndexName,
594
+ metadataConfiguration: {
595
+ nonFilterableMetadataKeys: ["text", "summary", "sourceUri", "title"],
596
+ },
597
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
598
+ });
599
+ knowledgeVectorIndex.node.addDependency(knowledgeVectorBucket);
600
+ const knowledgeVectorBucketPolicy = new CfnVectorBucketPolicy(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucketPolicy", {
601
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
602
+ policy: {
603
+ Version: "2012-10-17",
604
+ Statement: [
605
+ {
606
+ Sid: "AllowSameAccountPapyrusKnowledgeVectorAccess",
607
+ Effect: "Allow",
608
+ Principal: {
609
+ AWS: `arn:aws:iam::${knowledgeVectorsStack.account}:root`,
610
+ },
611
+ Action: [
612
+ "s3vectors:GetIndex",
613
+ "s3vectors:GetVectors",
614
+ "s3vectors:ListVectors",
615
+ "s3vectors:PutVectors",
616
+ "s3vectors:QueryVectors",
617
+ "s3vectors:DeleteVectors",
618
+ ],
619
+ Resource: [
620
+ knowledgeVectorBucket.attrVectorBucketArn,
621
+ knowledgeVectorIndex.attrIndexArn,
622
+ ],
623
+ },
624
+ ],
625
+ },
626
+ });
627
+ knowledgeVectorBucketPolicy.node.addDependency(knowledgeVectorIndex);
628
+
629
+ const knowledgeQueryLambda = backend.knowledgeQuery.resources.lambda as LambdaFunction;
630
+ backend.knowledgeQuery.addEnvironment("OPENAI_API_KEY", secret("OPENAI_API_KEY"));
631
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_STORAGE_BUCKET_NAME", storageBucket.bucketName);
632
+ knowledgeQueryLambda.addEnvironment(
633
+ "PAPYRUS_GRAPHQL_ENDPOINT",
634
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
635
+ );
636
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_ARN", knowledgeVectorIndex.attrIndexArn);
637
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_NAME", knowledgeVectorIndexName);
638
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_MODEL", knowledgeEmbeddingModel);
639
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_DIMENSIONS", String(knowledgeVectorDimension));
640
+ knowledgeQueryLambda.addToRolePolicy(
641
+ new PolicyStatement({
642
+ actions: [
643
+ "s3vectors:GetIndex",
644
+ "s3vectors:GetVectors",
645
+ "s3vectors:ListVectors",
646
+ "s3vectors:QueryVectors",
647
+ ],
648
+ resources: [knowledgeVectorIndex.attrIndexArn],
649
+ }),
650
+ );
651
+ knowledgeQueryLambda.addToRolePolicy(
652
+ new PolicyStatement({
653
+ actions: ["ssm:GetParameter"],
654
+ resources: [
655
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/papyrus/*/OPENAI_API_KEY`,
656
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/papyrus/OPENAI_API_KEY`,
657
+ // Pipeline-deployed apps store secrets under amplify/<appId>/... rather
658
+ // than the sandbox's amplify/papyrus/... path. Grant the app's own
659
+ // secret path so a second CMS app (e.g. pilobol-us) can read its key.
660
+ ...(amplifyAppId
661
+ ? [
662
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/${amplifyAppId}/*/OPENAI_API_KEY`,
663
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/${amplifyAppId}/OPENAI_API_KEY`,
664
+ ]
665
+ : []),
666
+ ],
667
+ }),
668
+ );
669
+
670
+ backend.addOutput({
671
+ custom: {
672
+ inboundEmail: enableInboundEmail
673
+ ? {
674
+ domain: inboundEmailDomain,
675
+ localParts: inboundEmailLocalParts,
676
+ addresses: inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`),
677
+ corpusKey: inboundEmailCorpusKey,
678
+ }
679
+ : null,
680
+ knowledgeQuery: {
681
+ s3VectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
682
+ s3VectorIndexArn: knowledgeVectorIndex.attrIndexArn,
683
+ s3VectorIndexName: knowledgeVectorIndexName,
684
+ embeddingModel: knowledgeEmbeddingModel,
685
+ embeddingDimensions: knowledgeVectorDimension,
686
+ },
687
+ storageBackups: storageBackupPlan && storageBackupVault
688
+ ? {
689
+ backupPlanId: storageBackupPlan.backupPlanId,
690
+ backupVaultArn: storageBackupVault.backupVaultArn,
691
+ backupVaultName: storageBackupVault.backupVaultName,
692
+ protectedBucketArn: storageBucket.bucketArn,
693
+ protectedBucketName: storageBucket.bucketName,
694
+ }
695
+ : null,
696
+ slack: enableSlackAgent && slackEventsUrl
697
+ ? {
698
+ eventsUrl: slackEventsUrl,
699
+ }
700
+ : null,
701
+ },
702
+ });
703
+ return backend;
704
+ }