@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.
Files changed (127) hide show
  1. package/amplify/auth/resource.js +17 -4
  2. package/amplify/auth/resource.ts +21 -4
  3. package/amplify/data/resource.js +26 -70
  4. package/amplify/data/resource.ts +27 -70
  5. package/amplify/data/schema.js +26 -58
  6. package/amplify/data/schema.ts +27 -58
  7. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  8. package/amplify/functions/console-chat-responder/resource.js +0 -7
  9. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  10. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  11. package/amplify/functions/content-actions/handler.py +45 -3
  12. package/amplify/rebuild-trigger-settings.js +22 -0
  13. package/amplify/rebuild-trigger-settings.ts +42 -0
  14. package/amplify/site-backend-config.ts +12 -1
  15. package/amplify/site-backend.js +41 -27
  16. package/amplify/site-backend.ts +51 -34
  17. package/amplify/storage/resource.js +3 -0
  18. package/amplify/storage/resource.ts +3 -0
  19. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  20. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
  21. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  22. package/app/api/media/[...path]/route.ts +5 -16
  23. package/app/api/revalidate/route.ts +8 -7
  24. package/app/articles/[slug]/page.tsx +6 -4
  25. package/app/blog-rhythm.css +278 -0
  26. package/app/globals.css +21 -0
  27. package/app/layout.tsx +20 -0
  28. package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
  29. package/app/newsroom/[section]/page.tsx +4 -3
  30. package/app/newsroom/articles/[id]/page.tsx +15 -0
  31. package/app/newsroom/articles/page.tsx +13 -0
  32. package/app/newsroom/layout.tsx +2 -9
  33. package/app/page.tsx +18 -5
  34. package/app/robots.ts +11 -0
  35. package/bin/papyrus-app.mjs +12 -2
  36. package/bin/papyrus-infra.mjs +74 -18
  37. package/components/archive-shell.tsx +2 -22
  38. package/components/article-video.tsx +247 -3
  39. package/components/blog-page-background.tsx +23 -6
  40. package/components/markus-ir-preview.tsx +300 -0
  41. package/components/newspaper.tsx +6 -2
  42. package/components/newsroom-access-gate.tsx +67 -0
  43. package/components/newsroom-article-editor.tsx +741 -0
  44. package/components/newsroom-articles-view.tsx +129 -0
  45. package/components/newsroom-client-shell.tsx +12 -0
  46. package/components/newsroom-ops-overview.tsx +28 -11
  47. package/components/newsroom-ops-shell.tsx +3 -1
  48. package/components/papyrus-console-shell.tsx +4 -2
  49. package/components/pictogram-figure.tsx +9 -2
  50. package/components/presentation-header.tsx +135 -0
  51. package/components/presentation-rhythm-hrule.tsx +17 -0
  52. package/components/settings-page.tsx +2 -1
  53. package/components/staging-banner.tsx +11 -0
  54. package/components/topic-steering-workspace.tsx +6 -63
  55. package/components/use-blog-header-obstacles.ts +81 -0
  56. package/components/use-brand-fonts-ready.ts +28 -0
  57. package/components/use-rhythm-overlay.ts +29 -0
  58. package/infra/amplify-app-shell.js +285 -80
  59. package/infra/amplify-app-shell.ts +326 -113
  60. package/infra/build-specs.js +95 -0
  61. package/infra/build-specs.ts +142 -0
  62. package/infra/github-oidc-provider.js +24 -0
  63. package/infra/github-oidc-provider.ts +30 -0
  64. package/infra/index.js +3 -0
  65. package/infra/site-config.js +273 -0
  66. package/infra/site-config.ts +336 -0
  67. package/lib/amplify-outputs-path.ts +5 -1
  68. package/lib/blog-feature-solver.ts +459 -0
  69. package/lib/blog-rhythm.ts +134 -0
  70. package/lib/brand-fonts-watch.ts +43 -0
  71. package/lib/brand-fonts.ts +112 -0
  72. package/lib/cached-content-repository.ts +14 -1
  73. package/lib/category-repository.ts +1 -93
  74. package/lib/content-actions-demo.ts +190 -0
  75. package/lib/content-repository.ts +6 -0
  76. package/lib/content-source-context.ts +26 -0
  77. package/lib/content-types.ts +4 -0
  78. package/lib/edition-routes.ts +31 -29
  79. package/lib/graphql-content-repository.ts +239 -43
  80. package/lib/media-route-response.ts +45 -0
  81. package/lib/newspaper-layout.ts +3 -2
  82. package/lib/newsroom-articles.ts +325 -0
  83. package/lib/newsroom-base-path.ts +12 -3
  84. package/lib/newsroom-category-drilldown.ts +3 -2
  85. package/lib/newsroom-demo-articles.ts +90 -0
  86. package/lib/newsroom-forum-routes.ts +6 -4
  87. package/lib/newsroom-index-filters.ts +14 -10
  88. package/lib/newsroom-media-backend.ts +142 -0
  89. package/lib/newsroom-media.ts +182 -0
  90. package/lib/newsroom-nav.ts +3 -0
  91. package/lib/papyrus-web-locations.ts +7 -6
  92. package/lib/presentation-footer.ts +3 -2
  93. package/lib/pretext-layout.ts +12 -3
  94. package/lib/reader-base-path.ts +24 -0
  95. package/lib/reader-static-params.ts +8 -0
  96. package/lib/renderer.ts +7 -0
  97. package/lib/revalidate-secret.ts +59 -0
  98. package/lib/root-route-redirect.ts +16 -0
  99. package/lib/semantic-graph.ts +5 -0
  100. package/lib/site-brand.ts +51 -0
  101. package/lib/site-env.ts +52 -0
  102. package/lib/staging-gate.ts +37 -0
  103. package/lib/staging-gated-path.ts +23 -0
  104. package/lib/staging-preview-object.ts +114 -0
  105. package/lib/video-mode.ts +42 -0
  106. package/lib/video-script.ts +77 -0
  107. package/middleware.ts +52 -10
  108. package/package.json +41 -1
  109. package/renderers/pretext/article-page.tsx +39 -3
  110. package/renderers/pretext/presentation-shared.tsx +113 -0
  111. package/renderers/pretext/presentation-shell.tsx +15 -102
  112. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  113. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  114. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  115. package/routes.manifest.json +37 -2
  116. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  117. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  118. package/scripts/videoml/bundle-common.mjs +129 -0
  119. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  120. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  121. package/scripts/videoml/preview/preview-timing.ts +42 -0
  122. package/scripts/videoml/preview.html +77 -0
  123. package/scripts/videoml/standard-entry.tsx +46 -0
  124. package/src/sync.mjs +30 -6
  125. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  126. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  127. 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
- /** Stable Cognito hosted-UI domain prefix. */
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;
@@ -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 { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
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(site.auth ?? authConfigFromEnv()),
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 jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild;
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
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
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 (jwtSsmEnvConfig) {
183
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
184
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
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) {
@@ -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, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
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 { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
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(site.auth ?? authConfigFromEnv()),
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 jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild as CfnFunction | undefined;
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
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
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 (jwtSsmEnvConfig) {
220
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
221
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
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
- const signedUrl = await signStorageUrl(storagePath);
20
- const upstream = await fetch(signedUrl, { cache: "no-store" });
21
- if (!upstream.ok) {
22
- return NextResponse.json(
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 configuredSecret = process.env.PAPYRUS_REVALIDATE_SECRET?.trim();
25
- const providedSecret = request.headers.get("x-papyrus-revalidate-secret")?.trim();
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
  }