@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.21

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 (140) hide show
  1. package/amplify/auth/resource.js +17 -4
  2. package/amplify/auth/resource.ts +21 -4
  3. package/amplify/data/resource.js +52 -70
  4. package/amplify/data/resource.ts +57 -70
  5. package/amplify/data/schema.js +52 -58
  6. package/amplify/data/schema.ts +57 -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 +149 -0
  12. package/amplify/functions/content-actions/requirements.txt +4 -0
  13. package/amplify/functions/content-actions/resource.js +40 -0
  14. package/amplify/functions/content-actions/resource.ts +54 -0
  15. package/amplify/functions/shared/python-bundle.js +17 -2
  16. package/amplify/functions/shared/python-bundle.ts +23 -2
  17. package/amplify/rebuild-trigger-settings.js +22 -0
  18. package/amplify/rebuild-trigger-settings.ts +42 -0
  19. package/amplify/site-backend-config.ts +12 -1
  20. package/amplify/site-backend.js +51 -27
  21. package/amplify/site-backend.ts +66 -34
  22. package/amplify/storage/resource.js +3 -0
  23. package/amplify/storage/resource.ts +3 -0
  24. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  25. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
  26. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  27. package/app/api/revalidate/route.ts +8 -7
  28. package/app/articles/[slug]/page.tsx +6 -4
  29. package/app/blog-rhythm.css +278 -0
  30. package/app/globals.css +22 -4
  31. package/app/layout.tsx +24 -12
  32. package/app/newsroom/articles/[id]/page.tsx +15 -0
  33. package/app/newsroom/articles/page.tsx +13 -0
  34. package/app/page.tsx +12 -4
  35. package/app/robots.ts +11 -0
  36. package/bin/papyrus-app.mjs +12 -2
  37. package/bin/papyrus-infra.mjs +74 -18
  38. package/components/archive-shell.tsx +2 -22
  39. package/components/article-video.tsx +247 -3
  40. package/components/blog-page-background.tsx +23 -6
  41. package/components/markus-ir-preview.tsx +300 -0
  42. package/components/news-desk-page.tsx +1 -4
  43. package/components/newspaper.tsx +6 -2
  44. package/components/newsroom-access-gate.tsx +66 -0
  45. package/components/newsroom-article-editor.tsx +741 -0
  46. package/components/newsroom-articles-view.tsx +129 -0
  47. package/components/newsroom-ops-overview.tsx +28 -11
  48. package/components/newsroom-ops-shell.tsx +3 -1
  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 +1 -59
  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 +94 -0
  61. package/infra/build-specs.ts +141 -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/blog-feature-solver.ts +459 -0
  68. package/lib/blog-rhythm.ts +134 -0
  69. package/lib/brand-fonts-watch.ts +43 -0
  70. package/lib/brand-fonts.ts +112 -0
  71. package/lib/cached-content-repository.ts +14 -1
  72. package/lib/category-repository.ts +1 -93
  73. package/lib/content-actions-client.ts +98 -0
  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/empty-theme.css +1 -0
  80. package/lib/graphql-content-repository.ts +239 -43
  81. package/lib/layout-scenarios.ts +8 -8
  82. package/lib/newspaper-layout.ts +3 -2
  83. package/lib/newsroom-articles.ts +325 -0
  84. package/lib/newsroom-demo-articles.ts +90 -0
  85. package/lib/newsroom-media-backend.ts +142 -0
  86. package/lib/newsroom-media.ts +182 -0
  87. package/lib/newsroom-nav.ts +3 -0
  88. package/lib/papyrus-web-locations.ts +7 -6
  89. package/lib/presentation-footer.ts +3 -2
  90. package/lib/pretext-layout.ts +12 -3
  91. package/lib/reader-base-path.ts +24 -0
  92. package/lib/reader-static-params.ts +8 -0
  93. package/lib/renderer.ts +7 -0
  94. package/lib/revalidate-secret.ts +59 -0
  95. package/lib/root-route-redirect.ts +16 -0
  96. package/lib/site-brand-tenant.ts +3 -22
  97. package/lib/site-brand.ts +95 -42
  98. package/lib/site-env.ts +52 -0
  99. package/lib/staging-gate.ts +37 -0
  100. package/lib/staging-gated-path.ts +22 -0
  101. package/lib/staging-preview-object.ts +98 -0
  102. package/lib/video-mode.ts +42 -0
  103. package/lib/video-script.ts +77 -0
  104. package/middleware.ts +56 -42
  105. package/package.json +40 -1
  106. package/renderers/pretext/article-page.tsx +40 -9
  107. package/renderers/pretext/presentation-shared.tsx +113 -0
  108. package/renderers/pretext/presentation-shell.tsx +18 -112
  109. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  110. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  111. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  112. package/routes.manifest.json +37 -2
  113. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  114. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  115. package/scripts/videoml/bundle-common.mjs +129 -0
  116. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  117. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  118. package/scripts/videoml/preview/preview-timing.ts +42 -0
  119. package/scripts/videoml/preview.html +77 -0
  120. package/scripts/videoml/standard-entry.tsx +46 -0
  121. package/src/sync.mjs +30 -6
  122. package/src/with-papyrus.mjs +15 -2
  123. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  124. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  125. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  126. package/publications/anth_us/brand.ts +0 -68
  127. package/publications/anth_us/theme.css +0 -66
  128. package/publications/pilobol_us/brand.ts +0 -37
  129. package/publications/pilobol_us/theme.css +0 -140
  130. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  131. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  132. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  133. package/publications/threat_intelligence/brand.ts +0 -40
  134. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  135. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  136. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  137. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  138. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  139. package/publications/threat_intelligence/theme.css +0 -1440
  140. /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+ from papyrus_content.graphql_authoring import create_authoring_client
8
+ from papyrus_content.markus_renderer.derive import derive_body
9
+ from papyrus_content.rebuild_trigger import (
10
+ DEFAULT_STAGING_BRANCH,
11
+ reader_target_from_environment,
12
+ trigger_pretext_revalidation,
13
+ trigger_rebuild,
14
+ trigger_staging_build,
15
+ )
16
+ from papyrus_content.publishing import (
17
+ ItemFields,
18
+ PublishError,
19
+ publish_item,
20
+ save_item,
21
+ unpublish_item,
22
+ )
23
+
24
+
25
+ def parse_input(raw: Any) -> dict:
26
+ parsed = json.loads(raw) if isinstance(raw, (str, bytes)) else raw
27
+ if not isinstance(parsed, dict):
28
+ raise PublishError("invalid-input", "The input must be a JSON object.")
29
+ return parsed
30
+
31
+
32
+ def actor_from_event(event: dict) -> str:
33
+ identity = event.get("identity") or {}
34
+ return identity.get("username") or identity.get("sub") or "unknown"
35
+
36
+
37
+ def failure_response(error: PublishError) -> dict:
38
+ if error.errors:
39
+ errors = [body_error.to_dict() for body_error in error.errors]
40
+ else:
41
+ errors = [{"code": error.code, "message": error.message, "line": None}]
42
+ return {"ok": False, "errors": errors}
43
+
44
+
45
+ def derive_markus_action(arguments: dict) -> dict:
46
+ derivation = derive_body(arguments.get("frontMatterYaml"), arguments.get("bodyMarkus") or "")
47
+ if not derivation.ok:
48
+ return {"ok": False, "errors": [body_error.to_dict() for body_error in derivation.errors]}
49
+ response = {
50
+ "ok": True,
51
+ "bodyIrBytes": len(json.dumps(derivation.body_ir, separators=(",", ":")).encode("utf-8")),
52
+ "errors": [],
53
+ }
54
+ if arguments.get("includeIr") is True:
55
+ response["bodyIr"] = derivation.body_ir
56
+ return response
57
+
58
+
59
+ def save_item_draft_action(arguments: dict, actor: str) -> dict:
60
+ client, _claims = create_authoring_client()
61
+ fields = ItemFields(
62
+ type=arguments["type"],
63
+ slug=arguments["slug"],
64
+ section=arguments.get("section"),
65
+ front_matter_yaml=arguments.get("frontMatterYaml"),
66
+ body_markus=arguments.get("bodyMarkus") or "",
67
+ aliases=list(arguments.get("aliases") or []),
68
+ id=arguments.get("id"),
69
+ )
70
+ record = save_item(client, fields, actor=actor, expected_content_hash=arguments.get("expectedContentHash"))
71
+ return {
72
+ "ok": True,
73
+ "item": {
74
+ "id": record["id"],
75
+ "contentHash": record["contentHash"],
76
+ "status": record["status"],
77
+ "slug": record["slug"],
78
+ "versionNumber": record.get("versionNumber"),
79
+ },
80
+ }
81
+
82
+
83
+ def site_triggers_for(client, item_id: str) -> dict:
84
+ triggers: dict = {}
85
+ reader_app_id, reader_branch = reader_target_from_environment()
86
+ revalidate_base_url = (os.environ.get("PAPYRUS_REVALIDATE_BASE_URL") or "").strip()
87
+ if revalidate_base_url:
88
+ try:
89
+ item = client.get_record("Item", item_id) or {}
90
+ slugs = [item["slug"]] if item.get("slug") else []
91
+ revalidated = trigger_pretext_revalidation(slugs, None, base_url=revalidate_base_url)
92
+ triggers["revalidated"] = revalidated if revalidated is not None else {"ok": False, "reason": "not-configured"}
93
+ except Exception as error: # noqa: BLE001 - a trigger failure never fails the publish
94
+ triggers["revalidated"] = {"ok": False, "error": f"{type(error).__name__}: {error}"}
95
+ if reader_app_id:
96
+ triggers["rebuild"] = trigger_rebuild(reader_app_id=reader_app_id, reader_branch=reader_branch)
97
+ return triggers
98
+
99
+
100
+ def publish_item_action(arguments: dict, actor: str) -> dict:
101
+ client, _claims = create_authoring_client()
102
+ result = publish_item(client, arguments["id"], actor=actor)
103
+ response = {
104
+ "ok": True,
105
+ "changed": result.changed,
106
+ "publishedId": result.published_item_id,
107
+ "versionNumber": result.version_number,
108
+ }
109
+ if result.changed:
110
+ response.update(site_triggers_for(client, arguments["id"]))
111
+ return response
112
+
113
+
114
+ def unpublish_item_action(arguments: dict, actor: str) -> dict:
115
+ client, _claims = create_authoring_client()
116
+ result = unpublish_item(client, arguments["id"], actor=actor)
117
+ response = {"ok": True, "changed": result.changed}
118
+ if result.changed:
119
+ response.update(site_triggers_for(client, arguments["id"]))
120
+ return response
121
+
122
+
123
+ def request_staging_build_action() -> dict:
124
+ cms_app_id = (os.environ.get("PAPYRUS_CMS_AMPLIFY_APP_ID") or "").strip()
125
+ if not cms_app_id:
126
+ return {"ok": False, "errors": [{"code": "staging-build-not-configured", "message": "This site has no staging build configured.", "line": None}]}
127
+ staging_branch = (os.environ.get("PAPYRUS_STAGING_BRANCH") or "").strip() or DEFAULT_STAGING_BRANCH
128
+ build = trigger_staging_build(cms_app_id=cms_app_id, staging_branch=staging_branch)
129
+ return {"ok": True, "build": build}
130
+
131
+
132
+ def handler(event, context):
133
+ field = event["fieldName"]
134
+ arguments = parse_input(event["arguments"]["input"])
135
+ actor = actor_from_event(event)
136
+ try:
137
+ if field == "deriveMarkus":
138
+ return derive_markus_action(arguments)
139
+ if field == "saveItemDraft":
140
+ return save_item_draft_action(arguments, actor)
141
+ if field == "publishItem":
142
+ return publish_item_action(arguments, actor)
143
+ if field == "unpublishItem":
144
+ return unpublish_item_action(arguments, actor)
145
+ if field == "requestStagingBuild":
146
+ return request_staging_build_action()
147
+ except PublishError as error:
148
+ return failure_response(error)
149
+ raise ValueError(f"Unsupported field {field}")
@@ -0,0 +1,4 @@
1
+ PyYAML>=6.0.2,<7
2
+ citeproc-py>=0.8.2,<1
3
+ anthus-markus==0.5.1
4
+ Pillow>=10,<12
@@ -0,0 +1,40 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ import { Duration } from "aws-cdk-lib";
3
+ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
4
+ import { execSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { pythonLambdaCode } from "../shared/python-bundle.js";
8
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const projectRoot = path.resolve(dirname, "../../..");
10
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=10,<12"];
11
+ function installCheckoutRequirements(outputDir) {
12
+ const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
13
+ execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
14
+ }
15
+ export const contentActions = defineFunction((scope) => {
16
+ return new Function(scope, "papyrus-content-actions", {
17
+ runtime: Runtime.PYTHON_3_12,
18
+ architecture: Architecture.ARM_64,
19
+ handler: "handler.handler",
20
+ timeout: Duration.seconds(60),
21
+ memorySize: 1024,
22
+ code: pythonLambdaCode({
23
+ functionDir: "content-actions",
24
+ modules: ["papyrus_content"],
25
+ requirements: contentActionsRequirements,
26
+ noDeps: true,
27
+ checkoutExtras: installCheckoutRequirements,
28
+ }, [
29
+ "bash",
30
+ "-c",
31
+ [
32
+ "set -euo pipefail",
33
+ "mkdir -p /asset-output/papyrus_content",
34
+ "cp amplify/functions/content-actions/handler.py /asset-output/handler.py",
35
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
36
+ "python3 -m pip install -r amplify/functions/content-actions/requirements.txt -t /asset-output --no-cache-dir",
37
+ ].join(" && "),
38
+ ]),
39
+ });
40
+ }, { resourceGroupName: "data" });
@@ -0,0 +1,54 @@
1
+ import { defineFunction } from "@aws-amplify/backend";
2
+ import { Duration } from "aws-cdk-lib";
3
+ import { Architecture, Function, Runtime } from "aws-cdk-lib/aws-lambda";
4
+ import { Construct } from "constructs";
5
+ import { execSync } from "node:child_process";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { pythonLambdaCode } from "../shared/python-bundle";
9
+
10
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const projectRoot = path.resolve(dirname, "../../..");
12
+
13
+ export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.5.1", "Pillow>=10,<12"];
14
+
15
+ function installCheckoutRequirements(outputDir: string): void {
16
+ const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
17
+ execSync(
18
+ `python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`,
19
+ { cwd: projectRoot, stdio: "inherit" },
20
+ );
21
+ }
22
+
23
+ export const contentActions = defineFunction(
24
+ (scope: Construct) => {
25
+ return new Function(scope, "papyrus-content-actions", {
26
+ runtime: Runtime.PYTHON_3_12,
27
+ architecture: Architecture.ARM_64,
28
+ handler: "handler.handler",
29
+ timeout: Duration.seconds(60),
30
+ memorySize: 1024,
31
+ code: pythonLambdaCode(
32
+ {
33
+ functionDir: "content-actions",
34
+ modules: ["papyrus_content"],
35
+ requirements: contentActionsRequirements,
36
+ noDeps: true,
37
+ checkoutExtras: installCheckoutRequirements,
38
+ },
39
+ [
40
+ "bash",
41
+ "-c",
42
+ [
43
+ "set -euo pipefail",
44
+ "mkdir -p /asset-output/papyrus_content",
45
+ "cp amplify/functions/content-actions/handler.py /asset-output/handler.py",
46
+ "cp -R src/papyrus_content/. /asset-output/papyrus_content/",
47
+ "python3 -m pip install -r amplify/functions/content-actions/requirements.txt -t /asset-output --no-cache-dir",
48
+ ].join(" && "),
49
+ ],
50
+ ),
51
+ });
52
+ },
53
+ { resourceGroupName: "data" },
54
+ );
@@ -58,9 +58,24 @@ function bundleFromPip(outputDir, options) {
58
58
  // Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
59
59
  "--ignore-requires-python",
60
60
  ...extra,
61
- pipSpec(),
61
+ ...(options.noDeps ? [] : [pipSpec()]),
62
62
  ...(options.requirements ?? []),
63
63
  ], { stdio: "inherit" });
64
+ if (options.noDeps) {
65
+ execFileSync(python, [
66
+ "-m", "pip", "install",
67
+ "--target", outputDir,
68
+ "--no-cache-dir",
69
+ "--no-deps",
70
+ "--platform", "manylinux2014_aarch64",
71
+ "--implementation", "cp",
72
+ "--python-version", "3.12",
73
+ "--only-binary=:all:",
74
+ "--ignore-requires-python",
75
+ ...extra,
76
+ pipSpec(),
77
+ ], { stdio: "inherit" });
78
+ }
64
79
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
65
80
  if (options.corpora) {
66
81
  const corporaDir = path.join(outputDir, "corpora");
@@ -74,7 +89,7 @@ function bundleFromPip(outputDir, options) {
74
89
  }
75
90
  }
76
91
  }
77
- function bundleFromCheckout(outputDir, options) {
92
+ export function bundleFromCheckout(outputDir, options) {
78
93
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
79
94
  for (const module of options.modules) {
80
95
  fs.mkdirSync(path.join(outputDir, module), { recursive: true });
@@ -53,6 +53,8 @@ export type PythonBundleOptions = {
53
53
  checkoutExtras?: (outputDir: string) => void;
54
54
  /** Extra pip requirements in package mode, beyond `papyrus-newsroom`. */
55
55
  requirements?: string[];
56
+ /** Package mode: install `papyrus-newsroom` with `--no-deps`, then only `requirements`. */
57
+ noDeps?: boolean;
56
58
  /** Copy `<cwd>/corpora/*.yml` into the asset (publication-owned steering config). */
57
59
  corpora?: boolean;
58
60
  };
@@ -77,11 +79,30 @@ function bundleFromPip(outputDir: string, options: PythonBundleOptions): void {
77
79
  // Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
78
80
  "--ignore-requires-python",
79
81
  ...extra,
80
- pipSpec(),
82
+ ...(options.noDeps ? [] : [pipSpec()]),
81
83
  ...(options.requirements ?? []),
82
84
  ],
83
85
  { stdio: "inherit" },
84
86
  );
87
+ if (options.noDeps) {
88
+ execFileSync(
89
+ python,
90
+ [
91
+ "-m", "pip", "install",
92
+ "--target", outputDir,
93
+ "--no-cache-dir",
94
+ "--no-deps",
95
+ "--platform", "manylinux2014_aarch64",
96
+ "--implementation", "cp",
97
+ "--python-version", "3.12",
98
+ "--only-binary=:all:",
99
+ "--ignore-requires-python",
100
+ ...extra,
101
+ pipSpec(),
102
+ ],
103
+ { stdio: "inherit" },
104
+ );
105
+ }
85
106
  fs.copyFileSync(
86
107
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
87
108
  path.join(outputDir, "handler.py"),
@@ -98,7 +119,7 @@ function bundleFromPip(outputDir: string, options: PythonBundleOptions): void {
98
119
  }
99
120
  }
100
121
 
101
- function bundleFromCheckout(outputDir: string, options: PythonBundleOptions): void {
122
+ export function bundleFromCheckout(outputDir: string, options: PythonBundleOptions): void {
102
123
  fs.copyFileSync(
103
124
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
104
125
  path.join(outputDir, "handler.py"),
@@ -0,0 +1,22 @@
1
+ export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
2
+ function branchJobsArn(input, appId, branch) {
3
+ return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
4
+ }
5
+ export function rebuildTriggerSettings(input) {
6
+ const environment = {};
7
+ const resources = [];
8
+ const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
9
+ if (readerAppId !== "") {
10
+ const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
11
+ environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
12
+ environment.PAPYRUS_READER_BRANCH = readerBranch;
13
+ resources.push(branchJobsArn(input, readerAppId, readerBranch));
14
+ }
15
+ const cmsAppId = input.cmsAppId.trim();
16
+ if (input.stagingBuildEnabled && cmsAppId !== "") {
17
+ environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
18
+ environment.PAPYRUS_STAGING_BRANCH = "staging";
19
+ resources.push(branchJobsArn(input, cmsAppId, "staging"));
20
+ }
21
+ return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
22
+ }
@@ -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,13 @@ 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
+ import { contentActions } from "./functions/content-actions/resource.js";
21
22
  import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
22
23
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
23
24
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
@@ -75,12 +76,13 @@ export function defineSiteBackend(papyrusSite) {
75
76
  const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
76
77
  const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
77
78
  const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
79
+ const authConfig = site.auth ?? authConfigFromEnv();
78
80
  const backend = defineBackend({
79
81
  assignmentAction,
80
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
82
+ auth: defineSiteAuth(authConfig),
81
83
  categoryAction,
84
+ contentActions,
82
85
  data,
83
- graphqlJwtAuthorizer,
84
86
  knowledgeQuery,
85
87
  manageUserRole,
86
88
  modelAttachmentUpload,
@@ -93,6 +95,12 @@ export function defineSiteBackend(papyrusSite) {
93
95
  slackDelivery,
94
96
  storage,
95
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
+ }
96
104
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
97
105
  // Repo root in a Papyrus checkout; package root when installed from npm.
98
106
  const projectRoot = resolve(amplifyBackendDir, "..");
@@ -136,36 +144,18 @@ export function defineSiteBackend(papyrusSite) {
136
144
  const messageThreadTable = backend.data.resources.tables.MessageThread;
137
145
  const dataStack = Stack.of(messageTable);
138
146
  const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
139
- const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild;
140
- const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
141
- const jwtAuthorizerVariables = jwtAuthorizerEnvironment
142
- && typeof jwtAuthorizerEnvironment === "object"
143
- && "variables" in jwtAuthorizerEnvironment
144
- ? jwtAuthorizerEnvironment.variables
145
- : undefined;
146
- const jwtSsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
147
- || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
148
- || "";
147
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
149
148
  if (enableConsoleResponder) {
150
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
149
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
151
150
  messageTable,
152
151
  messageStreamArn,
153
152
  threadTable: messageThreadTable,
154
153
  projectRoot,
155
154
  graphqlEndpoint,
156
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
157
155
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
158
156
  model: process.env.PAPYRUS_CONSOLE_MODEL,
159
157
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
160
158
  });
161
- if (jwtSsmEnvConfig) {
162
- consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
163
- }
164
- const jwtSecretSsmParam = process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
165
- || (productionAppId !== "" && amplifyAppId === productionAppId
166
- ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
167
- : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
168
- consoleChatResponder.responderFunction.addEnvironment("PAPYRUS_JWT_SECRET_SSM_PARAM", jwtSecretSsmParam);
169
159
  }
170
160
  if (enableSlackAgent) {
171
161
  const slackEventsLambda = backend.slackEvents.resources.lambda;
@@ -177,9 +167,9 @@ export function defineSiteBackend(papyrusSite) {
177
167
  backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
178
168
  backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
179
169
  backend.slackEvents.addEnvironment("PAPYRUS_SLACK_ALLOWED_USER_IDS", (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim());
180
- if (jwtSsmEnvConfig) {
181
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
182
- 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);
183
173
  }
184
174
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
185
175
  backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
@@ -310,6 +300,7 @@ export function defineSiteBackend(papyrusSite) {
310
300
  for (const resource of [
311
301
  backend.assignmentAction,
312
302
  backend.categoryAction,
303
+ backend.contentActions,
313
304
  backend.emailSubmissionProcessor,
314
305
  backend.knowledgeQuery,
315
306
  backend.modelAttachmentUpload,
@@ -323,6 +314,39 @@ export function defineSiteBackend(papyrusSite) {
323
314
  grantNewsroomReadWrite(backend.assignmentAction.resources.lambda);
324
315
  grantNewsroomReadWrite(backend.categoryAction.resources.lambda);
325
316
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
317
+ const contentActionsLambda = backend.contentActions.resources.lambda;
318
+ grantNewsroomReadWriteDelete(contentActionsLambda);
319
+ contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
320
+ contentActionsLambda.addToRolePolicy(new PolicyStatement({
321
+ actions: ["appsync:GraphQL"],
322
+ resources: ["*"],
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
+ }
326
350
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
327
351
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
328
352
  if (backend.sesInboundReceive) {