@anthusai/papyrus 5.19.1 → 5.21.0
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/resource.js +1 -0
- package/amplify/data/resource.ts +1 -0
- package/amplify/data/schema.js +1 -0
- package/amplify/data/schema.ts +1 -0
- package/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/content-actions/handler.py +20 -2
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/plugins/newsletter/config.js +195 -0
- package/amplify/plugins/newsletter/config.ts +240 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +260 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +308 -0
- package/amplify/plugins/newsletter/schema.js +56 -0
- package/amplify/plugins/newsletter/schema.ts +61 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +140 -0
- package/amplify/plugins/newsletter/sending.ts +186 -0
- package/amplify/plugins/reader-steering/config.js +162 -0
- package/amplify/plugins/reader-steering/config.ts +248 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +230 -0
- package/amplify/plugins/reader-steering/function/handler.py +1363 -0
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/resource.js +27 -0
- package/amplify/plugins/reader-steering/function/resource.ts +35 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +175 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +201 -0
- package/amplify/plugins/reader-steering/schema.js +140 -0
- package/amplify/plugins/reader-steering/schema.ts +142 -0
- package/amplify/plugins/registry.js +4 -0
- package/amplify/plugins/registry.ts +4 -0
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/handle/route.ts +9 -0
- package/app/api/reader/me/route.ts +9 -0
- package/app/api/reader/rating/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/retired/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/app/api/reader/vote/route.ts +9 -0
- package/components/moderation-panel.tsx +248 -0
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-organizer.css +14 -0
- package/components/reference-cyclotron-priming.tsx +54 -15
- package/components/topic-steering-workspace.tsx +42 -5
- package/infra/amplify-app-shell.js +15 -1
- package/infra/amplify-app-shell.ts +17 -1
- package/infra/site-config.js +12 -2
- package/infra/site-config.ts +19 -2
- package/lib/editorial-checks.ts +6 -2
- package/lib/newsletter-api.ts +391 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/newsletter-pages.ts +264 -0
- package/lib/prospectus-session.ts +1 -0
- package/lib/prospectus-sign-in-routes.ts +2 -2
- package/lib/prospectus-site-identity.ts +24 -1
- package/lib/reader-api.ts +145 -0
- package/lib/reader-moderation.ts +220 -0
- package/lib/reader-retirement-route.ts +89 -0
- package/lib/reader-subject.md +29 -0
- package/lib/reader-subject.ts +53 -0
- package/package.json +27 -1
- package/routes.manifest.json +158 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useId, useRef, useState, type KeyboardEvent } from "react";
|
|
4
4
|
import { primingStateText, type PrimingRequestView } from "../lib/relevance-status";
|
|
5
5
|
|
|
6
6
|
export type PrimingFormInput = { url: string; label: string; explanation: string };
|
|
@@ -23,6 +23,19 @@ export function ReferenceCyclotronPriming({
|
|
|
23
23
|
const [explanation, setExplanation] = useState("");
|
|
24
24
|
const [message, setMessage] = useState<string | null>(null);
|
|
25
25
|
|
|
26
|
+
const ratingLabelId = useId();
|
|
27
|
+
const buttons = useRef<Array<HTMLButtonElement | null>>([]);
|
|
28
|
+
|
|
29
|
+
function moveSelection(event: KeyboardEvent<HTMLButtonElement>, index: number) {
|
|
30
|
+
const forward = event.key === "ArrowRight" || event.key === "ArrowDown";
|
|
31
|
+
const backward = event.key === "ArrowLeft" || event.key === "ArrowUp";
|
|
32
|
+
if (!forward && !backward) return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const next = (index + (forward ? 1 : -1) + labels.length) % labels.length;
|
|
35
|
+
setLabel(labels[next]);
|
|
36
|
+
buttons.current[next]?.focus();
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
async function submit() {
|
|
27
40
|
setMessage("Adding…");
|
|
28
41
|
try {
|
|
@@ -38,13 +51,13 @@ export function ReferenceCyclotronPriming({
|
|
|
38
51
|
return (
|
|
39
52
|
<div className="space-y-2 text-sm" data-reference-cyclotron-priming>
|
|
40
53
|
<form
|
|
41
|
-
className="flex flex-
|
|
54
|
+
className="flex flex-col gap-3"
|
|
42
55
|
onSubmit={(event) => {
|
|
43
56
|
event.preventDefault();
|
|
44
57
|
void submit();
|
|
45
58
|
}}
|
|
46
59
|
>
|
|
47
|
-
<label className="flex
|
|
60
|
+
<label className="flex flex-col gap-1">
|
|
48
61
|
<span className="text-xs text-muted-foreground">Web page address</span>
|
|
49
62
|
<input
|
|
50
63
|
aria-label="Reference URL"
|
|
@@ -55,20 +68,46 @@ export function ReferenceCyclotronPriming({
|
|
|
55
68
|
value={url}
|
|
56
69
|
/>
|
|
57
70
|
</label>
|
|
71
|
+
<div className="flex flex-col gap-1">
|
|
72
|
+
<span className="text-xs text-muted-foreground" id={ratingLabelId}>Should Papyrus include or exclude pages like this?</span>
|
|
73
|
+
<div aria-labelledby={ratingLabelId} className="flex flex-wrap gap-2" role="radiogroup">
|
|
74
|
+
{labels.map((choice, index) => {
|
|
75
|
+
const checked = choice === label;
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
aria-checked={checked}
|
|
79
|
+
className={`min-w-32 flex-1 rounded border px-4 py-3 text-base font-semibold capitalize ${checked ? "border-foreground bg-foreground text-background" : "border-border bg-transparent"}`}
|
|
80
|
+
data-rating-choice={choice}
|
|
81
|
+
key={choice}
|
|
82
|
+
onClick={() => setLabel(choice)}
|
|
83
|
+
onKeyDown={(event) => moveSelection(event, index)}
|
|
84
|
+
ref={(node) => { buttons.current[index] = node; }}
|
|
85
|
+
role="radio"
|
|
86
|
+
tabIndex={checked || (!labels.includes(label) && index === 0) ? 0 : -1}
|
|
87
|
+
type="button"
|
|
88
|
+
>
|
|
89
|
+
{choice}
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
58
95
|
<label className="flex flex-col gap-1">
|
|
59
|
-
<span className="text-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
))}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
96
|
+
<span className="text-sm font-medium">Why?</span>
|
|
97
|
+
<textarea
|
|
98
|
+
aria-label="Reason"
|
|
99
|
+
className="w-full"
|
|
100
|
+
onChange={(event) => setExplanation(event.target.value)}
|
|
101
|
+
placeholder="Why? A sentence or two helps Papyrus learn what you want."
|
|
102
|
+
rows={4}
|
|
103
|
+
value={explanation}
|
|
104
|
+
/>
|
|
105
|
+
<span className="text-xs text-muted-foreground">Optional, but a short reason helps Papyrus learn what you want.</span>
|
|
69
106
|
</label>
|
|
70
|
-
<
|
|
71
|
-
|
|
107
|
+
<div>
|
|
108
|
+
<button className="cyclotron-review-secondary" type="submit">Add reference</button>
|
|
109
|
+
</div>
|
|
110
|
+
{message ? <p className="text-xs" role="status">{message}</p> : null}
|
|
72
111
|
</form>
|
|
73
112
|
{requests.length ? (
|
|
74
113
|
<ul aria-label="Added references" className="space-y-1 text-xs text-muted-foreground">
|
|
@@ -15,6 +15,8 @@ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useSta
|
|
|
15
15
|
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
16
16
|
import { EditorialChecksAdminPanel } from "./editorial-checks-admin-panel";
|
|
17
17
|
import { MonitoringPanel } from "./monitoring-panel";
|
|
18
|
+
import { ModerationPanel } from "./moderation-panel";
|
|
19
|
+
import { NewsletterPanel } from "./newsletter-panel";
|
|
18
20
|
import { SignupEntriesPanel } from "./signup-entries-panel";
|
|
19
21
|
import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
|
|
20
22
|
import { generateClient } from "aws-amplify/data";
|
|
@@ -249,7 +251,7 @@ type AssignmentAction = "claim" | "release" | "complete" | "cancel" | "reopen" |
|
|
|
249
251
|
type ReportingPacketReviewDecision = "select" | "merge" | "brief" | "hold" | "kill";
|
|
250
252
|
type AssignmentDeskViewMode = "queue" | "budget";
|
|
251
253
|
type UserRoleAction = "grant" | "revoke";
|
|
252
|
-
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups" | "editorial-checks" | "monitoring";
|
|
254
|
+
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups" | "newsletter" | "moderation" | "editorial-checks" | "monitoring";
|
|
253
255
|
export type NewsDeskTab = "overview" | "desks" | "topics" | "concepts" | "references" | "messages" | "assignments" | "administration" | "search";
|
|
254
256
|
type LexicalRuleScope = "publication" | "corpus" | "classifier" | "category";
|
|
255
257
|
type AnalysisReindexMode = "online-update" | "classifier-retrain" | "scoped-topic-rebuild" | "entity-graph-rebuild" | "generated-analysis-rebuild";
|
|
@@ -881,7 +883,7 @@ function NewsDeskDashboard({
|
|
|
881
883
|
const [mergeSelection, setMergeSelection] = useState<MergeSelection | null>(null);
|
|
882
884
|
const [isPending, startTransition] = useTransition();
|
|
883
885
|
const controlsDisabled = isPending || !canEdit;
|
|
884
|
-
const initialAdministrationPanel = normalizeAdministrationPanel(initialSelection.panel);
|
|
886
|
+
const initialAdministrationPanel = normalizeAdministrationPanel(initialSelection.panel, Boolean(dashboard.isDemo));
|
|
885
887
|
const [administrationPanel, setAdministrationPanel] = useState<AdministrationPanel>(initialAdministrationPanel);
|
|
886
888
|
const [doctrineDrafts, setDoctrineDrafts] = useState<DoctrineEditorState>(() => buildDoctrineEditorState(dashboard.doctrineRecords));
|
|
887
889
|
const [loadedSections, setLoadedSections] = useState<Record<string, boolean>>({
|
|
@@ -1122,8 +1124,8 @@ function NewsDeskDashboard({
|
|
|
1122
1124
|
}, [dashboard.procedureDefinitions, dashboard.procedureRuns, dashboard.procedureVersions]);
|
|
1123
1125
|
|
|
1124
1126
|
useEffect(() => {
|
|
1125
|
-
setAdministrationPanel(normalizeAdministrationPanel(initialSelection.panel));
|
|
1126
|
-
}, [initialSelection.panel]);
|
|
1127
|
+
setAdministrationPanel(normalizeAdministrationPanel(initialSelection.panel, Boolean(dashboard.isDemo)));
|
|
1128
|
+
}, [initialSelection.panel, dashboard.isDemo]);
|
|
1127
1129
|
|
|
1128
1130
|
useEffect(() => {
|
|
1129
1131
|
if (activeTab !== "administration") return;
|
|
@@ -4351,9 +4353,33 @@ function AdministrationDeskView({
|
|
|
4351
4353
|
<span>Addresses from the site's signup forms</span>
|
|
4352
4354
|
</Link>
|
|
4353
4355
|
) : null}
|
|
4356
|
+
{newsletterPluginEnabled(isDemo) ? (
|
|
4357
|
+
<Link
|
|
4358
|
+
href={administrationPanelHref("newsletter", isDemo)}
|
|
4359
|
+
data-news-desk-admin-nav="newsletter"
|
|
4360
|
+
data-active={administrationPanel === "newsletter" || undefined}
|
|
4361
|
+
onClick={() => onPanelChange("newsletter")}
|
|
4362
|
+
>
|
|
4363
|
+
<strong>Newsletter</strong>
|
|
4364
|
+
<span>Subscribers, issues and delivery for the site's email</span>
|
|
4365
|
+
</Link>
|
|
4366
|
+
) : null}
|
|
4367
|
+
{readerPluginEnabled() ? (
|
|
4368
|
+
<Link
|
|
4369
|
+
href={administrationPanelHref("moderation", isDemo)}
|
|
4370
|
+
data-news-desk-admin-nav="moderation"
|
|
4371
|
+
data-active={administrationPanel === "moderation" || undefined}
|
|
4372
|
+
onClick={() => onPanelChange("moderation")}
|
|
4373
|
+
>
|
|
4374
|
+
<strong>Moderation</strong>
|
|
4375
|
+
<span>Reader submissions and comments</span>
|
|
4376
|
+
</Link>
|
|
4377
|
+
) : null}
|
|
4354
4378
|
</nav>}
|
|
4355
4379
|
<div className="news-desk-settings-panel">
|
|
4356
4380
|
{administrationPanel === "signups" ? <SignupEntriesPanel /> : null}
|
|
4381
|
+
{administrationPanel === "newsletter" ? <NewsletterPanel isAdmin={canManageUsers} isDemo={isDemo} /> : null}
|
|
4382
|
+
{administrationPanel === "moderation" ? <ModerationPanel /> : null}
|
|
4357
4383
|
{administrationPanel === "editorial-checks" ? <EditorialChecksAdminPanel /> : null}
|
|
4358
4384
|
{administrationPanel === "monitoring" ? <MonitoringPanel isAdmin={canManageUsers} isDemo={isDemo} /> : null}
|
|
4359
4385
|
{administrationPanel === "delegations" ? <AdministrationDelegationsPanel disabled={disabled} /> : null}
|
|
@@ -12819,9 +12845,20 @@ function signupPluginEnabled(): boolean {
|
|
|
12819
12845
|
return pluginSettings(amplifyOutputs as PluginOutputs, "signup") !== null;
|
|
12820
12846
|
}
|
|
12821
12847
|
|
|
12822
|
-
|
|
12848
|
+
/** The Newsletter panel exists when the site enabled the plugin; `?demo=1` shows it with fixture data so it can be seen and tested. */
|
|
12849
|
+
function newsletterPluginEnabled(demo = false): boolean {
|
|
12850
|
+
return demo || pluginSettings(amplifyOutputs as PluginOutputs, "newsletter") !== null;
|
|
12851
|
+
}
|
|
12852
|
+
|
|
12853
|
+
function readerPluginEnabled(): boolean {
|
|
12854
|
+
return pluginSettings(amplifyOutputs as PluginOutputs, "reader-steering") !== null;
|
|
12855
|
+
}
|
|
12856
|
+
|
|
12857
|
+
function normalizeAdministrationPanel(value: string | null | undefined, demo = false): AdministrationPanel {
|
|
12823
12858
|
if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations" || value === "editorial-checks" || value === "monitoring") return value;
|
|
12824
12859
|
if (value === "signups" && signupPluginEnabled()) return value;
|
|
12860
|
+
if (value === "newsletter" && newsletterPluginEnabled(demo)) return value;
|
|
12861
|
+
if (value === "moderation" && readerPluginEnabled()) return value;
|
|
12825
12862
|
return "users";
|
|
12826
12863
|
}
|
|
12827
12864
|
|
|
@@ -4,7 +4,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
4
4
|
import { SecureStringParameter } from "./secure-string-parameter.js";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
7
|
-
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
|
+
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveReaderPollSecretParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
8
8
|
function environmentVariables(variables) {
|
|
9
9
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
10
10
|
}
|
|
@@ -123,6 +123,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
123
123
|
resources: [prospectusSessionSealing.parameterArn],
|
|
124
124
|
}));
|
|
125
125
|
}
|
|
126
|
+
if (prospectusAccountsApiStageArn && config.cms.readerRetirementPoll) {
|
|
127
|
+
// Prospectus admits one caller per site, this compute role, so the scheduled snapshot run reaches the retired-subject
|
|
128
|
+
// route through the CMS route /api/reader/retired, which it authenticates with this secret.
|
|
129
|
+
const readerPollSecret = new SecureStringParameter(this, "ReaderPollSecretParameter", {
|
|
130
|
+
parameterName: resolveReaderPollSecretParameterName(config),
|
|
131
|
+
description: `Authorizes the scheduled reader snapshot run to call the retired-subject route of ${cmsAppName}`,
|
|
132
|
+
});
|
|
133
|
+
prospectusEnvironment.PAPYRUS_READER_POLL_SECRET_PARAMETER = readerPollSecret.parameterName;
|
|
134
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
135
|
+
sid: "ReadReaderPollSecret",
|
|
136
|
+
actions: ["ssm:GetParameter"],
|
|
137
|
+
resources: [readerPollSecret.parameterArn],
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
126
140
|
if (prospectusAccountsApiStageArn) {
|
|
127
141
|
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
128
142
|
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
@@ -5,7 +5,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
5
5
|
import { SecureStringParameter } from "./secure-string-parameter";
|
|
6
6
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
7
7
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
8
|
-
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
|
+
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveSiteOrigin, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveProspectusSessionSealingParameterName, resolveReaderPollSecretParameterName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
9
9
|
|
|
10
10
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
11
11
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
@@ -136,6 +136,22 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
136
136
|
}),
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
|
+
if (prospectusAccountsApiStageArn && config.cms.readerRetirementPoll) {
|
|
140
|
+
// Prospectus admits one caller per site, this compute role, so the scheduled snapshot run reaches the retired-subject
|
|
141
|
+
// route through the CMS route /api/reader/retired, which it authenticates with this secret.
|
|
142
|
+
const readerPollSecret = new SecureStringParameter(this, "ReaderPollSecretParameter", {
|
|
143
|
+
parameterName: resolveReaderPollSecretParameterName(config),
|
|
144
|
+
description: `Authorizes the scheduled reader snapshot run to call the retired-subject route of ${cmsAppName}`,
|
|
145
|
+
});
|
|
146
|
+
prospectusEnvironment.PAPYRUS_READER_POLL_SECRET_PARAMETER = readerPollSecret.parameterName;
|
|
147
|
+
computeRole.addToPolicy(
|
|
148
|
+
new iam.PolicyStatement({
|
|
149
|
+
sid: "ReadReaderPollSecret",
|
|
150
|
+
actions: ["ssm:GetParameter"],
|
|
151
|
+
resources: [readerPollSecret.parameterArn],
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
139
155
|
if (prospectusAccountsApiStageArn) {
|
|
140
156
|
computeRole.addToPolicy(
|
|
141
157
|
new iam.PolicyStatement({
|
package/infra/site-config.js
CHANGED
|
@@ -35,7 +35,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
35
35
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
36
36
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
37
37
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
38
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_SITE_ORIGIN"];
|
|
38
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
39
39
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
40
40
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
41
41
|
"GET/v1/identities/me",
|
|
@@ -63,6 +63,9 @@ function originOfRedirectUrl(value) {
|
|
|
63
63
|
export function resolveProspectusSessionSealingParameterName(config) {
|
|
64
64
|
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
65
65
|
}
|
|
66
|
+
export function resolveReaderPollSecretParameterName(config) {
|
|
67
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
68
|
+
}
|
|
66
69
|
export function resolveRevalidateSecretParameterName(config) {
|
|
67
70
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
68
71
|
}
|
|
@@ -222,7 +225,7 @@ function parseStagingPreview(value) {
|
|
|
222
225
|
}
|
|
223
226
|
function parseCms(value) {
|
|
224
227
|
const cmsRecord = requireObject(value, "cms");
|
|
225
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
228
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
226
229
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
227
230
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
228
231
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -233,6 +236,12 @@ function parseCms(value) {
|
|
|
233
236
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
234
237
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
235
238
|
}
|
|
239
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
240
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
241
|
+
}
|
|
242
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
243
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
244
|
+
}
|
|
236
245
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
237
246
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
238
247
|
}
|
|
@@ -250,6 +259,7 @@ function parseCms(value) {
|
|
|
250
259
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
251
260
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
252
261
|
deployStackInBuild: cmsRecord.deployStackInBuild,
|
|
262
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll,
|
|
253
263
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
254
264
|
prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
|
|
255
265
|
};
|
package/infra/site-config.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type CmsConfig = {
|
|
|
44
44
|
stagingDomainPrefix?: string;
|
|
45
45
|
redirects?: CmsRedirect[];
|
|
46
46
|
prospectusAccountsApiStageArn?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The site is a public-reader site that polls Prospectus for retired reader subjects. The template then generates a
|
|
49
|
+
* shared secret that the scheduled snapshot run presents to the CMS route `/api/reader/retired` (requires
|
|
50
|
+
* `prospectusAccountsApiStageArn`; docs/reader-steering.md).
|
|
51
|
+
*/
|
|
52
|
+
readerRetirementPoll?: boolean;
|
|
47
53
|
deployStackInBuild?: boolean;
|
|
48
54
|
};
|
|
49
55
|
|
|
@@ -146,7 +152,7 @@ const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
|
146
152
|
const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
147
153
|
const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
|
|
148
154
|
const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
|
|
149
|
-
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_SITE_ORIGIN"];
|
|
155
|
+
const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT", "PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_SITE_PRODUCT", "PAPYRUS_READER_POLL_SECRET_PARAMETER", "PAPYRUS_SITE_ORIGIN"];
|
|
150
156
|
|
|
151
157
|
/** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
|
|
152
158
|
export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
|
|
@@ -177,6 +183,10 @@ export function resolveProspectusSessionSealingParameterName(config: Pick<Amplif
|
|
|
177
183
|
return `/papyrus/${config.siteId}/prospectus-session-sealing`;
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
export function resolveReaderPollSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
187
|
+
return `/papyrus/${config.siteId}/reader-poll-secret`;
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
export function resolveRevalidateSecretParameterName(config: Pick<AmplifyAppShellSiteConfig, "siteId">): string {
|
|
181
191
|
return `/papyrus/${config.siteId}/revalidate-secret`;
|
|
182
192
|
}
|
|
@@ -336,7 +346,7 @@ function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
|
336
346
|
|
|
337
347
|
function parseCms(value: unknown): CmsConfig {
|
|
338
348
|
const cmsRecord = requireObject(value, "cms");
|
|
339
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "deployStackInBuild"], "cms");
|
|
349
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "siteOrigin", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn", "readerRetirementPoll", "deployStackInBuild"], "cms");
|
|
340
350
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
341
351
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
342
352
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -347,6 +357,12 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
347
357
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
348
358
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
349
359
|
}
|
|
360
|
+
if (cmsRecord.readerRetirementPoll !== undefined && typeof cmsRecord.readerRetirementPoll !== "boolean") {
|
|
361
|
+
fail("cms.readerRetirementPoll", `must be a boolean, got ${JSON.stringify(cmsRecord.readerRetirementPoll)}`);
|
|
362
|
+
}
|
|
363
|
+
if (cmsRecord.readerRetirementPoll === true && cmsRecord.prospectusAccountsApiStageArn === undefined) {
|
|
364
|
+
fail("cms.readerRetirementPoll", "requires cms.prospectusAccountsApiStageArn: the poll calls Prospectus as the site");
|
|
365
|
+
}
|
|
350
366
|
if (cmsRecord.deployStackInBuild !== undefined && typeof cmsRecord.deployStackInBuild !== "boolean") {
|
|
351
367
|
fail("cms.deployStackInBuild", `must be a boolean, got ${JSON.stringify(cmsRecord.deployStackInBuild)}`);
|
|
352
368
|
}
|
|
@@ -369,6 +385,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
369
385
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
370
386
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
371
387
|
deployStackInBuild: cmsRecord.deployStackInBuild as boolean | undefined,
|
|
388
|
+
readerRetirementPoll: cmsRecord.readerRetirementPoll as boolean | undefined,
|
|
372
389
|
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
373
390
|
prospectusAccountsApiStageArn: optionalString(
|
|
374
391
|
cmsRecord.prospectusAccountsApiStageArn,
|
package/lib/editorial-checks.ts
CHANGED
|
@@ -38,7 +38,8 @@ export type EditorialSegment = {
|
|
|
38
38
|
text?: string;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** "skipped": Papyrus did not ask for judgments because the publication has no mission or policy for them to apply. */
|
|
42
|
+
export type LaneStatus = { status: "ran" | "disabled" | "unavailable" | "failed" | "partial" | "skipped"; reason?: string; detectors?: number };
|
|
42
43
|
|
|
43
44
|
export type EditorialCheckResult = {
|
|
44
45
|
schemaVersion: 1;
|
|
@@ -49,7 +50,8 @@ export type EditorialCheckResult = {
|
|
|
49
50
|
protected: { kind: string; start: number; end: number }[];
|
|
50
51
|
findings: EditorialFinding[];
|
|
51
52
|
coverage: unknown[];
|
|
52
|
-
lanes
|
|
53
|
+
/** lanes.section is "skipped" when the story's section has no NewsroomSection record to take a mission and policy from. */
|
|
54
|
+
lanes: { rules: LaneStatus; classifier: LaneStatus; section?: LaneStatus };
|
|
53
55
|
usage?: Record<string, number>;
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -164,8 +166,10 @@ export function describeProbability(finding: EditorialFinding): string | null {
|
|
|
164
166
|
|
|
165
167
|
export function laneNotice(result: EditorialCheckResult): string | null {
|
|
166
168
|
const classifier = result.lanes.classifier;
|
|
169
|
+
if (classifier.status === "skipped") return `Mission-based checks skipped: ${classifier.reason ?? "this publication has no mission yet"}.`;
|
|
167
170
|
if (classifier.status === "partial") return `Some sentences were not judged: ${classifier.reason ?? "the check ran out of time"}.`;
|
|
168
171
|
if (classifier.status === "failed") return `The judgment checks failed: ${classifier.reason ?? "unknown error"}. Rule checks still ran.`;
|
|
169
172
|
if (classifier.status === "unavailable" && (classifier.detectors ?? 0) === 0 && classifier.reason) return `Judgment checks are not configured: ${classifier.reason}.`;
|
|
173
|
+
if (result.lanes.section?.status === "skipped") return `Section-based checks skipped: ${result.lanes.section.reason ?? "no section configuration"}.`;
|
|
170
174
|
return null;
|
|
171
175
|
}
|