@anthusai/papyrus 5.42.1 → 5.43.0-next.10
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/data/schema.js +2 -0
- package/amplify/data/schema.ts +2 -0
- package/amplify/functions/category-action/handler.ts +2 -1
- package/amplify/functions/monitoring-view/schema.js +22 -0
- package/amplify/functions/monitoring-view/schema.ts +22 -0
- package/amplify/functions/monitoring-view/wire.js +1 -0
- package/amplify/functions/monitoring-view/wire.ts +1 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/handler.py +137 -0
- package/amplify/{plugins/reader-steering/function → functions/proposal-actions}/proposals.py +20 -3
- package/amplify/functions/proposal-actions/resource.js +15 -0
- package/amplify/functions/proposal-actions/resource.ts +22 -0
- package/amplify/functions/proposal-actions/schema.js +106 -0
- package/amplify/functions/proposal-actions/schema.ts +108 -0
- package/amplify/functions/proposal-actions/wire.js +10 -0
- package/amplify/functions/proposal-actions/wire.ts +13 -0
- package/amplify/plugins/reader-steering/function/handler.py +13 -72
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +5 -5
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +5 -5
- package/amplify/plugins/reader-steering/schema.js +0 -95
- package/amplify/plugins/reader-steering/schema.ts +0 -95
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- package/app/globals.css +31 -35
- package/app/tailwind.css +2 -0
- package/bin/papyrus-infra.mjs +21 -1
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/monitoring-overview-view.tsx +140 -23
- package/components/monitoring-panel.tsx +96 -6
- package/components/news-desk-client-provider.tsx +8 -2
- package/components/news-desk-taxonomy-client.ts +2 -1
- package/components/newsroom-mobile-navigation.tsx +23 -0
- package/components/newsroom-ops-overview.tsx +6 -1
- package/components/newsroom-ops-shell.tsx +8 -10
- package/components/newsroom-organizer.css +1 -0
- package/components/newsroom-references-view.tsx +139 -257
- package/components/notification-center.tsx +345 -63
- package/components/papyrus-console-frame.tsx +66 -0
- package/components/papyrus-console-shell.tsx +14 -42
- package/components/papyrus-private-chat-panel.tsx +50 -16
- package/components/proposals-panel.tsx +107 -0
- package/components/proposals-view.tsx +95 -0
- package/components/reference-review-card.tsx +400 -0
- package/components/topic-steering-workspace.tsx +13 -2
- package/components/use-console-visibility.ts +47 -0
- package/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +18 -2
- package/infra/chat-stack.js +222 -0
- package/infra/chat-stack.ts +250 -0
- package/infra/chat.test.js +142 -0
- package/infra/chat.test.ts +151 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +58 -1
- package/infra/site-config.ts +78 -1
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/monitoring-client.ts +39 -3
- package/lib/monitoring-overview-fixture.ts +2 -2
- package/lib/monitoring-overview.ts +47 -3
- package/lib/monitoring-paging.ts +58 -0
- package/lib/newsroom-chat-client.ts +30 -0
- package/lib/newsroom-chat-identity.ts +40 -0
- package/lib/newsroom-organizer-nav.ts +1 -0
- package/lib/papyrus-hosted-chat.ts +62 -80
- package/lib/papyrus-private-chat-client.ts +7 -4
- package/lib/proposals-admin.ts +115 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/prospectus-session.ts +2 -0
- package/lib/prospectus-sign-in-routes.ts +19 -2
- package/lib/prospectus-site-identity.ts +7 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/relevance-decisions.ts +9 -0
- package/lib/review-client.ts +38 -1
- package/lib/review-drafts.ts +231 -0
- package/lib/review-fixture.ts +36 -2
- package/lib/review-requests.ts +66 -24
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/package.json +16 -1
- package/routes.manifest.json +12 -1
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.mjs +1 -1
|
@@ -30,7 +30,6 @@ if _FUNCTION_DIRECTORY not in sys.path: # a Lambda already has its code directo
|
|
|
30
30
|
sys.path.insert(0, _FUNCTION_DIRECTORY)
|
|
31
31
|
import ai_moderation # noqa: E402
|
|
32
32
|
import moderation as moderation_rules # noqa: E402
|
|
33
|
-
import proposals as proposal_rules # noqa: E402
|
|
34
33
|
|
|
35
34
|
SITE_SUBJECT = re.compile(r"ss1_[A-Za-z0-9_-]{43}")
|
|
36
35
|
INTERIM_SUBJECT = re.compile(r"ssdev_[A-Za-z0-9_-]{43}")
|
|
@@ -979,7 +978,6 @@ class DynamoReaderStore:
|
|
|
979
978
|
report_table: str = "",
|
|
980
979
|
action_table: str = "",
|
|
981
980
|
item_table: str = "",
|
|
982
|
-
proposal_table: str = "",
|
|
983
981
|
flag_table: str = "",
|
|
984
982
|
) -> None:
|
|
985
983
|
self._resource = resource
|
|
@@ -994,72 +992,12 @@ class DynamoReaderStore:
|
|
|
994
992
|
"report": report_table,
|
|
995
993
|
"action": action_table,
|
|
996
994
|
"item": item_table,
|
|
997
|
-
"proposal": proposal_table,
|
|
998
995
|
"flag": flag_table,
|
|
999
996
|
}
|
|
1000
997
|
|
|
1001
998
|
def _table(self, key: str) -> Any:
|
|
1002
999
|
return self._resource.Table(self._names[key])
|
|
1003
1000
|
|
|
1004
|
-
def get_proposal(self, proposal_id: str) -> dict[str, Any] | None:
|
|
1005
|
-
item = self._table("proposal").get_item(Key={"id": proposal_id}, ConsistentRead=True).get("Item")
|
|
1006
|
-
return plain(item) if item else None
|
|
1007
|
-
|
|
1008
|
-
def create_proposal(self, row: dict[str, Any]) -> bool:
|
|
1009
|
-
item = {**_without_none(row), "__typename": "Proposal", "updatedAt": row["createdAt"]}
|
|
1010
|
-
try:
|
|
1011
|
-
self._table("proposal").put_item(Item=item, ConditionExpression="attribute_not_exists(id)")
|
|
1012
|
-
except Exception as error:
|
|
1013
|
-
if is_conditional_failure(error):
|
|
1014
|
-
return False
|
|
1015
|
-
raise
|
|
1016
|
-
return True
|
|
1017
|
-
|
|
1018
|
-
def list_story_drafts(self, limit: int) -> list[dict[str, Any]]:
|
|
1019
|
-
"""The story drafts of the table, only the fields ``listStoryDrafts`` may answer; at most ``limit`` (the newest if there are more)."""
|
|
1020
|
-
fields = proposal_rules.STORY_DRAFT_FIELDS
|
|
1021
|
-
names = {"#kind": "kind", **{f"#f{index}": field for index, field in enumerate(fields)}}
|
|
1022
|
-
request: dict[str, Any] = {
|
|
1023
|
-
"FilterExpression": "#kind = :kind",
|
|
1024
|
-
"ProjectionExpression": ", ".join(f"#f{index}" for index in range(len(fields))) + ", #kind",
|
|
1025
|
-
"ExpressionAttributeNames": names,
|
|
1026
|
-
"ExpressionAttributeValues": {":kind": "story_draft"},
|
|
1027
|
-
}
|
|
1028
|
-
rows: list[dict[str, Any]] = []
|
|
1029
|
-
while True:
|
|
1030
|
-
page = self._table("proposal").scan(**request)
|
|
1031
|
-
rows.extend(plain(item) for item in page.get("Items") or [])
|
|
1032
|
-
if not page.get("LastEvaluatedKey"):
|
|
1033
|
-
break
|
|
1034
|
-
request["ExclusiveStartKey"] = page["LastEvaluatedKey"]
|
|
1035
|
-
rows.sort(key=lambda row: (row.get("createdAt") or "", row.get("id") or ""), reverse=True)
|
|
1036
|
-
return [{field: row.get(field) for field in fields} for row in rows[:limit]]
|
|
1037
|
-
|
|
1038
|
-
def change_proposal(self, proposal_id: str, changes: dict[str, Any], expected_state: str, now_iso: str, *, expected_updated_at: str | None = None) -> bool:
|
|
1039
|
-
"""Apply ``changes`` if the state (and, when given, ``updatedAt``) is still what the caller saw; a value of ``None`` removes the attribute."""
|
|
1040
|
-
names = {"#st": "state", "#u": "updatedAt", **{f"#c{index}": key for index, key in enumerate(changes)}}
|
|
1041
|
-
sets = ", ".join(f"#c{index} = :c{index}" for index, value in enumerate(changes.values()) if value is not None)
|
|
1042
|
-
removes = ", ".join(f"#c{index}" for index, value in enumerate(changes.values()) if value is None)
|
|
1043
|
-
values = {":expected": expected_state, ":now": now_iso, **{f":c{index}": value for index, value in enumerate(changes.values()) if value is not None}}
|
|
1044
|
-
condition = "#st = :expected"
|
|
1045
|
-
if expected_updated_at is not None:
|
|
1046
|
-
condition += " AND #u = :seen"
|
|
1047
|
-
values[":seen"] = expected_updated_at
|
|
1048
|
-
expression = f"SET {sets + ', ' if sets else ''}#u = :now" + (f" REMOVE {removes}" if removes else "")
|
|
1049
|
-
try:
|
|
1050
|
-
self._table("proposal").update_item(
|
|
1051
|
-
Key={"id": proposal_id},
|
|
1052
|
-
UpdateExpression=expression,
|
|
1053
|
-
ConditionExpression=condition,
|
|
1054
|
-
ExpressionAttributeNames=names,
|
|
1055
|
-
ExpressionAttributeValues=values,
|
|
1056
|
-
)
|
|
1057
|
-
except Exception as error:
|
|
1058
|
-
if is_conditional_failure(error):
|
|
1059
|
-
return False
|
|
1060
|
-
raise
|
|
1061
|
-
return True
|
|
1062
|
-
|
|
1063
1001
|
def get_profile(self, site_subject: str) -> dict[str, Any] | None:
|
|
1064
1002
|
item = self._table("profile").get_item(Key={"id": site_subject}, ConsistentRead=True).get("Item")
|
|
1065
1003
|
return plain(item) if item else None
|
|
@@ -1473,7 +1411,6 @@ def store_from_environment(environment: dict[str, str] | None = None) -> DynamoR
|
|
|
1473
1411
|
report_table=environment.get("PAPYRUS_READER_REPORT_TABLE", ""),
|
|
1474
1412
|
action_table=environment.get("PAPYRUS_READER_ACTION_TABLE", ""),
|
|
1475
1413
|
item_table=environment.get("PAPYRUS_READER_ITEM_TABLE", ""),
|
|
1476
|
-
proposal_table=environment.get("PAPYRUS_READER_PROPOSAL_TABLE", ""),
|
|
1477
1414
|
flag_table=environment.get("PAPYRUS_READER_FLAG_TABLE", ""),
|
|
1478
1415
|
)
|
|
1479
1416
|
|
|
@@ -1528,17 +1465,11 @@ def run_ai_worker(
|
|
|
1528
1465
|
|
|
1529
1466
|
def handle_staff_event(event: dict[str, Any], *, store: Any, settings: ReaderSettings, now_seconds: float, account_id: str | None = None) -> dict[str, Any]:
|
|
1530
1467
|
"""``moderateReader`` from AppSync: an editor or admin hides, approves, promotes, bans or undoes. Answers a receipt, never an exception."""
|
|
1531
|
-
if proposal_rules.event_field_name(event) in proposal_rules.MUTATIONS:
|
|
1532
|
-
return proposal_rules.handle_proposal_event(event, store, now_iso=iso(now_seconds), account_id=account_id)
|
|
1533
|
-
if proposal_rules.event_field_name(event) in proposal_rules.QUERIES:
|
|
1534
|
-
return proposal_rules.handle_proposal_query(event, store, account_id=account_id)
|
|
1535
1468
|
identity = event.get("identity") or {}
|
|
1536
1469
|
claims = identity.get("claims") or {}
|
|
1537
1470
|
groups = claims.get("cognito:groups") or identity.get("groups") or []
|
|
1538
|
-
if isinstance(groups, str):
|
|
1539
|
-
groups = [group.strip() for group in groups.strip("[]").replace(",", " ").split()]
|
|
1540
1471
|
subject = identity.get("sub")
|
|
1541
|
-
if not isinstance(subject, str) or not subject or not moderation_rules.
|
|
1472
|
+
if not isinstance(subject, str) or not subject or not moderation_rules.has_editor_rights(groups):
|
|
1542
1473
|
return moderation_rules.refused("not_staff", "Only editors and admins can moderate.").to_dict()
|
|
1543
1474
|
arguments = event.get("arguments") or {}
|
|
1544
1475
|
outcome = moderation_rules.apply_action(
|
|
@@ -1556,8 +1487,18 @@ def handle_staff_event(event: dict[str, Any], *, store: Any, settings: ReaderSet
|
|
|
1556
1487
|
return outcome.to_dict()
|
|
1557
1488
|
|
|
1558
1489
|
|
|
1490
|
+
def is_appsync_event(event: dict[str, Any]) -> bool:
|
|
1491
|
+
"""True for an AppSync invocation (arguments plus ``info`` or a top-level ``fieldName``); the public path sends siteSubject, action and payload.
|
|
1492
|
+
|
|
1493
|
+
The Amplify Gen 2 Lambda data source sends ``fieldName`` at the top level with no ``info`` key; a direct resolver nests it under ``info``.
|
|
1494
|
+
"""
|
|
1495
|
+
info = event.get("info")
|
|
1496
|
+
name = (info.get("fieldName") if isinstance(info, dict) else None) or event.get("fieldName")
|
|
1497
|
+
return "arguments" in event and ("info" in event or (isinstance(name, str) and bool(name)))
|
|
1498
|
+
|
|
1499
|
+
|
|
1559
1500
|
def function_account(context: Any) -> str | None:
|
|
1560
|
-
"""The AWS account this function runs in, from its own ARN: the
|
|
1501
|
+
"""The AWS account this function runs in, from its own ARN: the account of the function."""
|
|
1561
1502
|
parts = str(getattr(context, "invoked_function_arn", None) or "").split(":")
|
|
1562
1503
|
return parts[4] if len(parts) > 4 and parts[4] else None
|
|
1563
1504
|
|
|
@@ -1568,7 +1509,7 @@ def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
|
1568
1509
|
settings = settings_from_environment()
|
|
1569
1510
|
store = store_from_environment()
|
|
1570
1511
|
now_seconds = time.time()
|
|
1571
|
-
if
|
|
1512
|
+
if is_appsync_event(event):
|
|
1572
1513
|
return handle_staff_event(event, store=store, settings=settings, now_seconds=now_seconds, account_id=function_account(_context))
|
|
1573
1514
|
target = snapshot_target_from_environment()
|
|
1574
1515
|
if settings.moderation_mode == "ai" and target is not None and _context is not None:
|
|
@@ -37,6 +37,25 @@ READER_ID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f
|
|
|
37
37
|
ACTION_ID = re.compile(r"mod-[0-9a-f]{32}")
|
|
38
38
|
MAXIMUM_REASON = 500
|
|
39
39
|
STAFF_GROUPS = frozenset({"editor", "admin"})
|
|
40
|
+
|
|
41
|
+
_GROUP_SEPARATORS = re.compile(r"[\s,]+")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def claim_groups(value: Any) -> list[str]:
|
|
45
|
+
"""Copy of ``papyrus_content.staff_groups.claim_groups`` (this function ships without that package; a test keeps them equal)."""
|
|
46
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
47
|
+
raw = [item for item in value if isinstance(item, str)]
|
|
48
|
+
elif isinstance(value, str):
|
|
49
|
+
raw = _GROUP_SEPARATORS.split(value)
|
|
50
|
+
else:
|
|
51
|
+
return []
|
|
52
|
+
cleaned = (item.strip().strip("\"'[](){}").strip() for item in raw)
|
|
53
|
+
return [item for item in cleaned if item]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def has_editor_rights(groups: Any) -> bool:
|
|
57
|
+
"""``admin`` has every editor right: an admin who is not in the ``editor`` group is staff."""
|
|
58
|
+
return any(group.lower() in STAFF_GROUPS for group in claim_groups(groups))
|
|
40
59
|
ITEM_ID_PREFIX = "item-"
|
|
41
60
|
|
|
42
61
|
|
|
@@ -14,9 +14,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
14
14
|
export const readerSteeringManifest = {
|
|
15
15
|
name: "reader-steering",
|
|
16
16
|
contract: 1,
|
|
17
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
18
|
-
mutations: ["moderateReader"
|
|
19
|
-
queries: ["listStoryDrafts"],
|
|
17
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
18
|
+
mutations: ["moderateReader"],
|
|
20
19
|
guest: { mutations: [] },
|
|
21
20
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
22
21
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -119,7 +118,6 @@ export const readerSteeringPlugin = {
|
|
|
119
118
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
120
119
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
121
120
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
122
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
123
121
|
]) {
|
|
124
122
|
tables[model].grantReadWriteData(lambda);
|
|
125
123
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -136,8 +134,10 @@ export const readerSteeringPlugin = {
|
|
|
136
134
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
137
135
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
138
136
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
139
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
137
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
138
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
140
139
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
140
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
141
141
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
142
142
|
if (config.reservedConcurrency !== undefined) {
|
|
143
143
|
lambda.node.defaultChild.addPropertyOverride("ReservedConcurrentExecutions", config.reservedConcurrency);
|
|
@@ -21,9 +21,8 @@ export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARA
|
|
|
21
21
|
export const readerSteeringManifest: PluginManifest = {
|
|
22
22
|
name: "reader-steering",
|
|
23
23
|
contract: 1,
|
|
24
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"
|
|
25
|
-
mutations: ["moderateReader"
|
|
26
|
-
queries: ["listStoryDrafts"],
|
|
24
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderFlag", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
25
|
+
mutations: ["moderateReader"],
|
|
27
26
|
guest: { mutations: [] },
|
|
28
27
|
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
29
28
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
@@ -138,7 +137,6 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
138
137
|
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
139
138
|
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
140
139
|
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
141
|
-
["PAPYRUS_READER_PROPOSAL_TABLE", "Proposal"],
|
|
142
140
|
] as const) {
|
|
143
141
|
tables[model].grantReadWriteData(lambda);
|
|
144
142
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
@@ -155,8 +153,10 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
155
153
|
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
156
154
|
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
157
155
|
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
158
|
-
// The snapshot reads proposals and freezes the community result of one that reaches editor review.
|
|
156
|
+
// The snapshot reads proposals and freezes the community result of one that reaches editor review. The Proposal model and the
|
|
157
|
+
// functions that file and move proposals are core (amplify/functions/proposal-actions); this plugin only reads the same table.
|
|
159
158
|
tables.Proposal.grantReadWriteData(contentActions);
|
|
159
|
+
contentActions.addEnvironment("PAPYRUS_READER_PROPOSAL_TABLE", tables.Proposal.tableName);
|
|
160
160
|
|
|
161
161
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
162
162
|
if (config.reservedConcurrency !== undefined) {
|
|
@@ -135,101 +135,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
135
135
|
.disableOperations(["subscriptions"])
|
|
136
136
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
137
137
|
.authorization(staffOnly),
|
|
138
|
-
Proposal: a
|
|
139
|
-
.model({
|
|
140
|
-
id: a.id().required(),
|
|
141
|
-
kind: a.string().required(),
|
|
142
|
-
title: a.string().required(),
|
|
143
|
-
summary: a.string(),
|
|
144
|
-
body: a.string().required(),
|
|
145
|
-
targets: a.json().required(),
|
|
146
|
-
evidence: a.json(),
|
|
147
|
-
proposer: a.string().required(),
|
|
148
|
-
state: a.string().required(),
|
|
149
|
-
history: a.json().required(),
|
|
150
|
-
communityResult: a.json(),
|
|
151
|
-
pinned: a.boolean(),
|
|
152
|
-
held: a.boolean(),
|
|
153
|
-
override: a.json(),
|
|
154
|
-
exclusions: a.json(),
|
|
155
|
-
editorLog: a.json(),
|
|
156
|
-
needsLook: a.json(),
|
|
157
|
-
createdAt: a.datetime().required(),
|
|
158
|
-
})
|
|
159
|
-
.disableOperations(["subscriptions"])
|
|
160
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
161
|
-
.authorization(staffOnly),
|
|
162
|
-
ProposalReceipt: a.customType({
|
|
163
|
-
status: a.string().required(),
|
|
164
|
-
id: a.string(),
|
|
165
|
-
error: a.string(),
|
|
166
|
-
message: a.string(),
|
|
167
|
-
}),
|
|
168
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
169
|
-
StoryDraftSummary: a.customType({
|
|
170
|
-
id: a.string().required(),
|
|
171
|
-
title: a.string().required(),
|
|
172
|
-
summary: a.string(),
|
|
173
|
-
targets: a.json(),
|
|
174
|
-
state: a.string().required(),
|
|
175
|
-
createdAt: a.string().required(),
|
|
176
|
-
}),
|
|
177
|
-
StoryDraftList: a.customType({
|
|
178
|
-
status: a.string().required(),
|
|
179
|
-
id: a.string(),
|
|
180
|
-
error: a.string(),
|
|
181
|
-
message: a.string(),
|
|
182
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
183
|
-
}),
|
|
184
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
185
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
186
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
187
|
-
fileProposal: a
|
|
188
|
-
.mutation()
|
|
189
|
-
.arguments({
|
|
190
|
-
kind: a.string().required(),
|
|
191
|
-
title: a.string().required(),
|
|
192
|
-
body: a.string().required(),
|
|
193
|
-
summary: a.string(),
|
|
194
|
-
targets: a.json(),
|
|
195
|
-
evidence: a.json(),
|
|
196
|
-
})
|
|
197
|
-
.returns(a.ref("ProposalReceipt"))
|
|
198
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
199
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
200
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
201
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
202
|
-
transitionProposal: a
|
|
203
|
-
.mutation()
|
|
204
|
-
.arguments({
|
|
205
|
-
proposalId: a.string().required(),
|
|
206
|
-
state: a.string().required(),
|
|
207
|
-
reason: a.string(),
|
|
208
|
-
result: a.string(),
|
|
209
|
-
})
|
|
210
|
-
.returns(a.ref("ProposalReceipt"))
|
|
211
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
212
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
213
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
214
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
215
|
-
listStoryDrafts: a
|
|
216
|
-
.query()
|
|
217
|
-
.returns(a.ref("StoryDraftList"))
|
|
218
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
219
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
220
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
221
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
222
|
-
editProposal: a
|
|
223
|
-
.mutation()
|
|
224
|
-
.arguments({
|
|
225
|
-
proposalId: a.string().required(),
|
|
226
|
-
action: a.string().required(),
|
|
227
|
-
rule: a.string(),
|
|
228
|
-
exclusionId: a.string(),
|
|
229
|
-
})
|
|
230
|
-
.returns(a.ref("ProposalReceipt"))
|
|
231
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
232
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
233
138
|
ModerationReceipt: a.customType({
|
|
234
139
|
status: a.string().required(),
|
|
235
140
|
id: a.string(),
|
|
@@ -137,101 +137,6 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
137
137
|
.disableOperations(["subscriptions"])
|
|
138
138
|
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
139
139
|
.authorization(staffOnly),
|
|
140
|
-
Proposal: a
|
|
141
|
-
.model({
|
|
142
|
-
id: a.id().required(),
|
|
143
|
-
kind: a.string().required(),
|
|
144
|
-
title: a.string().required(),
|
|
145
|
-
summary: a.string(),
|
|
146
|
-
body: a.string().required(),
|
|
147
|
-
targets: a.json().required(),
|
|
148
|
-
evidence: a.json(),
|
|
149
|
-
proposer: a.string().required(),
|
|
150
|
-
state: a.string().required(),
|
|
151
|
-
history: a.json().required(),
|
|
152
|
-
communityResult: a.json(),
|
|
153
|
-
pinned: a.boolean(),
|
|
154
|
-
held: a.boolean(),
|
|
155
|
-
override: a.json(),
|
|
156
|
-
exclusions: a.json(),
|
|
157
|
-
editorLog: a.json(),
|
|
158
|
-
needsLook: a.json(),
|
|
159
|
-
createdAt: a.datetime().required(),
|
|
160
|
-
})
|
|
161
|
-
.disableOperations(["subscriptions"])
|
|
162
|
-
.secondaryIndexes((index) => [index("state").sortKeys(["createdAt"]).name("proposalsByState").queryField("listProposalsByStateAndCreatedAt")])
|
|
163
|
-
.authorization(staffOnly),
|
|
164
|
-
ProposalReceipt: a.customType({
|
|
165
|
-
status: a.string().required(),
|
|
166
|
-
id: a.string(),
|
|
167
|
-
error: a.string(),
|
|
168
|
-
message: a.string(),
|
|
169
|
-
}),
|
|
170
|
-
// What an operator may read of a story draft: these six fields and nothing else (no body, evidence, history, proposer, vote or log).
|
|
171
|
-
StoryDraftSummary: a.customType({
|
|
172
|
-
id: a.string().required(),
|
|
173
|
-
title: a.string().required(),
|
|
174
|
-
summary: a.string(),
|
|
175
|
-
targets: a.json(),
|
|
176
|
-
state: a.string().required(),
|
|
177
|
-
createdAt: a.string().required(),
|
|
178
|
-
}),
|
|
179
|
-
StoryDraftList: a.customType({
|
|
180
|
-
status: a.string().required(),
|
|
181
|
-
id: a.string(),
|
|
182
|
-
error: a.string(),
|
|
183
|
-
message: a.string(),
|
|
184
|
-
drafts: a.ref("StoryDraftSummary").array(),
|
|
185
|
-
}),
|
|
186
|
-
// Editors and admins file as themselves. AppSync also admits an IAM principal whose own policy allows the field (the
|
|
187
|
-
// schema accepts IAM callers for the functions that call it); the function files an AI proposal for an IAM principal
|
|
188
|
-
// of its own account only, and refuses an identity-pool user and any other account. No IAM rule is declared here.
|
|
189
|
-
fileProposal: a
|
|
190
|
-
.mutation()
|
|
191
|
-
.arguments({
|
|
192
|
-
kind: a.string().required(),
|
|
193
|
-
title: a.string().required(),
|
|
194
|
-
body: a.string().required(),
|
|
195
|
-
summary: a.string(),
|
|
196
|
-
targets: a.json(),
|
|
197
|
-
evidence: a.json(),
|
|
198
|
-
})
|
|
199
|
-
.returns(a.ref("ProposalReceipt"))
|
|
200
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
201
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
202
|
-
// The same IAM rule as fileProposal, narrower: an operator may move only draft to in_steering or declined.
|
|
203
|
-
// Approving, deciding after review and overriding are refused for IAM in the function (`operator_not_allowed`).
|
|
204
|
-
transitionProposal: a
|
|
205
|
-
.mutation()
|
|
206
|
-
.arguments({
|
|
207
|
-
proposalId: a.string().required(),
|
|
208
|
-
state: a.string().required(),
|
|
209
|
-
reason: a.string(),
|
|
210
|
-
result: a.string(),
|
|
211
|
-
})
|
|
212
|
-
.returns(a.ref("ProposalReceipt"))
|
|
213
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
214
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
215
|
-
// Read only, and the same caller rule as fileProposal: editors and admins, or an IAM principal of the function's own account. The function
|
|
216
|
-
// answers the story drafts' id, title, summary, targets, state and createdAt so an unattended trend run can dedupe; nothing else.
|
|
217
|
-
listStoryDrafts: a
|
|
218
|
-
.query()
|
|
219
|
-
.returns(a.ref("StoryDraftList"))
|
|
220
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
221
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
222
|
-
// Editors and admins only, and the function refuses an IAM operator: pin, unpin, hold, release, exclude_votes and
|
|
223
|
-
// undo_exclusion. `rule` is a batch rule for exclude_votes; `exclusionId` names the exclusion undo_exclusion reverses.
|
|
224
|
-
editProposal: a
|
|
225
|
-
.mutation()
|
|
226
|
-
.arguments({
|
|
227
|
-
proposalId: a.string().required(),
|
|
228
|
-
action: a.string().required(),
|
|
229
|
-
rule: a.string(),
|
|
230
|
-
exclusionId: a.string(),
|
|
231
|
-
})
|
|
232
|
-
.returns(a.ref("ProposalReceipt"))
|
|
233
|
-
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
234
|
-
.handler(functionHandler("readerActions", readerActions)),
|
|
235
140
|
ModerationReceipt: a.customType({
|
|
236
141
|
status: a.string().required(),
|
|
237
142
|
id: a.string(),
|
package/amplify/site-backend.js
CHANGED
|
@@ -24,6 +24,8 @@ import { defineSiteData } from "./data/resource.js";
|
|
|
24
24
|
import { editorialChecks } from "./functions/editorial-checks/resource.js";
|
|
25
25
|
import { automationView } from "./functions/automation-view/resource.js";
|
|
26
26
|
import { wireAutomationView } from "./functions/automation-view/wire.js";
|
|
27
|
+
import { proposalActions } from "./functions/proposal-actions/resource.js";
|
|
28
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire.js";
|
|
27
29
|
import { monitoringView } from "./functions/monitoring-view/resource.js";
|
|
28
30
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire.js";
|
|
29
31
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire.js";
|
|
@@ -110,6 +112,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
110
112
|
modelAttachmentUpload,
|
|
111
113
|
newsroomSummary,
|
|
112
114
|
procedureAction,
|
|
115
|
+
proposalActions,
|
|
113
116
|
publicForms,
|
|
114
117
|
readerSettings,
|
|
115
118
|
emailSubmissionProcessor,
|
|
@@ -631,6 +634,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
631
634
|
wireEditorialChecks(backend);
|
|
632
635
|
wireMonitoringView(backend, emailMonitorId);
|
|
633
636
|
wireAutomationView(backend);
|
|
637
|
+
wireProposalActions(backend);
|
|
634
638
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
635
639
|
return backend;
|
|
636
640
|
}
|
package/amplify/site-backend.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { defineSiteData } from "./data/resource";
|
|
|
31
31
|
import { editorialChecks } from "./functions/editorial-checks/resource";
|
|
32
32
|
import { automationView } from "./functions/automation-view/resource";
|
|
33
33
|
import { wireAutomationView } from "./functions/automation-view/wire";
|
|
34
|
+
import { proposalActions } from "./functions/proposal-actions/resource";
|
|
35
|
+
import { wireProposalActions } from "./functions/proposal-actions/wire";
|
|
34
36
|
import { monitoringView } from "./functions/monitoring-view/resource";
|
|
35
37
|
import { wireEmailReferenceIntake, wireMonitoringView } from "./functions/monitoring-view/wire";
|
|
36
38
|
import { wireEditorialChecks } from "./functions/editorial-checks/wire";
|
|
@@ -131,6 +133,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
131
133
|
modelAttachmentUpload,
|
|
132
134
|
newsroomSummary,
|
|
133
135
|
procedureAction,
|
|
136
|
+
proposalActions,
|
|
134
137
|
publicForms,
|
|
135
138
|
readerSettings,
|
|
136
139
|
emailSubmissionProcessor,
|
|
@@ -777,6 +780,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
777
780
|
wireEditorialChecks(backend);
|
|
778
781
|
wireMonitoringView(backend, emailMonitorId);
|
|
779
782
|
wireAutomationView(backend);
|
|
783
|
+
wireProposalActions(backend);
|
|
780
784
|
wirePlugins(backend, plugins, contentActionsLambda);
|
|
781
785
|
|
|
782
786
|
return backend;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleCliLoginDiscovery } from "../../../lib/cli-login-discovery";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleCliLoginDiscovery(request);
|
|
9
|
+
}
|
package/app/globals.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@import "./papyrus-theme.css";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/* Tailwind (import, @source globs, @theme tokens) lives only in ./tailwind.css. Importing it again here
|
|
4
|
+
emitted a second full Tailwind build that loaded later and whose `.hidden` overrode `md:flex`. */
|
|
5
5
|
@import "@radix-ui/colors/slate.css";
|
|
6
6
|
@import "@radix-ui/colors/slate-alpha.css";
|
|
7
7
|
@import "@radix-ui/colors/slate-dark.css";
|
|
@@ -15,39 +15,6 @@
|
|
|
15
15
|
@import "@radix-ui/colors/tomato-dark.css";
|
|
16
16
|
@import "@radix-ui/colors/tomato-dark-alpha.css";
|
|
17
17
|
|
|
18
|
-
@theme inline {
|
|
19
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
20
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
21
|
-
--radius-lg: var(--radius);
|
|
22
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
23
|
-
--color-background: var(--background);
|
|
24
|
-
--color-foreground: var(--foreground);
|
|
25
|
-
--color-card: var(--card);
|
|
26
|
-
--color-card-foreground: var(--card-foreground);
|
|
27
|
-
--color-popover: var(--popover);
|
|
28
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
29
|
-
--color-primary: var(--primary);
|
|
30
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
31
|
-
--color-secondary: var(--secondary);
|
|
32
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
-
--color-muted: var(--muted);
|
|
34
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
35
|
-
--color-accent: var(--accent);
|
|
36
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
37
|
-
--color-destructive: var(--destructive);
|
|
38
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
39
|
-
--color-border: var(--border);
|
|
40
|
-
--color-input: var(--input);
|
|
41
|
-
--color-ring: var(--ring);
|
|
42
|
-
--color-danger: var(--danger);
|
|
43
|
-
--color-danger-foreground: var(--danger-foreground);
|
|
44
|
-
--color-surface: var(--surface);
|
|
45
|
-
--color-surface-foreground: var(--surface-foreground);
|
|
46
|
-
--color-foreground-strong: var(--foreground-strong);
|
|
47
|
-
--color-foreground-muted: var(--foreground-muted);
|
|
48
|
-
--color-foreground-subtle: var(--foreground-subtle);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
18
|
:root {
|
|
52
19
|
--radius: 0.875rem;
|
|
53
20
|
--background: var(--slate-2);
|
|
@@ -10170,3 +10137,32 @@ img {
|
|
|
10170
10137
|
}
|
|
10171
10138
|
}
|
|
10172
10139
|
|
|
10140
|
+
|
|
10141
|
+
@media (max-width: 900px) {
|
|
10142
|
+
.edition-progress__button--console { display: none; }
|
|
10143
|
+
.papyrus-console--open {
|
|
10144
|
+
--papyrus-console-width: 100vw;
|
|
10145
|
+
background: rgb(var(--paper-rgb));
|
|
10146
|
+
height: 100dvh;
|
|
10147
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
10148
|
+
box-sizing: border-box;
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
10151
|
+
|
|
10152
|
+
.papyrus-console__access {
|
|
10153
|
+
display: flex;
|
|
10154
|
+
flex-direction: column;
|
|
10155
|
+
align-items: flex-start;
|
|
10156
|
+
gap: 16px;
|
|
10157
|
+
padding: 24px 18px;
|
|
10158
|
+
overflow-y: auto;
|
|
10159
|
+
color: var(--ink);
|
|
10160
|
+
font-size: 0.95rem;
|
|
10161
|
+
line-height: 1.5;
|
|
10162
|
+
}
|
|
10163
|
+
.papyrus-console__access strong { font-size: 1.15rem; }
|
|
10164
|
+
.papyrus-console__access button { min-height: 44px; padding: 10px 14px; font: inherit; text-transform: none; letter-spacing: normal; }
|
|
10165
|
+
|
|
10166
|
+
@media (max-width: 900px) {
|
|
10167
|
+
.papyrus-console__header button { min-width: 44px; min-height: 44px; }
|
|
10168
|
+
}
|
package/app/tailwind.css
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
--color-accent: var(--accent);
|
|
25
25
|
--color-accent-foreground: var(--accent-foreground);
|
|
26
26
|
--color-destructive: var(--destructive);
|
|
27
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
28
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
27
29
|
--color-danger: var(--danger);
|
|
28
30
|
--color-danger-foreground: var(--danger-foreground);
|
|
29
31
|
--color-surface: var(--surface);
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
|
|
4
4
|
* papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
|
|
5
5
|
* papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
|
|
6
|
+
* papyrus-infra synth --site infra/site.json --part chat [--chat-lambdas DIR] [--out cdk.out] (the private chat data stack)
|
|
6
7
|
* papyrus-infra deploy --site infra/site.json [--only-branch main] [--only-app-id ID] [--out cdk.out] [--account A] [--region R]
|
|
7
8
|
* Synthesizes the app-shell stack for a publication from its own site.json,
|
|
8
9
|
* prints one of the generated Amplify build specs, or deploys the stack from
|
|
@@ -16,6 +17,7 @@ import { resolveCdkEnvironment } from "../src/cdk-environment.mjs";
|
|
|
16
17
|
|
|
17
18
|
const USAGE = [
|
|
18
19
|
"Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
|
|
20
|
+
" papyrus-infra synth --site infra/site.json --part chat [--chat-lambdas DIR] [--out cdk.out] [--account A] [--region R]",
|
|
19
21
|
" papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
|
|
20
22
|
" papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
|
|
21
23
|
" papyrus-infra deploy-edge --stack NAME --template FILE [--adoption-template FILE --adopt-resources FILE] [--parameter KEY=VALUE]... [--delete-function NAME] [--only-branch main] [--only-app-id ID] [--region R]",
|
|
@@ -186,8 +188,9 @@ if (command === "deploy") {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
let AmplifyAppShellStack;
|
|
191
|
+
let PapyrusChatStack;
|
|
189
192
|
try {
|
|
190
|
-
({ AmplifyAppShellStack } = await import("../infra/index.js"));
|
|
193
|
+
({ AmplifyAppShellStack, PapyrusChatStack } = await import("../infra/index.js"));
|
|
191
194
|
} catch (error) {
|
|
192
195
|
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
193
196
|
process.exit(1);
|
|
@@ -195,6 +198,23 @@ try {
|
|
|
195
198
|
|
|
196
199
|
const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
|
|
197
200
|
const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
|
|
201
|
+
if (opt("part", "shell") === "chat") {
|
|
202
|
+
// The site's private newsroom chat data stack (docs/private-newsroom-chat-per-site.md); deployed before the app shell enables chat.
|
|
203
|
+
if (!config.chat?.enabled) {
|
|
204
|
+
console.error(`papyrus-infra: site ${config.siteId} has no enabled chat block`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const lambdaAssetDirectory = opt("chat-lambdas");
|
|
208
|
+
const chatStack = new PapyrusChatStack(app, {
|
|
209
|
+
config,
|
|
210
|
+
env: resolveEnvironmentOrExit(),
|
|
211
|
+
lambdaAssetDirectory: lambdaAssetDirectory ? path.resolve(lambdaAssetDirectory) : undefined,
|
|
212
|
+
description: `Private Papyrus newsroom chat data plane for ${config.siteId}.`,
|
|
213
|
+
});
|
|
214
|
+
app.synth();
|
|
215
|
+
console.log(`synthesized ${chatStack.stackName} -> ${path.resolve(opt("out", "cdk.out"))}`);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
198
218
|
new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
|
|
199
219
|
env: resolveEnvironmentOrExit(),
|
|
200
220
|
stackName: resolveStackName(config),
|