@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.
- package/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- 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
|
+
}
|