@anthusai/papyrus 1.0.0-next.3 → 1.0.0-next.30
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/resource.js +17 -4
- package/amplify/auth/resource.ts +21 -4
- package/amplify/data/resource.js +26 -70
- package/amplify/data/resource.ts +27 -70
- package/amplify/data/schema.js +26 -58
- package/amplify/data/schema.ts +27 -58
- 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/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +12 -1
- package/amplify/site-backend.js +41 -27
- package/amplify/site-backend.ts +51 -34
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -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 +20 -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 +2 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +11 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +74 -18
- 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/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/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/settings-page.tsx +2 -1
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +6 -63
- 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 +285 -80
- package/infra/amplify-app-shell.ts +326 -113
- package/infra/build-specs.js +95 -0
- package/infra/build-specs.ts +142 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +3 -0
- package/infra/site-config.js +273 -0
- package/infra/site-config.ts +336 -0
- 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 +14 -1
- package/lib/category-repository.ts +1 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +6 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +239 -43
- 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/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 +8 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +5 -0
- package/lib/site-brand.ts +51 -0
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -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 +52 -10
- package/package.json +41 -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 +37 -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/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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment and IAM policy for the content-actions Lambda's site triggers.
|
|
3
|
+
* Pure (no CDK imports) so the policy can be tested without synthesizing.
|
|
4
|
+
*/
|
|
5
|
+
export type RebuildTriggerInput = {
|
|
6
|
+
reader?: { amplifyAppId: string; branchName?: string };
|
|
7
|
+
stagingBuildEnabled: boolean;
|
|
8
|
+
cmsAppId: string;
|
|
9
|
+
region: string;
|
|
10
|
+
account: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RebuildTriggerSettings = {
|
|
14
|
+
environment: Record<string, string>;
|
|
15
|
+
actions: string[];
|
|
16
|
+
resources: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
|
|
20
|
+
|
|
21
|
+
function branchJobsArn(input: RebuildTriggerInput, appId: string, branch: string): string {
|
|
22
|
+
return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rebuildTriggerSettings(input: RebuildTriggerInput): RebuildTriggerSettings {
|
|
26
|
+
const environment: Record<string, string> = {};
|
|
27
|
+
const resources: string[] = [];
|
|
28
|
+
const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
|
|
29
|
+
if (readerAppId !== "") {
|
|
30
|
+
const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
|
|
31
|
+
environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
|
|
32
|
+
environment.PAPYRUS_READER_BRANCH = readerBranch;
|
|
33
|
+
resources.push(branchJobsArn(input, readerAppId, readerBranch));
|
|
34
|
+
}
|
|
35
|
+
const cmsAppId = input.cmsAppId.trim();
|
|
36
|
+
if (input.stagingBuildEnabled && cmsAppId !== "") {
|
|
37
|
+
environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
|
|
38
|
+
environment.PAPYRUS_STAGING_BRANCH = "staging";
|
|
39
|
+
resources.push(branchJobsArn(input, cmsAppId, "staging"));
|
|
40
|
+
}
|
|
41
|
+
return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
|
|
42
|
+
}
|
|
@@ -7,8 +7,13 @@ export type PapyrusAuthConfig = {
|
|
|
7
7
|
/** OAuth callback/logout URLs for Google sign-in (ignored when Google is disabled). */
|
|
8
8
|
redirectUrls?: string[];
|
|
9
9
|
disableGoogleOAuth?: boolean;
|
|
10
|
-
/**
|
|
10
|
+
/** Desired Cognito hosted-UI domain prefix. Only applied when `applyCognitoDomainPrefix` is true. */
|
|
11
11
|
cognitoDomainPrefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Opt in to the prefix. Off by default because changing a live pool's
|
|
14
|
+
* domain replaces it and changes the Google redirect URI.
|
|
15
|
+
*/
|
|
16
|
+
applyCognitoDomainPrefix?: boolean;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
export type PapyrusSiteBackendConfig = {
|
|
@@ -21,6 +26,12 @@ export type PapyrusSiteBackendConfig = {
|
|
|
21
26
|
*/
|
|
22
27
|
productionAppId?: string;
|
|
23
28
|
auth?: PapyrusAuthConfig;
|
|
29
|
+
/** Static reader app (Markus-static sites) that a publish rebuilds through IAM `amplify:StartJob`. */
|
|
30
|
+
reader?: { amplifyAppId: string; branchName?: string };
|
|
31
|
+
/** Lets the newsroom start the `staging` branch build of this CMS app. */
|
|
32
|
+
stagingBuild?: { enabled: boolean };
|
|
33
|
+
/** Base URL of a Pretext reader whose `/api/revalidate` a publish calls (authenticated by the SSM secret named in branch variable `PAPYRUS_REVALIDATE_SECRET_PARAMETER`). */
|
|
34
|
+
revalidateBaseUrl?: string;
|
|
24
35
|
features?: {
|
|
25
36
|
consoleResponder?: boolean;
|
|
26
37
|
inboundEmail?: boolean;
|
package/amplify/site-backend.js
CHANGED
|
@@ -12,12 +12,12 @@ import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lamb
|
|
|
12
12
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import {
|
|
15
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
16
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
16
17
|
import { data } from "./data/resource.js";
|
|
17
18
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
18
19
|
import { categoryAction } from "./functions/category-action/resource.js";
|
|
19
20
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
|
|
20
|
-
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
|
|
21
21
|
import { contentActions } from "./functions/content-actions/resource.js";
|
|
22
22
|
import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
|
|
23
23
|
import { manageUserRole } from "./functions/manage-user-role/resource.js";
|
|
@@ -76,13 +76,13 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
76
76
|
const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
|
|
77
77
|
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
78
78
|
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
79
|
+
const authConfig = site.auth ?? authConfigFromEnv();
|
|
79
80
|
const backend = defineBackend({
|
|
80
81
|
assignmentAction,
|
|
81
|
-
auth: defineSiteAuth(
|
|
82
|
+
auth: defineSiteAuth(authConfig),
|
|
82
83
|
categoryAction,
|
|
83
84
|
contentActions,
|
|
84
85
|
data,
|
|
85
|
-
graphqlJwtAuthorizer,
|
|
86
86
|
knowledgeQuery,
|
|
87
87
|
manageUserRole,
|
|
88
88
|
modelAttachmentUpload,
|
|
@@ -95,6 +95,12 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
95
95
|
slackDelivery,
|
|
96
96
|
storage,
|
|
97
97
|
});
|
|
98
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
99
|
+
if (authConfig.applyCognitoDomainPrefix === true
|
|
100
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
101
|
+
&& authConfig.disableGoogleOAuth !== true) {
|
|
102
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
103
|
+
}
|
|
98
104
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
99
105
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
100
106
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
|
@@ -138,36 +144,18 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
138
144
|
const messageThreadTable = backend.data.resources.tables.MessageThread;
|
|
139
145
|
const dataStack = Stack.of(messageTable);
|
|
140
146
|
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
|
-
|| "";
|
|
147
|
+
const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
|
|
151
148
|
if (enableConsoleResponder) {
|
|
152
|
-
|
|
149
|
+
new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
|
|
153
150
|
messageTable,
|
|
154
151
|
messageStreamArn,
|
|
155
152
|
threadTable: messageThreadTable,
|
|
156
153
|
projectRoot,
|
|
157
154
|
graphqlEndpoint,
|
|
158
|
-
amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
|
|
159
155
|
responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
|
|
160
156
|
model: process.env.PAPYRUS_CONSOLE_MODEL,
|
|
161
157
|
prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
|
|
162
158
|
});
|
|
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
159
|
}
|
|
172
160
|
if (enableSlackAgent) {
|
|
173
161
|
const slackEventsLambda = backend.slackEvents.resources.lambda;
|
|
@@ -179,9 +167,9 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
179
167
|
backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
180
168
|
backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
|
|
181
169
|
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",
|
|
170
|
+
if (amplifySsmEnvConfig) {
|
|
171
|
+
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
172
|
+
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
185
173
|
}
|
|
186
174
|
backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
187
175
|
backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
|
|
@@ -333,6 +321,32 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
333
321
|
actions: ["appsync:GraphQL"],
|
|
334
322
|
resources: ["*"],
|
|
335
323
|
}));
|
|
324
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
325
|
+
reader: site.reader,
|
|
326
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
327
|
+
cmsAppId: amplifyAppId,
|
|
328
|
+
region: Stack.of(contentActionsLambda).region,
|
|
329
|
+
account: Stack.of(contentActionsLambda).account,
|
|
330
|
+
});
|
|
331
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
332
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
333
|
+
}
|
|
334
|
+
if (rebuildSettings.resources.length > 0) {
|
|
335
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
|
|
336
|
+
}
|
|
337
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
338
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
339
|
+
const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
|
|
340
|
+
if (!revalidateSecretParameter.startsWith("/")) {
|
|
341
|
+
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.");
|
|
342
|
+
}
|
|
343
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
|
|
344
|
+
const revalidateStack = Stack.of(contentActionsLambda);
|
|
345
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({
|
|
346
|
+
actions: ["ssm:GetParameter"],
|
|
347
|
+
resources: [`arn:${revalidateStack.partition}:ssm:${revalidateStack.region}:${revalidateStack.account}:parameter${revalidateSecretParameter}`],
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
336
350
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
337
351
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
338
352
|
if (backend.sesInboundReceive) {
|
package/amplify/site-backend.ts
CHANGED
|
@@ -8,17 +8,17 @@ import * as route53 from "aws-cdk-lib/aws-route53";
|
|
|
8
8
|
import * as ses from "aws-cdk-lib/aws-ses";
|
|
9
9
|
import * as sesActions from "aws-cdk-lib/aws-ses-actions";
|
|
10
10
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
11
|
-
import { CfnEventSourceMapping,
|
|
11
|
+
import { CfnEventSourceMapping, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
|
|
12
12
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
13
13
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
14
14
|
import { dirname, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
16
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
17
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
17
18
|
import { data } from "./data/resource";
|
|
18
19
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
19
20
|
import { categoryAction } from "./functions/category-action/resource";
|
|
20
21
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
|
|
21
|
-
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
|
|
22
22
|
import { contentActions } from "./functions/content-actions/resource";
|
|
23
23
|
import { knowledgeQuery } from "./functions/knowledge-query/resource";
|
|
24
24
|
import { manageUserRole } from "./functions/manage-user-role/resource";
|
|
@@ -87,13 +87,13 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
87
87
|
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
88
88
|
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
89
89
|
|
|
90
|
+
const authConfig = site.auth ?? authConfigFromEnv();
|
|
90
91
|
const backend = defineBackend({
|
|
91
92
|
assignmentAction,
|
|
92
|
-
auth: defineSiteAuth(
|
|
93
|
+
auth: defineSiteAuth(authConfig),
|
|
93
94
|
categoryAction,
|
|
94
95
|
contentActions,
|
|
95
96
|
data,
|
|
96
|
-
graphqlJwtAuthorizer,
|
|
97
97
|
knowledgeQuery,
|
|
98
98
|
manageUserRole,
|
|
99
99
|
modelAttachmentUpload,
|
|
@@ -107,6 +107,15 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
107
107
|
storage,
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
111
|
+
if (
|
|
112
|
+
authConfig.applyCognitoDomainPrefix === true
|
|
113
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
114
|
+
&& authConfig.disableGoogleOAuth !== true
|
|
115
|
+
) {
|
|
116
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
111
120
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
112
121
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
|
@@ -159,43 +168,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
159
168
|
const messageThreadTable = backend.data.resources.tables.MessageThread;
|
|
160
169
|
const dataStack = Stack.of(messageTable);
|
|
161
170
|
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
|
-
|| "";
|
|
171
|
+
const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
|
|
174
172
|
|
|
175
173
|
if (enableConsoleResponder) {
|
|
176
|
-
|
|
174
|
+
new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
|
|
177
175
|
messageTable,
|
|
178
176
|
messageStreamArn,
|
|
179
177
|
threadTable: messageThreadTable,
|
|
180
178
|
projectRoot,
|
|
181
179
|
graphqlEndpoint,
|
|
182
|
-
amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
|
|
183
180
|
responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
|
|
184
181
|
model: process.env.PAPYRUS_CONSOLE_MODEL,
|
|
185
182
|
prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
|
|
186
183
|
});
|
|
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
184
|
}
|
|
200
185
|
|
|
201
186
|
if (enableSlackAgent) {
|
|
@@ -216,9 +201,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
216
201
|
"PAPYRUS_SLACK_ALLOWED_USER_IDS",
|
|
217
202
|
(process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
|
|
218
203
|
);
|
|
219
|
-
if (
|
|
220
|
-
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
221
|
-
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
204
|
+
if (amplifySsmEnvConfig) {
|
|
205
|
+
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
206
|
+
backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
|
|
222
207
|
}
|
|
223
208
|
backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
|
|
224
209
|
backend.slackDelivery.addEnvironment(
|
|
@@ -414,6 +399,38 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
414
399
|
resources: ["*"],
|
|
415
400
|
}),
|
|
416
401
|
);
|
|
402
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
403
|
+
reader: site.reader,
|
|
404
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
405
|
+
cmsAppId: amplifyAppId,
|
|
406
|
+
region: Stack.of(contentActionsLambda).region,
|
|
407
|
+
account: Stack.of(contentActionsLambda).account,
|
|
408
|
+
});
|
|
409
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
410
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
411
|
+
}
|
|
412
|
+
if (rebuildSettings.resources.length > 0) {
|
|
413
|
+
contentActionsLambda.addToRolePolicy(
|
|
414
|
+
new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
418
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
419
|
+
const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
|
|
420
|
+
if (!revalidateSecretParameter.startsWith("/")) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
"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.",
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
|
|
426
|
+
const revalidateStack = Stack.of(contentActionsLambda);
|
|
427
|
+
contentActionsLambda.addToRolePolicy(
|
|
428
|
+
new PolicyStatement({
|
|
429
|
+
actions: ["ssm:GetParameter"],
|
|
430
|
+
resources: [`arn:${revalidateStack.partition}:ssm:${revalidateStack.region}:${revalidateStack.account}:parameter${revalidateSecretParameter}`],
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
417
434
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
418
435
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
419
436
|
if (backend.sesInboundReceive) {
|
|
@@ -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,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();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { notFound } from "next/navigation";
|
|
2
2
|
import { getSiteRenderer } from "../../../lib/site-renderer";
|
|
3
|
-
import { getCachedArticle } from "../../../lib/cached-content-repository";
|
|
4
|
-
import { generateArticleStaticParams } from "../../../lib/reader-static-params";
|
|
3
|
+
import { getCachedArticle, getCachedVideoScript } from "../../../lib/cached-content-repository";
|
|
4
|
+
import { generateArticleStaticParams, siteServesNextReaderRoutes } from "../../../lib/reader-static-params";
|
|
5
5
|
import { SITE_BRAND } from "../../../lib/site-brand";
|
|
6
6
|
|
|
7
7
|
// Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
|
|
@@ -16,6 +16,7 @@ type ArticlePageProps = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export async function generateMetadata({ params }: ArticlePageProps) {
|
|
19
|
+
if (!siteServesNextReaderRoutes()) return {};
|
|
19
20
|
const { slug } = await params;
|
|
20
21
|
const article = await getCachedArticle(slug);
|
|
21
22
|
if (!article) return {};
|
|
@@ -26,10 +27,11 @@ export async function generateMetadata({ params }: ArticlePageProps) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export default async function ArticlePage({ params }: ArticlePageProps) {
|
|
30
|
+
if (!siteServesNextReaderRoutes()) notFound();
|
|
29
31
|
const { slug } = await params;
|
|
30
|
-
const article = await getCachedArticle(slug);
|
|
32
|
+
const [article, videoScript] = await Promise.all([getCachedArticle(slug), getCachedVideoScript(slug)]);
|
|
31
33
|
if (!article) notFound();
|
|
32
34
|
|
|
33
35
|
const siteRenderer = getSiteRenderer();
|
|
34
|
-
return <siteRenderer.renderArticle article={article} backHref="/" />;
|
|
36
|
+
return <siteRenderer.renderArticle article={article} videoScript={videoScript} backHref="/" />;
|
|
35
37
|
}
|