@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41
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 +228 -50
- package/infra/amplify-app-shell.ts +256 -53
- package/infra/build-specs.js +15 -5
- package/infra/build-specs.ts +18 -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 +236 -11
- package/infra/site-config.ts +276 -15
- 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
package/amplify/site-backend.ts
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
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";
|
|
3
|
+
import type * as backup from "aws-cdk-lib/aws-backup";
|
|
5
4
|
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
5
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
11
|
-
import { CfnEventSourceMapping,
|
|
6
|
+
import { CfnEventSourceMapping, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
|
|
12
7
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
13
8
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
9
|
+
import type { Construct } from "constructs";
|
|
14
10
|
import { dirname, resolve } from "node:path";
|
|
15
11
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
12
|
+
import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves";
|
|
13
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
14
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
17
15
|
import { data } from "./data/resource";
|
|
18
16
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
19
17
|
import { categoryAction } from "./functions/category-action/resource";
|
|
20
18
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
|
|
21
|
-
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
|
|
22
19
|
import { contentActions } from "./functions/content-actions/resource";
|
|
23
20
|
import { knowledgeQuery } from "./functions/knowledge-query/resource";
|
|
24
21
|
import { manageUserRole } from "./functions/manage-user-role/resource";
|
|
@@ -30,7 +27,19 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
|
|
|
30
27
|
import { sesInboundReceive } from "./functions/ses-inbound-receive/resource";
|
|
31
28
|
import { slackDelivery } from "./functions/slack-delivery/resource";
|
|
32
29
|
import { slackEvents } from "./functions/slack-events/resource";
|
|
30
|
+
import { addInboundEmailSesIntake } from "./inbound-email/ses-intake";
|
|
33
31
|
import { InboundEmailStack } from "./inbound-email/stack";
|
|
32
|
+
import {
|
|
33
|
+
deriveKnowledgeVectorIndexName,
|
|
34
|
+
deriveReceiptRuleName,
|
|
35
|
+
deriveReceiptRuleSetName,
|
|
36
|
+
deriveStorageBackupVaultName,
|
|
37
|
+
isLegacyProductionPipeline,
|
|
38
|
+
planInboundEmailSes,
|
|
39
|
+
resolveSiteBackendFeatureFlags,
|
|
40
|
+
type SiteBackendIdentity,
|
|
41
|
+
} from "./site-backend-names";
|
|
42
|
+
import { addStorageBackups } from "./storage-backups/construct";
|
|
34
43
|
import { storage } from "./storage/resource";
|
|
35
44
|
import type { PapyrusSite } from "../lib/define-site";
|
|
36
45
|
|
|
@@ -43,57 +52,39 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
43
52
|
|
|
44
53
|
const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
|
|
45
54
|
const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
|
|
46
|
-
// Amplify app id whose `main` branch is
|
|
47
|
-
//
|
|
55
|
+
// Amplify app id whose `main` branch is the original p.apyr.us backend; only
|
|
56
|
+
// that app keeps the historical account-global names (see site-backend-names.ts).
|
|
48
57
|
const productionAppId = (site.productionAppId ?? "").trim();
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
site.brandId
|
|
74
|
-
?? process.env.PAPYRUS_SITE_BRAND
|
|
75
|
-
?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
|
|
76
|
-
?? "papyrus"
|
|
77
|
-
).trim().toLowerCase();
|
|
78
|
-
const knowledgeVectorIndexName =
|
|
79
|
-
productionAppId !== "" && amplifyAppId === productionAppId
|
|
80
|
-
? "papyrus-knowledge"
|
|
81
|
-
: `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
|
|
82
|
-
|
|
83
|
-
// SES active receipt rule sets and shared backup vault names are account-global.
|
|
84
|
-
// Keep them on the production pipeline only unless explicitly opted in.
|
|
85
|
-
// A feature flag in the site config wins over the environment.
|
|
86
|
-
const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
|
|
87
|
-
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
88
|
-
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
89
|
-
|
|
58
|
+
const siteBackendIdentity: SiteBackendIdentity = {
|
|
59
|
+
brandId: (
|
|
60
|
+
site.brandId
|
|
61
|
+
?? process.env.PAPYRUS_SITE_BRAND
|
|
62
|
+
?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
|
|
63
|
+
?? "papyrus"
|
|
64
|
+
).trim().toLowerCase(),
|
|
65
|
+
amplifyAppId,
|
|
66
|
+
amplifyBranch,
|
|
67
|
+
productionAppId,
|
|
68
|
+
};
|
|
69
|
+
const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
|
|
70
|
+
const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
|
|
71
|
+
const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
|
|
72
|
+
const enableInboundEmail = featureFlags.inboundEmail;
|
|
73
|
+
const enableSlackAgent = featureFlags.slack;
|
|
74
|
+
const enableStorageBackups = featureFlags.storageBackups;
|
|
75
|
+
|
|
76
|
+
const baseAuthConfig = site.auth ?? authConfigFromEnv();
|
|
77
|
+
const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
|
|
78
|
+
const authConfig = {
|
|
79
|
+
...baseAuthConfig,
|
|
80
|
+
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
81
|
+
};
|
|
90
82
|
const backend = defineBackend({
|
|
91
83
|
assignmentAction,
|
|
92
|
-
auth: defineSiteAuth(
|
|
84
|
+
auth: defineSiteAuth(authConfig),
|
|
93
85
|
categoryAction,
|
|
94
86
|
contentActions,
|
|
95
87
|
data,
|
|
96
|
-
graphqlJwtAuthorizer,
|
|
97
88
|
knowledgeQuery,
|
|
98
89
|
manageUserRole,
|
|
99
90
|
modelAttachmentUpload,
|
|
@@ -107,10 +98,25 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
107
98
|
storage,
|
|
108
99
|
});
|
|
109
100
|
|
|
101
|
+
orderModelStacksInWaves(
|
|
102
|
+
backend.data.resources.graphqlApi.node.scope as Construct,
|
|
103
|
+
Object.keys(backend.data.resources.tables),
|
|
104
|
+
site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
108
|
+
if (
|
|
109
|
+
authConfig.applyCognitoDomainPrefix === true
|
|
110
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
111
|
+
&& authConfig.disableGoogleOAuth !== true
|
|
112
|
+
) {
|
|
113
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
111
117
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
112
118
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
|
113
|
-
const enableConsoleResponder =
|
|
119
|
+
const enableConsoleResponder = featureFlags.consoleResponder;
|
|
114
120
|
|
|
115
121
|
if (
|
|
116
122
|
enableConsoleResponder
|
|
@@ -159,43 +165,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
159
165
|
const messageThreadTable = backend.data.resources.tables.MessageThread;
|
|
160
166
|
const dataStack = Stack.of(messageTable);
|
|
161
167
|
const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
|
|
162
|
-
const
|
|
163
|
-
const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
|
|
164
|
-
const jwtAuthorizerVariables =
|
|
165
|
-
jwtAuthorizerEnvironment
|
|
166
|
-
&& typeof jwtAuthorizerEnvironment === "object"
|
|
167
|
-
&& "variables" in jwtAuthorizerEnvironment
|
|
168
|
-
? (jwtAuthorizerEnvironment as { variables?: Record<string, string> }).variables
|
|
169
|
-
: undefined;
|
|
170
|
-
const jwtSsmEnvConfig =
|
|
171
|
-
process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
|
|
172
|
-
|| jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
|
|
173
|
-
|| "";
|
|
168
|
+
const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
|
|
174
169
|
|
|
175
170
|
if (enableConsoleResponder) {
|
|
176
|
-
|
|
171
|
+
new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
|
|
177
172
|
messageTable,
|
|
178
173
|
messageStreamArn,
|
|
179
174
|
threadTable: messageThreadTable,
|
|
180
175
|
projectRoot,
|
|
181
176
|
graphqlEndpoint,
|
|
182
|
-
amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
|
|
183
177
|
responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
|
|
184
178
|
model: process.env.PAPYRUS_CONSOLE_MODEL,
|
|
185
179
|
prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
|
|
186
180
|
});
|
|
187
|
-
if (jwtSsmEnvConfig) {
|
|
188
|
-
consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
|
|
189
|
-
}
|
|
190
|
-
const jwtSecretSsmParam =
|
|
191
|
-
process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
|
|
192
|
-
|| (productionAppId !== "" && amplifyAppId === productionAppId
|
|
193
|
-
? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
|
|
194
|
-
: "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
|
|
195
|
-
consoleChatResponder.responderFunction.addEnvironment(
|
|
196
|
-
"PAPYRUS_JWT_SECRET_SSM_PARAM",
|
|
197
|
-
jwtSecretSsmParam,
|
|
198
|
-
);
|
|
199
181
|
}
|
|
200
182
|
|
|
201
183
|
if (enableSlackAgent) {
|
|
@@ -216,9 +198,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
216
198
|
"PAPYRUS_SLACK_ALLOWED_USER_IDS",
|
|
217
199
|
(process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
|
|
218
200
|
);
|
|
219
|
-
if (
|
|
220
|
-
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
221
|
-
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
201
|
+
if (amplifySsmEnvConfig) {
|
|
202
|
+
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
203
|
+
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
222
204
|
}
|
|
223
205
|
backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
224
206
|
backend.slackDelivery.addEnvironment(
|
|
@@ -306,41 +288,17 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
306
288
|
|
|
307
289
|
if (enableStorageBackups) {
|
|
308
290
|
const storageBackupsStack = backend.createStack("storage-backups");
|
|
309
|
-
const storageBackupVaultName =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
? `papyrus-${productionAppId}-main-media-backup-vault`
|
|
313
|
-
: `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`),
|
|
291
|
+
const storageBackupVaultName = deriveStorageBackupVaultName(
|
|
292
|
+
siteBackendIdentity,
|
|
293
|
+
process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME,
|
|
314
294
|
);
|
|
315
295
|
|
|
316
|
-
|
|
296
|
+
const storageBackups = addStorageBackups(storageBackupsStack, {
|
|
297
|
+
storageBucket,
|
|
317
298
|
backupVaultName: storageBackupVaultName,
|
|
318
|
-
removalPolicy: RemovalPolicy.RETAIN,
|
|
319
|
-
});
|
|
320
|
-
storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
|
|
321
|
-
backupVault: storageBackupVault,
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
storageBackupPlan.addRule(
|
|
325
|
-
new backup.BackupPlanRule({
|
|
326
|
-
ruleName: "papyrus-storage-pitr-35d",
|
|
327
|
-
enableContinuousBackup: true,
|
|
328
|
-
deleteAfter: Duration.days(35),
|
|
329
|
-
}),
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
storageBackupPlan.addRule(
|
|
333
|
-
new backup.BackupPlanRule({
|
|
334
|
-
ruleName: "papyrus-storage-daily-365d",
|
|
335
|
-
scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
|
|
336
|
-
deleteAfter: Duration.days(365),
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
|
|
341
|
-
allowRestores: true,
|
|
342
|
-
resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
|
|
343
299
|
});
|
|
300
|
+
storageBackupVault = storageBackups.vault;
|
|
301
|
+
storageBackupPlan = storageBackups.plan;
|
|
344
302
|
}
|
|
345
303
|
|
|
346
304
|
// Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
|
|
@@ -414,6 +372,38 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
414
372
|
resources: ["*"],
|
|
415
373
|
}),
|
|
416
374
|
);
|
|
375
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
376
|
+
reader: site.reader,
|
|
377
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
378
|
+
cmsAppId: amplifyAppId,
|
|
379
|
+
region: Stack.of(contentActionsLambda).region,
|
|
380
|
+
account: Stack.of(contentActionsLambda).account,
|
|
381
|
+
});
|
|
382
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
383
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
384
|
+
}
|
|
385
|
+
if (rebuildSettings.resources.length > 0) {
|
|
386
|
+
contentActionsLambda.addToRolePolicy(
|
|
387
|
+
new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
391
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
392
|
+
const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
|
|
393
|
+
if (!revalidateSecretParameter.startsWith("/")) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"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.",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
|
|
399
|
+
const revalidateStack = Stack.of(contentActionsLambda);
|
|
400
|
+
contentActionsLambda.addToRolePolicy(
|
|
401
|
+
new PolicyStatement({
|
|
402
|
+
actions: ["ssm:GetParameter"],
|
|
403
|
+
resources: [`arn:${revalidateStack.partition}:ssm:${revalidateStack.region}:${revalidateStack.account}:parameter${revalidateSecretParameter}`],
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
417
407
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
418
408
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
419
409
|
if (backend.sesInboundReceive) {
|
|
@@ -507,86 +497,26 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
507
497
|
|
|
508
498
|
const inboundEventStack = Stack.of(receiveLambda);
|
|
509
499
|
|
|
510
|
-
// SES receipt rule
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
},
|
|
531
|
-
physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
|
|
532
|
-
},
|
|
533
|
-
policy: AwsCustomResourcePolicy.fromSdkCalls({
|
|
534
|
-
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
|
|
535
|
-
}),
|
|
536
|
-
timeout: Duration.minutes(2),
|
|
537
|
-
});
|
|
538
|
-
new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
|
|
539
|
-
zone: inboundDnsZone,
|
|
540
|
-
recordName: `_amazonses.${inboundDnsRecordName}`,
|
|
541
|
-
ttl: Duration.minutes(5),
|
|
542
|
-
values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
|
|
546
|
-
receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
|
|
547
|
-
});
|
|
548
|
-
inboundRuleSet.addRule("PapyrusInboundSubmissions", {
|
|
549
|
-
recipients: inboundRecipients,
|
|
550
|
-
enabled: true,
|
|
551
|
-
scanEnabled: true,
|
|
552
|
-
tlsPolicy: ses.TlsPolicy.REQUIRE,
|
|
553
|
-
actions: [
|
|
554
|
-
new sesActions.S3({
|
|
555
|
-
bucket: storageBucket,
|
|
556
|
-
objectKeyPrefix: "inbound-email/",
|
|
557
|
-
}),
|
|
558
|
-
],
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
|
|
562
|
-
onCreate: {
|
|
563
|
-
service: "SES",
|
|
564
|
-
action: "setActiveReceiptRuleSet",
|
|
565
|
-
parameters: {
|
|
566
|
-
RuleSetName: inboundRuleSet.receiptRuleSetName,
|
|
567
|
-
},
|
|
568
|
-
physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
|
|
569
|
-
},
|
|
570
|
-
onUpdate: {
|
|
571
|
-
service: "SES",
|
|
572
|
-
action: "setActiveReceiptRuleSet",
|
|
573
|
-
parameters: {
|
|
574
|
-
RuleSetName: inboundRuleSet.receiptRuleSetName,
|
|
575
|
-
},
|
|
576
|
-
physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
|
|
577
|
-
},
|
|
578
|
-
onDelete: {
|
|
579
|
-
service: "SES",
|
|
580
|
-
action: "setActiveReceiptRuleSet",
|
|
581
|
-
parameters: {
|
|
582
|
-
RuleSetName: null,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
policy: AwsCustomResourcePolicy.fromSdkCalls({
|
|
586
|
-
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
|
|
587
|
-
}),
|
|
588
|
-
timeout: Duration.minutes(2),
|
|
589
|
-
});
|
|
500
|
+
// SES allows one active receipt rule set per region, so a non-legacy site
|
|
501
|
+
// creates its own brand-named rule set and never activates it here.
|
|
502
|
+
const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
|
|
503
|
+
if (inboundSesPlan.createReceiptRules) {
|
|
504
|
+
addInboundEmailSesIntake(storageStack, {
|
|
505
|
+
storageBucket,
|
|
506
|
+
recipients: inboundRecipients,
|
|
507
|
+
ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
|
|
508
|
+
existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
|
|
509
|
+
ruleName: deriveReceiptRuleName(siteBackendIdentity),
|
|
510
|
+
activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
|
|
511
|
+
domainIdentity: inboundSesPlan.manageDomainIdentity
|
|
512
|
+
? {
|
|
513
|
+
domain: inboundEmailDomain,
|
|
514
|
+
hostedZoneId: inboundDnsZoneId,
|
|
515
|
+
hostedZoneName: inboundDnsZoneName,
|
|
516
|
+
recordName: inboundDnsRecordName,
|
|
517
|
+
}
|
|
518
|
+
: undefined,
|
|
519
|
+
});
|
|
590
520
|
}
|
|
591
521
|
|
|
592
522
|
new InboundEmailStack(inboundEventStack, "InboundEmail", {
|
|
@@ -11,6 +11,9 @@ export const storage = defineStorage({
|
|
|
11
11
|
"corpora/*": [
|
|
12
12
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
13
13
|
],
|
|
14
|
+
"preview/*": [
|
|
15
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
16
|
+
],
|
|
14
17
|
"newsroom/*": [
|
|
15
18
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
16
19
|
],
|
|
@@ -12,6 +12,9 @@ export const storage = defineStorage({
|
|
|
12
12
|
"corpora/*": [
|
|
13
13
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
14
14
|
],
|
|
15
|
+
"preview/*": [
|
|
16
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
17
|
+
],
|
|
15
18
|
"newsroom/*": [
|
|
16
19
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
17
20
|
],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import * as backup from "aws-cdk-lib/aws-backup";
|
|
3
|
+
import * as events from "aws-cdk-lib/aws-events";
|
|
4
|
+
/**
|
|
5
|
+
* Adds the resources directly to `scope` (no wrapper construct) so the logical
|
|
6
|
+
* ids of the live p.apyr.us backup stack stay unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export function addStorageBackups(scope, props) {
|
|
9
|
+
const vault = new backup.BackupVault(scope, "PapyrusStorageBackupVault", {
|
|
10
|
+
backupVaultName: props.backupVaultName,
|
|
11
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
12
|
+
});
|
|
13
|
+
const plan = new backup.BackupPlan(scope, "PapyrusStorageBackupPlan", {
|
|
14
|
+
backupVault: vault,
|
|
15
|
+
});
|
|
16
|
+
plan.addRule(new backup.BackupPlanRule({
|
|
17
|
+
ruleName: "papyrus-storage-pitr-35d",
|
|
18
|
+
enableContinuousBackup: true,
|
|
19
|
+
deleteAfter: Duration.days(35),
|
|
20
|
+
}));
|
|
21
|
+
plan.addRule(new backup.BackupPlanRule({
|
|
22
|
+
ruleName: "papyrus-storage-daily-365d",
|
|
23
|
+
scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
|
|
24
|
+
deleteAfter: Duration.days(365),
|
|
25
|
+
}));
|
|
26
|
+
plan.addSelection("PapyrusStorageBackupSelection", {
|
|
27
|
+
allowRestores: true,
|
|
28
|
+
resources: [backup.BackupResource.fromArn(props.storageBucket.bucketArn)],
|
|
29
|
+
});
|
|
30
|
+
return { plan, vault };
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import * as backup from "aws-cdk-lib/aws-backup";
|
|
3
|
+
import * as events from "aws-cdk-lib/aws-events";
|
|
4
|
+
import type * as s3 from "aws-cdk-lib/aws-s3";
|
|
5
|
+
import type { Construct } from "constructs";
|
|
6
|
+
|
|
7
|
+
export type StorageBackupsProps = {
|
|
8
|
+
storageBucket: s3.IBucket;
|
|
9
|
+
backupVaultName: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adds the resources directly to `scope` (no wrapper construct) so the logical
|
|
14
|
+
* ids of the live p.apyr.us backup stack stay unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export function addStorageBackups(scope: Construct, props: StorageBackupsProps) {
|
|
17
|
+
const vault = new backup.BackupVault(scope, "PapyrusStorageBackupVault", {
|
|
18
|
+
backupVaultName: props.backupVaultName,
|
|
19
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
20
|
+
});
|
|
21
|
+
const plan = new backup.BackupPlan(scope, "PapyrusStorageBackupPlan", {
|
|
22
|
+
backupVault: vault,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
plan.addRule(
|
|
26
|
+
new backup.BackupPlanRule({
|
|
27
|
+
ruleName: "papyrus-storage-pitr-35d",
|
|
28
|
+
enableContinuousBackup: true,
|
|
29
|
+
deleteAfter: Duration.days(35),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
plan.addRule(
|
|
34
|
+
new backup.BackupPlanRule({
|
|
35
|
+
ruleName: "papyrus-storage-daily-365d",
|
|
36
|
+
scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
|
|
37
|
+
deleteAfter: Duration.days(365),
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
plan.addSelection("PapyrusStorageBackupSelection", {
|
|
42
|
+
allowRestores: true,
|
|
43
|
+
resources: [backup.BackupResource.fromArn(props.storageBucket.bucketArn)],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { plan, vault };
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { streamPreviewObject } from "../../../lib/staging-preview-object";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
7
|
+
return streamPreviewObject(request.nextUrl.pathname);
|
|
8
|
+
}
|
|
@@ -52,10 +52,12 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
|
|
|
52
52
|
editionFooter={{
|
|
53
53
|
editionBasePath,
|
|
54
54
|
entries: buildPresentationFooterEntries(content),
|
|
55
|
+
sections: content.sections.map((section) => ({ key: section.key, label: section.label })),
|
|
55
56
|
subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
|
|
56
57
|
title: SITE_BRAND.footerTitle,
|
|
57
58
|
}}
|
|
58
59
|
item={item}
|
|
60
|
+
videoScript={content.videoScripts?.[item.slug] ?? null}
|
|
59
61
|
backHref={`${editionBasePath}#${item.slug}`}
|
|
60
62
|
/>
|
|
61
63
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { notFound, redirect } from "next/navigation";
|
|
2
|
+
import { siteServesNextReaderRoutes } from "../../../../lib/reader-static-params";
|
|
2
3
|
import { getSiteRenderer } from "../../../../lib/site-renderer";
|
|
3
4
|
import type { PresentationTarget } from "../../../../renderers/pretext";
|
|
4
5
|
import { loadCachedEditionContent } from "../../../../lib/cached-content-repository";
|
|
@@ -22,6 +23,7 @@ export async function EditionRoutePage({
|
|
|
22
23
|
lockedPresentation,
|
|
23
24
|
target = { kind: "edition" },
|
|
24
25
|
}: EditionRoutePageProps) {
|
|
26
|
+
if (!siteServesNextReaderRoutes()) notFound();
|
|
25
27
|
const route = parseEditionDateRoute({ year, month, day });
|
|
26
28
|
if (!route) notFound();
|
|
27
29
|
if (!route.isCanonical) redirect(route.canonicalPath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { buildMediaRouteResponse } from "../../../../lib/media-route-response";
|
|
2
3
|
import { isGuestReadableStoragePath, signStorageUrl } from "../../../../lib/reader-storage-url";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-dynamic";
|
|
@@ -16,20 +17,8 @@ export async function GET(_request: Request, context: MediaRouteContext) {
|
|
|
16
17
|
return NextResponse.json({ error: "Forbidden storage path." }, { status: 403 });
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ error: `Upstream media fetch failed (${upstream.status}).` },
|
|
24
|
-
{ status: upstream.status === 404 ? 404 : 502 },
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const headers = new Headers();
|
|
29
|
-
const contentType = upstream.headers.get("content-type");
|
|
30
|
-
if (contentType) {
|
|
31
|
-
headers.set("content-type", contentType);
|
|
32
|
-
}
|
|
33
|
-
headers.set("cache-control", "public, max-age=3600, stale-while-revalidate=86400");
|
|
34
|
-
return new NextResponse(upstream.body, { status: 200, headers });
|
|
20
|
+
return buildMediaRouteResponse(storagePath, {
|
|
21
|
+
signStorageUrl,
|
|
22
|
+
fetchUpstream: (signedUrl) => fetch(signedUrl, { cache: "no-store" }),
|
|
23
|
+
});
|
|
35
24
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toPublicReaderPath } from "../../../lib/reader-base-path";
|
|
1
2
|
import { revalidatePath, revalidateTag } from "next/cache";
|
|
2
3
|
import { NextResponse } from "next/server";
|
|
3
4
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
editionContentCacheTag,
|
|
9
10
|
editionItemCacheTag,
|
|
10
11
|
} from "../../../lib/cached-content-repository";
|
|
12
|
+
import { isAuthorizedRevalidateRequest } from "../../../lib/revalidate-secret";
|
|
11
13
|
import { getEditionArticlePath, getEditionDatePath } from "../../../lib/edition-routes";
|
|
12
14
|
|
|
13
15
|
export const dynamic = "force-dynamic";
|
|
@@ -21,9 +23,8 @@ type RevalidateRequestBody = {
|
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export async function POST(request: Request) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
if (!configuredSecret || providedSecret !== configuredSecret) {
|
|
26
|
+
const providedSecret = request.headers.get("x-papyrus-revalidate-secret");
|
|
27
|
+
if (!(await isAuthorizedRevalidateRequest(providedSecret))) {
|
|
27
28
|
return NextResponse.json({ ok: false, error: "Unauthorized." }, { status: 401 });
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -44,8 +45,8 @@ export async function POST(request: Request) {
|
|
|
44
45
|
if (!normalized) continue;
|
|
45
46
|
revalidateTag(articleCacheTag(normalized));
|
|
46
47
|
revalidatedTags.add(articleCacheTag(normalized));
|
|
47
|
-
revalidatePath(`/articles/${encodeURIComponent(normalized)}`);
|
|
48
|
-
revalidatedPaths.add(`/articles/${encodeURIComponent(normalized)}`);
|
|
48
|
+
revalidatePath(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
|
|
49
|
+
revalidatedPaths.add(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
|
|
49
50
|
if (editionDate) {
|
|
50
51
|
revalidateTag(editionItemCacheTag(editionDate, normalized));
|
|
51
52
|
revalidatedTags.add(editionItemCacheTag(editionDate, normalized));
|
|
@@ -76,8 +77,8 @@ export async function POST(request: Request) {
|
|
|
76
77
|
revalidatedTags.add(ARCHIVE_CACHE_TAG);
|
|
77
78
|
revalidateTag(ARTICLES_CACHE_TAG);
|
|
78
79
|
revalidatedTags.add(ARTICLES_CACHE_TAG);
|
|
79
|
-
revalidatePath("/archive");
|
|
80
|
-
revalidatedPaths.add("/archive");
|
|
80
|
+
revalidatePath(toPublicReaderPath("/archive"));
|
|
81
|
+
revalidatedPaths.add(toPublicReaderPath("/archive"));
|
|
81
82
|
|
|
82
83
|
for (const path of body.paths ?? []) {
|
|
83
84
|
const normalized = path.trim();
|