@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.20
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 +52 -70
- package/amplify/data/resource.ts +57 -70
- package/amplify/data/schema.js +52 -58
- package/amplify/data/schema.ts +57 -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 +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- 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 +51 -27
- package/amplify/site-backend.ts +66 -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 +4 -2
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +3 -1
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +22 -4
- package/app/layout.tsx +24 -12
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/page.tsx +12 -4
- 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/news-desk-page.tsx +1 -4
- package/components/newspaper.tsx +2 -1
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- 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 +1 -59
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +31 -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 +94 -0
- package/infra/build-specs.ts +141 -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/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +2 -1
- package/lib/category-repository.ts +1 -93
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +3 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +201 -42
- package/lib/layout-scenarios.ts +8 -8
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- 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 +7 -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/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +95 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-gated-path.ts +22 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +56 -42
- package/package.json +39 -1
- package/renderers/pretext/article-page.tsx +40 -9
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +18 -112
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +280 -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/src/with-papyrus.mjs +15 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /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,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
|
-
/**
|
|
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,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 {
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 (
|
|
181
|
-
backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG",
|
|
182
|
-
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);
|
|
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) {
|