@anthusai/papyrus 1.0.0-next.43 → 1.0.0-next.45

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.
@@ -22,6 +22,80 @@ export type PapyrusAuthConfig = {
22
22
  applyCognitoDomainPrefix?: boolean;
23
23
  };
24
24
 
25
+ export type S3ProjectionDistributionConfig = {
26
+ create: boolean;
27
+ aliases?: string[];
28
+ certificateArn?: string;
29
+ acm?: { hostedZoneId: string; hostedZoneName: string };
30
+ };
31
+
32
+ export type PapyrusPublicationConfig = {
33
+ kind: "s3-projection";
34
+ /** Site repo path (relative, `.py`) of the SiteProjector bundled into content-actions as `site_projector`. */
35
+ projector?: string;
36
+ /** Site repo directory (relative) bundled into content-actions as `site_static/`. */
37
+ staticDir?: string;
38
+ /** Existing CloudFront distribution that reads the site bucket through OAC. */
39
+ edgeDistributionArn?: string;
40
+ /** Create the distribution (and optionally its certificate) in the site backend. Default: not created. */
41
+ distribution?: S3ProjectionDistributionConfig;
42
+ /** Default true: the site bucket is retained when the stack is deleted. `false` destroys it and its objects. */
43
+ retainBucketOnDelete?: boolean;
44
+ };
45
+
46
+ const CLOUDFRONT_DISTRIBUTION_ARN = /^arn:aws[a-z-]*:cloudfront::\d{12}:distribution\/[A-Z0-9]+$/;
47
+
48
+ function isRelativePathWithoutParentSegments(value: string): boolean {
49
+ if (value === "" || value.startsWith("/") || value.includes("\\")) return false;
50
+ return !value.split("/").includes("..");
51
+ }
52
+
53
+ export function validatePublicationConfig(publication: unknown): string[] {
54
+ const problems: string[] = [];
55
+ if (typeof publication !== "object" || publication === null) {
56
+ return ["publication must be an object"];
57
+ }
58
+ const config = publication as Partial<PapyrusPublicationConfig> & { kind?: unknown };
59
+ if (config.kind !== "s3-projection") {
60
+ problems.push(`publication.kind must be "s3-projection" (got ${JSON.stringify(config.kind)})`);
61
+ }
62
+ if (config.projector !== undefined) {
63
+ if (typeof config.projector !== "string" || !isRelativePathWithoutParentSegments(config.projector) || !config.projector.endsWith(".py")) {
64
+ problems.push("publication.projector must be a relative .py path without '..'");
65
+ }
66
+ }
67
+ if (config.staticDir !== undefined) {
68
+ if (typeof config.staticDir !== "string" || !isRelativePathWithoutParentSegments(config.staticDir)) {
69
+ problems.push("publication.staticDir must be a relative path without '..'");
70
+ }
71
+ }
72
+ if (config.edgeDistributionArn !== undefined && !CLOUDFRONT_DISTRIBUTION_ARN.test(config.edgeDistributionArn)) {
73
+ problems.push("publication.edgeDistributionArn must be a CloudFront distribution ARN (arn:aws:cloudfront::<account>:distribution/<id>)");
74
+ }
75
+ const distribution = config.distribution;
76
+ if (distribution !== undefined) {
77
+ if (config.edgeDistributionArn !== undefined && distribution.create === true) {
78
+ problems.push("publication.edgeDistributionArn and publication.distribution.create are mutually exclusive");
79
+ }
80
+ const aliases = distribution.aliases ?? [];
81
+ if (aliases.length > 0 && distribution.create !== true) {
82
+ problems.push("publication.distribution.aliases require distribution.create to be true");
83
+ }
84
+ if (aliases.length > 0 && !distribution.certificateArn && !distribution.acm) {
85
+ problems.push("publication.distribution.aliases require certificateArn or acm");
86
+ }
87
+ if (distribution.acm !== undefined && (!distribution.acm.hostedZoneId || !distribution.acm.hostedZoneName)) {
88
+ problems.push("publication.distribution.acm requires hostedZoneId and hostedZoneName");
89
+ }
90
+ }
91
+ return problems;
92
+ }
93
+
94
+ export function assertValidPublicationConfig(publication: unknown): void {
95
+ const problems = validatePublicationConfig(publication);
96
+ if (problems.length > 0) throw new Error(`Invalid backend.publication: ${problems.join("; ")}`);
97
+ }
98
+
25
99
  export type PapyrusSiteBackendConfig = {
26
100
  /** Brand id; namespaces account-global resources (S3 Vectors index name). */
27
101
  brandId?: string;
@@ -56,6 +130,8 @@ export type PapyrusSiteBackendConfig = {
56
130
  /** Let this stack verify the SES domain identity and write its TXT record. Default false: verify once by hand. */
57
131
  manageDomainIdentity?: boolean;
58
132
  };
133
+ /** Opt-in static projection of published pages into an S3 site bucket. Unset: nothing is created. */
134
+ publication?: PapyrusPublicationConfig;
59
135
  features?: {
60
136
  consoleResponder?: boolean;
61
137
  inboundEmail?: boolean;
@@ -7,14 +7,18 @@ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aw
7
7
  import { dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves.js";
10
+ import { addS3ProjectionPublication } from "./publication/s3-projection.js";
11
+ import { planRenditions } from "./publication/renditions-settings.js";
12
+ import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
10
13
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
14
+ import { assertValidPublicationConfig } from "./site-backend-config.js";
11
15
  import { revalidateSecretReadStatement } from "./revalidate-secret-policy.js";
12
16
  import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
13
17
  import { data } from "./data/resource.js";
14
18
  import { assignmentAction } from "./functions/assignment-action/resource.js";
15
19
  import { categoryAction } from "./functions/category-action/resource.js";
16
20
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
17
- import { contentActions } from "./functions/content-actions/resource.js";
21
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource.js";
18
22
  import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
19
23
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
20
24
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
@@ -62,6 +66,11 @@ export function defineSiteBackend(papyrusSite) {
62
66
  ...baseAuthConfig,
63
67
  newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
64
68
  };
69
+ if (site.publication !== undefined)
70
+ assertValidPublicationConfig(site.publication);
71
+ const publicationPlan = planS3ProjectionBackend(site.publication);
72
+ const renditionsPlan = planRenditions(site.publication, process.cwd());
73
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
65
74
  const backend = defineBackend({
66
75
  assignmentAction,
67
76
  auth: defineSiteAuth(authConfig),
@@ -217,8 +226,8 @@ export function defineSiteBackend(papyrusSite) {
217
226
  let storageBackupPlan;
218
227
  let storageBackupVault;
219
228
  const storageBucketCfn = storageBucket.node.defaultChild;
220
- if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
221
- // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
229
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
230
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
222
231
  storageBucketCfn.addPropertyOverride("NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled", true);
223
232
  }
224
233
  if (enableStorageBackups) {
@@ -302,6 +311,21 @@ export function defineSiteBackend(papyrusSite) {
302
311
  if (rebuildSettings.resources.length > 0) {
303
312
  contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
304
313
  }
314
+ let s3ProjectionPublication;
315
+ if (site.publication !== undefined) {
316
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), site.publication, {
317
+ mediaBucket: storageBucket,
318
+ renditions: renditionsPlan,
319
+ });
320
+ contentActionsLambda.node.defaultChild.memorySize = publicationPlan.contentActionsMemorySize;
321
+ }
322
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
323
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
324
+ contentActionsLambda.addEnvironment(name, value);
325
+ }
326
+ for (const statement of publicationWiring.policyStatements) {
327
+ contentActionsLambda.addToRolePolicy(statement);
328
+ }
305
329
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
306
330
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
307
331
  const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
@@ -3,21 +3,25 @@ import { Stack } from "aws-cdk-lib";
3
3
  import type * as backup from "aws-cdk-lib/aws-backup";
4
4
  import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
5
5
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
6
- import { CfnEventSourceMapping, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
6
+ import { CfnEventSourceMapping, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
7
7
  import * as s3 from "aws-cdk-lib/aws-s3";
8
8
  import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
9
9
  import type { Construct } from "constructs";
10
10
  import { dirname, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves";
13
+ import { addS3ProjectionPublication, type S3ProjectionPublication } from "./publication/s3-projection";
14
+ import { planRenditions } from "./publication/renditions-settings";
15
+ import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
13
16
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
17
+ import { assertValidPublicationConfig } from "./site-backend-config";
14
18
  import { revalidateSecretReadStatement } from "./revalidate-secret-policy";
15
19
  import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource";
16
20
  import { data } from "./data/resource";
17
21
  import { assignmentAction } from "./functions/assignment-action/resource";
18
22
  import { categoryAction } from "./functions/category-action/resource";
19
23
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
20
- import { contentActions } from "./functions/content-actions/resource";
24
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource";
21
25
  import { knowledgeQuery } from "./functions/knowledge-query/resource";
22
26
  import { manageUserRole } from "./functions/manage-user-role/resource";
23
27
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
@@ -80,6 +84,10 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
80
84
  ...baseAuthConfig,
81
85
  newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
82
86
  };
87
+ if (site.publication !== undefined) assertValidPublicationConfig(site.publication);
88
+ const publicationPlan = planS3ProjectionBackend(site.publication);
89
+ const renditionsPlan = planRenditions(site.publication, process.cwd());
90
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
83
91
  const backend = defineBackend({
84
92
  assignmentAction,
85
93
  auth: defineSiteAuth(authConfig),
@@ -279,8 +287,8 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
279
287
  let storageBackupVault: backup.BackupVault | undefined;
280
288
 
281
289
  const storageBucketCfn = storageBucket.node.defaultChild as s3.CfnBucket | undefined;
282
- if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
283
- // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
290
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
291
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
284
292
  storageBucketCfn.addPropertyOverride(
285
293
  "NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled",
286
294
  true,
@@ -388,6 +396,21 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
388
396
  new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
389
397
  );
390
398
  }
399
+ let s3ProjectionPublication: S3ProjectionPublication | undefined;
400
+ if (site.publication !== undefined) {
401
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), site.publication, {
402
+ mediaBucket: storageBucket,
403
+ renditions: renditionsPlan,
404
+ });
405
+ (contentActionsLambda.node.defaultChild as CfnFunction).memorySize = publicationPlan.contentActionsMemorySize;
406
+ }
407
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
408
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
409
+ contentActionsLambda.addEnvironment(name, value);
410
+ }
411
+ for (const statement of publicationWiring.policyStatements) {
412
+ contentActionsLambda.addToRolePolicy(statement);
413
+ }
391
414
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
392
415
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
393
416
  const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
package/package.json CHANGED
@@ -302,7 +302,7 @@
302
302
  "optional": true
303
303
  }
304
304
  },
305
- "version": "1.0.0-next.43",
305
+ "version": "1.0.0-next.45",
306
306
  "dependencies": {
307
307
  "@aws-amplify/adapter-nextjs": "^1.7.3",
308
308
  "@aws-sdk/client-cognito-identity-provider": "3.1045.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.43",
2
+ "version": "1.0.0-next.45",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",