@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.40
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/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +45 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +212 -50
- package/infra/amplify-app-shell.ts +240 -53
- package/infra/build-specs.js +14 -5
- package/infra/build-specs.ts +17 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +167 -10
- package/infra/site-config.ts +196 -14
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Token } from "aws-cdk-lib";
|
|
3
|
+
|
|
4
|
+
export type SiteBackendFeatureFlags = {
|
|
5
|
+
consoleResponder: boolean;
|
|
6
|
+
inboundEmail: boolean;
|
|
7
|
+
slack: boolean;
|
|
8
|
+
storageBackups: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SiteBackendFeatureInput = Partial<SiteBackendFeatureFlags> | undefined;
|
|
12
|
+
|
|
13
|
+
export function sanitizeAwsName(value: string, maxLength = 50): string {
|
|
14
|
+
return value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
17
|
+
.replace(/-+/g, "-")
|
|
18
|
+
.replace(/^-|-$/g, "")
|
|
19
|
+
.slice(0, maxLength);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Account-global names are immutable in AWS, so they must be built from literal
|
|
24
|
+
* synth-time inputs only. A CDK token (or text that came from one, such as
|
|
25
|
+
* `token-token-17565`) makes the name change on every synth and fail with
|
|
26
|
+
* AlreadyExists, so this throws instead.
|
|
27
|
+
*/
|
|
28
|
+
export function assertLiteralResourceName(label: string, name: string): string {
|
|
29
|
+
if (Token.isUnresolved(name) || /token|\$\{/i.test(name)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${label} "${name}" is derived from an unresolved CDK token. Build account-global names from literal site config values only.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readBooleanEnvFlag(
|
|
38
|
+
environment: Record<string, string | undefined>,
|
|
39
|
+
name: string,
|
|
40
|
+
defaultValue: boolean,
|
|
41
|
+
): boolean {
|
|
42
|
+
const raw = (environment[name] ?? "").trim().toLowerCase();
|
|
43
|
+
if (["1", "true", "yes", "on"].includes(raw)) return true;
|
|
44
|
+
if (["0", "false", "no", "off"].includes(raw)) return false;
|
|
45
|
+
return defaultValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SiteBackendIdentity = {
|
|
49
|
+
brandId: string;
|
|
50
|
+
amplifyAppId: string;
|
|
51
|
+
amplifyBranch: string;
|
|
52
|
+
productionAppId: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True only for the Amplify app that owns the original p.apyr.us backend. That
|
|
57
|
+
* backend keeps its historical account-global names so its live resources are
|
|
58
|
+
* never replaced. Every other site gets brand-scoped names.
|
|
59
|
+
*/
|
|
60
|
+
export function isLegacyProductionApp(identity: SiteBackendIdentity): boolean {
|
|
61
|
+
return identity.productionAppId !== "" && identity.amplifyAppId === identity.productionAppId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isLegacyProductionPipeline(identity: SiteBackendIdentity): boolean {
|
|
65
|
+
return isLegacyProductionApp(identity) && identity.amplifyBranch === "main";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveSiteBackendFeatureFlags(
|
|
69
|
+
features: SiteBackendFeatureInput,
|
|
70
|
+
environment: Record<string, string | undefined>,
|
|
71
|
+
identity: SiteBackendIdentity,
|
|
72
|
+
): SiteBackendFeatureFlags {
|
|
73
|
+
const legacyPipeline = isLegacyProductionPipeline(identity);
|
|
74
|
+
return {
|
|
75
|
+
consoleResponder:
|
|
76
|
+
features?.consoleResponder ?? readBooleanEnvFlag(environment, "PAPYRUS_ENABLE_CONSOLE_RESPONDER", true),
|
|
77
|
+
inboundEmail:
|
|
78
|
+
features?.inboundEmail ?? readBooleanEnvFlag(environment, "PAPYRUS_ENABLE_INBOUND_EMAIL", legacyPipeline),
|
|
79
|
+
slack: features?.slack ?? readBooleanEnvFlag(environment, "PAPYRUS_ENABLE_SLACK", false),
|
|
80
|
+
storageBackups:
|
|
81
|
+
features?.storageBackups ?? readBooleanEnvFlag(environment, "PAPYRUS_ENABLE_STORAGE_BACKUPS", legacyPipeline),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function deriveKnowledgeVectorIndexName(identity: SiteBackendIdentity): string {
|
|
86
|
+
const name = isLegacyProductionApp(identity)
|
|
87
|
+
? "papyrus-knowledge"
|
|
88
|
+
: `papyrus-knowledge-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
89
|
+
return assertLiteralResourceName("Knowledge vector index name", name);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deriveStorageBackupVaultName(identity: SiteBackendIdentity, explicitName?: string): string {
|
|
93
|
+
const explicit = (explicitName ?? "").trim();
|
|
94
|
+
if (explicit !== "") return assertLiteralResourceName("Backup vault name", sanitizeAwsName(explicit));
|
|
95
|
+
if (isLegacyProductionPipeline(identity)) {
|
|
96
|
+
return assertLiteralResourceName(
|
|
97
|
+
"Backup vault name",
|
|
98
|
+
sanitizeAwsName(`papyrus-${identity.productionAppId}-main-media-backup-vault`),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const literalSiteKey = [identity.brandId, identity.amplifyAppId, identity.amplifyBranch].join("|");
|
|
102
|
+
const siteHash = createHash("sha256").update(literalSiteKey).digest("hex").slice(0, 8);
|
|
103
|
+
return assertLiteralResourceName(
|
|
104
|
+
"Backup vault name",
|
|
105
|
+
`papyrus-${sanitizeAwsName(identity.brandId, 20)}-${siteHash}-media-vault`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The `papyrus-site-inbound-` prefix keeps a brand named `p-apyr-us` from
|
|
111
|
+
* colliding with the legacy `papyrus-inbound-p-apyr-us` set.
|
|
112
|
+
* SES allows one ACTIVE receipt rule set per region. A non-legacy site creates
|
|
113
|
+
* its own brand-named rule set and never activates it from CloudFormation.
|
|
114
|
+
*/
|
|
115
|
+
export function deriveReceiptRuleSetName(identity: SiteBackendIdentity, inboundEmailDomain: string): string {
|
|
116
|
+
const name = isLegacyProductionApp(identity)
|
|
117
|
+
? `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`
|
|
118
|
+
: `papyrus-site-inbound-${sanitizeAwsName(identity.brandId, 40)}`;
|
|
119
|
+
return assertLiteralResourceName("Receipt rule set name", name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `undefined` keeps the legacy auto-generated rule name of the live p.apyr.us rule. */
|
|
123
|
+
export function deriveReceiptRuleName(identity: SiteBackendIdentity): string | undefined {
|
|
124
|
+
if (isLegacyProductionApp(identity)) return undefined;
|
|
125
|
+
return assertLiteralResourceName("Receipt rule name", `${sanitizeAwsName(identity.brandId, 40)}-inbound-submissions`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type InboundEmailSesPlan = {
|
|
129
|
+
createReceiptRules: boolean;
|
|
130
|
+
activateReceiptRuleSet: boolean;
|
|
131
|
+
manageDomainIdentity: boolean;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function planInboundEmailSes(
|
|
135
|
+
identity: SiteBackendIdentity,
|
|
136
|
+
inboundEmailEnabled: boolean,
|
|
137
|
+
options: { manageDomainIdentity?: boolean } | undefined,
|
|
138
|
+
): InboundEmailSesPlan {
|
|
139
|
+
const legacyPipeline = isLegacyProductionPipeline(identity);
|
|
140
|
+
const deployedMainBranch = identity.amplifyAppId !== "" && identity.amplifyBranch === "main";
|
|
141
|
+
const createReceiptRules = inboundEmailEnabled && (legacyPipeline || deployedMainBranch);
|
|
142
|
+
return {
|
|
143
|
+
createReceiptRules,
|
|
144
|
+
activateReceiptRuleSet: createReceiptRules && legacyPipeline,
|
|
145
|
+
manageDomainIdentity: createReceiptRules && (legacyPipeline || options?.manageDomainIdentity === true),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type SiteGlobalResourceNames = {
|
|
150
|
+
knowledgeVectorIndexName: string;
|
|
151
|
+
storageBackupVaultName: string;
|
|
152
|
+
receiptRuleSetName: string;
|
|
153
|
+
receiptRuleName: string | undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function deriveSiteGlobalResourceNames(
|
|
157
|
+
identity: SiteBackendIdentity,
|
|
158
|
+
inboundEmailDomain: string,
|
|
159
|
+
): SiteGlobalResourceNames {
|
|
160
|
+
return {
|
|
161
|
+
knowledgeVectorIndexName: deriveKnowledgeVectorIndexName(identity),
|
|
162
|
+
storageBackupVaultName: deriveStorageBackupVaultName(identity),
|
|
163
|
+
receiptRuleSetName: deriveReceiptRuleSetName(identity, inboundEmailDomain),
|
|
164
|
+
receiptRuleName: deriveReceiptRuleName(identity),
|
|
165
|
+
};
|
|
166
|
+
}
|
package/amplify/site-backend.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { defineBackend, secret } from "@aws-amplify/backend";
|
|
2
|
-
import {
|
|
3
|
-
import * as backup from "aws-cdk-lib/aws-backup";
|
|
4
|
-
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
|
|
2
|
+
import { Stack } from "aws-cdk-lib";
|
|
5
3
|
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
4
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
11
5
|
import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
|
|
12
6
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
13
7
|
import { dirname, resolve } from "node:path";
|
|
14
8
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import {
|
|
9
|
+
import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves.js";
|
|
10
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
11
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
16
12
|
import { data } from "./data/resource.js";
|
|
17
13
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
18
14
|
import { categoryAction } from "./functions/category-action/resource.js";
|
|
19
15
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
|
|
20
|
-
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
|
|
21
16
|
import { contentActions } from "./functions/content-actions/resource.js";
|
|
22
17
|
import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
|
|
23
18
|
import { manageUserRole } from "./functions/manage-user-role/resource.js";
|
|
@@ -29,7 +24,10 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
|
|
|
29
24
|
import { sesInboundReceive } from "./functions/ses-inbound-receive/resource.js";
|
|
30
25
|
import { slackDelivery } from "./functions/slack-delivery/resource.js";
|
|
31
26
|
import { slackEvents } from "./functions/slack-events/resource.js";
|
|
27
|
+
import { addInboundEmailSesIntake } from "./inbound-email/ses-intake.js";
|
|
32
28
|
import { InboundEmailStack } from "./inbound-email/stack.js";
|
|
29
|
+
import { deriveKnowledgeVectorIndexName, deriveReceiptRuleName, deriveReceiptRuleSetName, deriveStorageBackupVaultName, isLegacyProductionPipeline, planInboundEmailSes, resolveSiteBackendFeatureFlags, } from "./site-backend-names.js";
|
|
30
|
+
import { addStorageBackups } from "./storage-backups/construct.js";
|
|
33
31
|
import { storage } from "./storage/resource.js";
|
|
34
32
|
export function defineSiteBackend(papyrusSite) {
|
|
35
33
|
// `papyrus.config.ts`'s default export. Backend options live under `backend`;
|
|
@@ -39,50 +37,36 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
39
37
|
const knowledgeEmbeddingModel = "text-embedding-3-small";
|
|
40
38
|
const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
|
|
41
39
|
const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
|
|
42
|
-
// Amplify app id whose `main` branch is
|
|
43
|
-
//
|
|
40
|
+
// Amplify app id whose `main` branch is the original p.apyr.us backend; only
|
|
41
|
+
// that app keeps the historical account-global names (see site-backend-names.ts).
|
|
44
42
|
const productionAppId = (site.productionAppId ?? "").trim();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const papyrusSiteBrand = (site.brandId
|
|
67
|
-
?? process.env.PAPYRUS_SITE_BRAND
|
|
68
|
-
?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
|
|
69
|
-
?? "papyrus").trim().toLowerCase();
|
|
70
|
-
const knowledgeVectorIndexName = productionAppId !== "" && amplifyAppId === productionAppId
|
|
71
|
-
? "papyrus-knowledge"
|
|
72
|
-
: `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
|
|
73
|
-
// SES active receipt rule sets and shared backup vault names are account-global.
|
|
74
|
-
// Keep them on the production pipeline only unless explicitly opted in.
|
|
75
|
-
// A feature flag in the site config wins over the environment.
|
|
76
|
-
const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
|
|
77
|
-
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
78
|
-
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
43
|
+
const siteBackendIdentity = {
|
|
44
|
+
brandId: (site.brandId
|
|
45
|
+
?? process.env.PAPYRUS_SITE_BRAND
|
|
46
|
+
?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
|
|
47
|
+
?? "papyrus").trim().toLowerCase(),
|
|
48
|
+
amplifyAppId,
|
|
49
|
+
amplifyBranch,
|
|
50
|
+
productionAppId,
|
|
51
|
+
};
|
|
52
|
+
const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
|
|
53
|
+
const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
|
|
54
|
+
const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
|
|
55
|
+
const enableInboundEmail = featureFlags.inboundEmail;
|
|
56
|
+
const enableSlackAgent = featureFlags.slack;
|
|
57
|
+
const enableStorageBackups = featureFlags.storageBackups;
|
|
58
|
+
const baseAuthConfig = site.auth ?? authConfigFromEnv();
|
|
59
|
+
const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
|
|
60
|
+
const authConfig = {
|
|
61
|
+
...baseAuthConfig,
|
|
62
|
+
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
63
|
+
};
|
|
79
64
|
const backend = defineBackend({
|
|
80
65
|
assignmentAction,
|
|
81
|
-
auth: defineSiteAuth(
|
|
66
|
+
auth: defineSiteAuth(authConfig),
|
|
82
67
|
categoryAction,
|
|
83
68
|
contentActions,
|
|
84
69
|
data,
|
|
85
|
-
graphqlJwtAuthorizer,
|
|
86
70
|
knowledgeQuery,
|
|
87
71
|
manageUserRole,
|
|
88
72
|
modelAttachmentUpload,
|
|
@@ -95,10 +79,17 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
95
79
|
slackDelivery,
|
|
96
80
|
storage,
|
|
97
81
|
});
|
|
82
|
+
orderModelStacksInWaves(backend.data.resources.graphqlApi.node.scope, Object.keys(backend.data.resources.tables), site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE);
|
|
83
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
84
|
+
if (authConfig.applyCognitoDomainPrefix === true
|
|
85
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
86
|
+
&& authConfig.disableGoogleOAuth !== true) {
|
|
87
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
88
|
+
}
|
|
98
89
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
99
90
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
100
91
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
|
101
|
-
const enableConsoleResponder =
|
|
92
|
+
const enableConsoleResponder = featureFlags.consoleResponder;
|
|
102
93
|
if (enableConsoleResponder
|
|
103
94
|
&& !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
|
|
104
95
|
&& !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
|
|
@@ -138,36 +129,18 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
138
129
|
const messageThreadTable = backend.data.resources.tables.MessageThread;
|
|
139
130
|
const dataStack = Stack.of(messageTable);
|
|
140
131
|
const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
|
|
141
|
-
const
|
|
142
|
-
const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
|
|
143
|
-
const jwtAuthorizerVariables = jwtAuthorizerEnvironment
|
|
144
|
-
&& typeof jwtAuthorizerEnvironment === "object"
|
|
145
|
-
&& "variables" in jwtAuthorizerEnvironment
|
|
146
|
-
? jwtAuthorizerEnvironment.variables
|
|
147
|
-
: undefined;
|
|
148
|
-
const jwtSsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
|
|
149
|
-
|| jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
|
|
150
|
-
|| "";
|
|
132
|
+
const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
|
|
151
133
|
if (enableConsoleResponder) {
|
|
152
|
-
|
|
134
|
+
new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
|
|
153
135
|
messageTable,
|
|
154
136
|
messageStreamArn,
|
|
155
137
|
threadTable: messageThreadTable,
|
|
156
138
|
projectRoot,
|
|
157
139
|
graphqlEndpoint,
|
|
158
|
-
amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
|
|
159
140
|
responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
|
|
160
141
|
model: process.env.PAPYRUS_CONSOLE_MODEL,
|
|
161
142
|
prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
|
|
162
143
|
});
|
|
163
|
-
if (jwtSsmEnvConfig) {
|
|
164
|
-
consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
|
|
165
|
-
}
|
|
166
|
-
const jwtSecretSsmParam = process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
|
|
167
|
-
|| (productionAppId !== "" && amplifyAppId === productionAppId
|
|
168
|
-
? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
|
|
169
|
-
: "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
|
|
170
|
-
consoleChatResponder.responderFunction.addEnvironment("PAPYRUS_JWT_SECRET_SSM_PARAM", jwtSecretSsmParam);
|
|
171
144
|
}
|
|
172
145
|
if (enableSlackAgent) {
|
|
173
146
|
const slackEventsLambda = backend.slackEvents.resources.lambda;
|
|
@@ -179,9 +152,9 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
179
152
|
backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
180
153
|
backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
|
|
181
154
|
backend.slackEvents.addEnvironment("PAPYRUS_SLACK_ALLOWED_USER_IDS", (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim());
|
|
182
|
-
if (
|
|
183
|
-
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
184
|
-
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
155
|
+
if (amplifySsmEnvConfig) {
|
|
156
|
+
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
157
|
+
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
185
158
|
}
|
|
186
159
|
backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
187
160
|
backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
|
|
@@ -249,31 +222,13 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
249
222
|
}
|
|
250
223
|
if (enableStorageBackups) {
|
|
251
224
|
const storageBackupsStack = backend.createStack("storage-backups");
|
|
252
|
-
const storageBackupVaultName =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
: `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`));
|
|
256
|
-
storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
|
|
225
|
+
const storageBackupVaultName = deriveStorageBackupVaultName(siteBackendIdentity, process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME);
|
|
226
|
+
const storageBackups = addStorageBackups(storageBackupsStack, {
|
|
227
|
+
storageBucket,
|
|
257
228
|
backupVaultName: storageBackupVaultName,
|
|
258
|
-
removalPolicy: RemovalPolicy.RETAIN,
|
|
259
|
-
});
|
|
260
|
-
storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
|
|
261
|
-
backupVault: storageBackupVault,
|
|
262
|
-
});
|
|
263
|
-
storageBackupPlan.addRule(new backup.BackupPlanRule({
|
|
264
|
-
ruleName: "papyrus-storage-pitr-35d",
|
|
265
|
-
enableContinuousBackup: true,
|
|
266
|
-
deleteAfter: Duration.days(35),
|
|
267
|
-
}));
|
|
268
|
-
storageBackupPlan.addRule(new backup.BackupPlanRule({
|
|
269
|
-
ruleName: "papyrus-storage-daily-365d",
|
|
270
|
-
scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
|
|
271
|
-
deleteAfter: Duration.days(365),
|
|
272
|
-
}));
|
|
273
|
-
storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
|
|
274
|
-
allowRestores: true,
|
|
275
|
-
resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
|
|
276
229
|
});
|
|
230
|
+
storageBackupVault = storageBackups.vault;
|
|
231
|
+
storageBackupPlan = storageBackups.plan;
|
|
277
232
|
}
|
|
278
233
|
// Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
|
|
279
234
|
// storage ↔ data nested-stack circular dependencies from allow.resource().
|
|
@@ -333,6 +288,32 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
333
288
|
actions: ["appsync:GraphQL"],
|
|
334
289
|
resources: ["*"],
|
|
335
290
|
}));
|
|
291
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
292
|
+
reader: site.reader,
|
|
293
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
294
|
+
cmsAppId: amplifyAppId,
|
|
295
|
+
region: Stack.of(contentActionsLambda).region,
|
|
296
|
+
account: Stack.of(contentActionsLambda).account,
|
|
297
|
+
});
|
|
298
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
299
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
300
|
+
}
|
|
301
|
+
if (rebuildSettings.resources.length > 0) {
|
|
302
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
|
|
303
|
+
}
|
|
304
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
305
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
306
|
+
const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
|
|
307
|
+
if (!revalidateSecretParameter.startsWith("/")) {
|
|
308
|
+
throw new Error("revalidateBaseUrl is set but PAPYRUS_REVALIDATE_SECRET_PARAMETER is not (an SSM parameter name starting with /). The app-shell template sets it on the production branch for Pretext sites; see docs/site-hosting.md.");
|
|
309
|
+
}
|
|
310
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
|
|
311
|
+
const revalidateStack = Stack.of(contentActionsLambda);
|
|
312
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({
|
|
313
|
+
actions: ["ssm:GetParameter"],
|
|
314
|
+
resources: [`arn:${revalidateStack.partition}:ssm:${revalidateStack.region}:${revalidateStack.account}:parameter${revalidateSecretParameter}`],
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
336
317
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
337
318
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
338
319
|
if (backend.sesInboundReceive) {
|
|
@@ -398,83 +379,25 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
398
379
|
resources: ["*"],
|
|
399
380
|
}));
|
|
400
381
|
const inboundEventStack = Stack.of(receiveLambda);
|
|
401
|
-
// SES receipt rule
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
|
|
408
|
-
onCreate: {
|
|
409
|
-
service: "SES",
|
|
410
|
-
action: "verifyDomainIdentity",
|
|
411
|
-
parameters: {
|
|
412
|
-
Domain: inboundEmailDomain,
|
|
413
|
-
},
|
|
414
|
-
physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
|
|
415
|
-
},
|
|
416
|
-
onUpdate: {
|
|
417
|
-
service: "SES",
|
|
418
|
-
action: "verifyDomainIdentity",
|
|
419
|
-
parameters: {
|
|
420
|
-
Domain: inboundEmailDomain,
|
|
421
|
-
},
|
|
422
|
-
physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
|
|
423
|
-
},
|
|
424
|
-
policy: AwsCustomResourcePolicy.fromSdkCalls({
|
|
425
|
-
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
|
|
426
|
-
}),
|
|
427
|
-
timeout: Duration.minutes(2),
|
|
428
|
-
});
|
|
429
|
-
new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
|
|
430
|
-
zone: inboundDnsZone,
|
|
431
|
-
recordName: `_amazonses.${inboundDnsRecordName}`,
|
|
432
|
-
ttl: Duration.minutes(5),
|
|
433
|
-
values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
|
|
434
|
-
});
|
|
435
|
-
const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
|
|
436
|
-
receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
|
|
437
|
-
});
|
|
438
|
-
inboundRuleSet.addRule("PapyrusInboundSubmissions", {
|
|
382
|
+
// SES allows one active receipt rule set per region, so a non-legacy site
|
|
383
|
+
// creates its own brand-named rule set and never activates it here.
|
|
384
|
+
const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
|
|
385
|
+
if (inboundSesPlan.createReceiptRules) {
|
|
386
|
+
addInboundEmailSesIntake(storageStack, {
|
|
387
|
+
storageBucket,
|
|
439
388
|
recipients: inboundRecipients,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
service: "SES",
|
|
453
|
-
action: "setActiveReceiptRuleSet",
|
|
454
|
-
parameters: {
|
|
455
|
-
RuleSetName: inboundRuleSet.receiptRuleSetName,
|
|
456
|
-
},
|
|
457
|
-
physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
|
|
458
|
-
},
|
|
459
|
-
onUpdate: {
|
|
460
|
-
service: "SES",
|
|
461
|
-
action: "setActiveReceiptRuleSet",
|
|
462
|
-
parameters: {
|
|
463
|
-
RuleSetName: inboundRuleSet.receiptRuleSetName,
|
|
464
|
-
},
|
|
465
|
-
physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
|
|
466
|
-
},
|
|
467
|
-
onDelete: {
|
|
468
|
-
service: "SES",
|
|
469
|
-
action: "setActiveReceiptRuleSet",
|
|
470
|
-
parameters: {
|
|
471
|
-
RuleSetName: null,
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
policy: AwsCustomResourcePolicy.fromSdkCalls({
|
|
475
|
-
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
|
|
476
|
-
}),
|
|
477
|
-
timeout: Duration.minutes(2),
|
|
389
|
+
ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
|
|
390
|
+
existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
|
|
391
|
+
ruleName: deriveReceiptRuleName(siteBackendIdentity),
|
|
392
|
+
activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
|
|
393
|
+
domainIdentity: inboundSesPlan.manageDomainIdentity
|
|
394
|
+
? {
|
|
395
|
+
domain: inboundEmailDomain,
|
|
396
|
+
hostedZoneId: inboundDnsZoneId,
|
|
397
|
+
hostedZoneName: inboundDnsZoneName,
|
|
398
|
+
recordName: inboundDnsRecordName,
|
|
399
|
+
}
|
|
400
|
+
: undefined,
|
|
478
401
|
});
|
|
479
402
|
}
|
|
480
403
|
new InboundEmailStack(inboundEventStack, "InboundEmail", {
|