@anthusai/papyrus 2.0.0-next.7 → 2.0.0-next.71
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 +23 -3
- package/amplify/data/resource.ts +25 -3
- package/amplify/data/schema.js +2028 -1979
- package/amplify/data/schema.ts +72 -14
- package/amplify/functions/assignment-action/data-access.js +90 -0
- package/amplify/functions/assignment-action/data-access.ts +89 -0
- package/amplify/functions/assignment-action/handler.ts +1 -1
- package/amplify/functions/content-actions/handler.py +61 -2
- package/amplify/functions/content-actions/requirements.txt +1 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/handler.py +3 -3
- package/amplify/functions/editorial-checks/requirements.txt +1 -0
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.js +7 -7
- package/amplify/{plugins/editorial-checks/function → functions/editorial-checks}/resource.ts +7 -7
- package/amplify/functions/editorial-checks/schema.js +121 -0
- package/amplify/{plugins → functions}/editorial-checks/schema.ts +33 -6
- package/amplify/functions/editorial-checks/wire.js +53 -0
- package/amplify/functions/editorial-checks/wire.ts +61 -0
- package/amplify/functions/monitoring-view/handler.py +11 -0
- package/amplify/functions/monitoring-view/resource.js +24 -0
- package/amplify/functions/monitoring-view/resource.ts +34 -0
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +18 -0
- package/amplify/functions/monitoring-view/wire.js +37 -0
- package/amplify/functions/monitoring-view/wire.ts +47 -0
- package/amplify/functions/public-forms/core.js +247 -0
- package/amplify/functions/public-forms/core.ts +310 -0
- package/amplify/functions/public-forms/handler.ts +41 -0
- package/amplify/functions/public-forms/origins.js +35 -0
- package/amplify/functions/public-forms/origins.ts +43 -0
- package/amplify/functions/public-forms/resource.js +9 -0
- package/amplify/functions/public-forms/resource.ts +10 -0
- package/amplify/functions/public-forms/wire.js +25 -0
- package/amplify/functions/public-forms/wire.ts +32 -0
- package/amplify/functions/shared/editorial-context.ts +63 -4
- package/amplify/functions/shared/python-bundle.js +20 -14
- package/amplify/functions/shared/python-bundle.ts +25 -18
- package/amplify/plugins/newsletter/config.js +245 -0
- package/amplify/plugins/newsletter/config.ts +305 -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 +263 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -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 +180 -0
- package/amplify/plugins/reader-steering/config.ts +280 -0
- package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
- package/amplify/plugins/reader-steering/function/handler.py +1554 -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/proposals.py +255 -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 +184 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +210 -0
- package/amplify/plugins/reader-steering/schema.js +204 -0
- package/amplify/plugins/reader-steering/schema.ts +206 -0
- package/amplify/plugins/registry.js +4 -2
- package/amplify/plugins/registry.ts +4 -2
- package/amplify/plugins/types.ts +5 -0
- package/amplify/plugins/wire.js +1 -0
- package/amplify/plugins/wire.ts +1 -0
- package/amplify/publication/projection-lease.js +30 -0
- package/amplify/publication/projection-lease.ts +39 -0
- package/amplify/publication/redirect-and-index.js +105 -0
- package/amplify/publication/redirect-rules.js +73 -0
- package/amplify/publication/redirect-rules.ts +93 -0
- package/amplify/publication/s3-projection.js +7 -1
- package/amplify/publication/s3-projection.ts +9 -1
- package/amplify/site-backend-config.ts +17 -0
- package/amplify/site-backend.js +38 -1
- package/amplify/site-backend.ts +46 -1
- package/app/api/auth/prospectus/sign-in/route.ts +4 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/state/route.ts +9 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/newsroom/curation/context/route.ts +9 -0
- package/app/api/newsroom/curation/priming/route.ts +9 -0
- package/app/api/newsroom/curation/publication/route.ts +9 -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/flag/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/bin/papyrus-infra.mjs +20 -1
- package/components/editorial-checks-admin-panel.tsx +36 -11
- package/components/moderation-panel.tsx +248 -0
- package/components/monitoring-overview-view.tsx +241 -0
- package/components/monitoring-panel.tsx +40 -0
- package/components/newsletter-panel.tsx +474 -0
- package/components/newsletter-sections.tsx +163 -0
- package/components/newsroom-article-editor.tsx +2 -4
- package/components/newsroom-curation-bootstrap.tsx +45 -0
- package/components/newsroom-organizer.css +31 -0
- package/components/newsroom-references-view.tsx +14 -14
- package/components/publication-cyclotron-panel.tsx +15 -10
- package/components/reference-add-entry.tsx +75 -0
- package/components/reference-curation-controls.tsx +31 -22
- package/components/reference-cyclotron-priming.tsx +58 -19
- package/components/reference-cyclotron-suggestion.tsx +1 -1
- package/components/reference-priming.tsx +14 -10
- package/components/reference-section-suggestion.tsx +1 -1
- package/components/topic-steering-workspace.tsx +64 -20
- package/components/use-editorial-checks.ts +12 -10
- package/infra/amplify-app-shell.js +90 -16
- package/infra/amplify-app-shell.ts +102 -18
- package/infra/index.js +1 -0
- package/infra/secure-string-parameter-handler.js +41 -0
- package/infra/secure-string-parameter-handler.ts +41 -0
- package/infra/secure-string-parameter.js +79 -0
- package/infra/secure-string-parameter.ts +91 -0
- package/infra/site-config.js +86 -6
- package/infra/site-config.ts +129 -6
- package/lib/category-repository.ts +11 -9
- package/lib/curation-routes.ts +345 -0
- package/lib/editorial-checks-backend.ts +1 -1
- package/lib/editorial-checks-client.ts +19 -1
- package/lib/editorial-checks-demo.ts +2 -2
- package/lib/editorial-checks-settings.ts +3 -0
- package/lib/editorial-checks.ts +8 -11
- package/lib/editorial-preview.ts +5 -5
- package/lib/monitoring-client.ts +29 -0
- package/lib/monitoring-overview-fixture.ts +51 -0
- package/lib/monitoring-overview.ts +145 -0
- package/lib/newsletter-api.ts +418 -0
- package/lib/newsletter-client.ts +46 -0
- package/lib/newsletter-fixture.ts +184 -0
- package/lib/newsletter-model.ts +202 -0
- package/lib/prospectus-session.ts +15 -14
- package/lib/prospectus-sign-in-routes.ts +84 -19
- 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/lib/relevance-status.ts +2 -2
- package/lib/semantic-graph.ts +2 -0
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/lib/site-origin.ts +67 -0
- package/middleware.ts +14 -3
- package/package.json +42 -3
- package/routes.manifest.json +215 -2
- package/amplify/plugins/editorial-checks/function/requirements.txt +0 -1
- package/amplify/plugins/editorial-checks/papyrus.plugin.js +0 -69
- package/amplify/plugins/editorial-checks/papyrus.plugin.ts +0 -85
- package/amplify/plugins/editorial-checks/schema.js +0 -94
|
@@ -14,6 +14,9 @@ import type { ReactNode, RefObject } from "react";
|
|
|
14
14
|
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
|
|
15
15
|
import amplifyOutputs from "papyrus-amplify-outputs";
|
|
16
16
|
import { EditorialChecksAdminPanel } from "./editorial-checks-admin-panel";
|
|
17
|
+
import { MonitoringPanel } from "./monitoring-panel";
|
|
18
|
+
import { ModerationPanel } from "./moderation-panel";
|
|
19
|
+
import { NewsletterPanel } from "./newsletter-panel";
|
|
17
20
|
import { SignupEntriesPanel } from "./signup-entries-panel";
|
|
18
21
|
import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
|
|
19
22
|
import { generateClient } from "aws-amplify/data";
|
|
@@ -89,6 +92,7 @@ import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
|
89
92
|
import type { ReaderAuthSnapshot } from "./reader-auth-state";
|
|
90
93
|
import { NewsDeskAccessGate } from "./newsroom-access-gate";
|
|
91
94
|
import { NewsroomOpsOverview } from "./newsroom-ops-overview";
|
|
95
|
+
import { NewsroomCurationBootstrap } from "./newsroom-curation-bootstrap";
|
|
92
96
|
import { NewsroomReferencesView } from "./newsroom-references-view";
|
|
93
97
|
import { NewsroomAssignmentsView } from "./newsroom-assignments-view";
|
|
94
98
|
import { NewsroomTopicsView } from "./newsroom-topics-view";
|
|
@@ -247,7 +251,7 @@ type AssignmentAction = "claim" | "release" | "complete" | "cancel" | "reopen" |
|
|
|
247
251
|
type ReportingPacketReviewDecision = "select" | "merge" | "brief" | "hold" | "kill";
|
|
248
252
|
type AssignmentDeskViewMode = "queue" | "budget";
|
|
249
253
|
type UserRoleAction = "grant" | "revoke";
|
|
250
|
-
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups" | "editorial-checks";
|
|
254
|
+
type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "delegations" | "signups" | "newsletter" | "moderation" | "editorial-checks" | "monitoring";
|
|
251
255
|
export type NewsDeskTab = "overview" | "desks" | "topics" | "concepts" | "references" | "messages" | "assignments" | "administration" | "search";
|
|
252
256
|
type LexicalRuleScope = "publication" | "corpus" | "classifier" | "category";
|
|
253
257
|
type AnalysisReindexMode = "online-update" | "classifier-retrain" | "scoped-topic-rebuild" | "entity-graph-rebuild" | "generated-analysis-rebuild";
|
|
@@ -879,7 +883,7 @@ function NewsDeskDashboard({
|
|
|
879
883
|
const [mergeSelection, setMergeSelection] = useState<MergeSelection | null>(null);
|
|
880
884
|
const [isPending, startTransition] = useTransition();
|
|
881
885
|
const controlsDisabled = isPending || !canEdit;
|
|
882
|
-
const initialAdministrationPanel = normalizeAdministrationPanel(initialSelection.panel);
|
|
886
|
+
const initialAdministrationPanel = normalizeAdministrationPanel(initialSelection.panel, Boolean(dashboard.isDemo));
|
|
883
887
|
const [administrationPanel, setAdministrationPanel] = useState<AdministrationPanel>(initialAdministrationPanel);
|
|
884
888
|
const [doctrineDrafts, setDoctrineDrafts] = useState<DoctrineEditorState>(() => buildDoctrineEditorState(dashboard.doctrineRecords));
|
|
885
889
|
const [loadedSections, setLoadedSections] = useState<Record<string, boolean>>({
|
|
@@ -1120,8 +1124,8 @@ function NewsDeskDashboard({
|
|
|
1120
1124
|
}, [dashboard.procedureDefinitions, dashboard.procedureRuns, dashboard.procedureVersions]);
|
|
1121
1125
|
|
|
1122
1126
|
useEffect(() => {
|
|
1123
|
-
setAdministrationPanel(normalizeAdministrationPanel(initialSelection.panel));
|
|
1124
|
-
}, [initialSelection.panel]);
|
|
1127
|
+
setAdministrationPanel(normalizeAdministrationPanel(initialSelection.panel, Boolean(dashboard.isDemo)));
|
|
1128
|
+
}, [initialSelection.panel, dashboard.isDemo]);
|
|
1125
1129
|
|
|
1126
1130
|
useEffect(() => {
|
|
1127
1131
|
if (activeTab !== "administration") return;
|
|
@@ -3115,6 +3119,7 @@ function NewsDeskDashboard({
|
|
|
3115
3119
|
/>
|
|
3116
3120
|
) : null}
|
|
3117
3121
|
{activeTab === "references" ? (
|
|
3122
|
+
<NewsroomCurationBootstrap enabled={!dashboard.isDemo}>
|
|
3118
3123
|
<NewsroomReferencesView
|
|
3119
3124
|
demo={Boolean(dashboard.isDemo)}
|
|
3120
3125
|
disabled={controlsDisabled}
|
|
@@ -3152,6 +3157,7 @@ function NewsDeskDashboard({
|
|
|
3152
3157
|
referenceAttachments={referenceAttachments}
|
|
3153
3158
|
references={references}
|
|
3154
3159
|
/>
|
|
3160
|
+
</NewsroomCurationBootstrap>
|
|
3155
3161
|
) : null}
|
|
3156
3162
|
{activeTab === "messages" ? (
|
|
3157
3163
|
<MessagesDeskView
|
|
@@ -4318,17 +4324,24 @@ function AdministrationDeskView({
|
|
|
4318
4324
|
<strong>Agent delegations</strong>
|
|
4319
4325
|
<span>Let an agent curate References for you</span>
|
|
4320
4326
|
</Link>
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
>
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4327
|
+
<Link
|
|
4328
|
+
href={administrationPanelHref("editorial-checks", isDemo)}
|
|
4329
|
+
data-news-desk-admin-nav="editorial-checks"
|
|
4330
|
+
data-active={administrationPanel === "editorial-checks" || undefined}
|
|
4331
|
+
onClick={() => onPanelChange("editorial-checks")}
|
|
4332
|
+
>
|
|
4333
|
+
<strong>Editorial checks</strong>
|
|
4334
|
+
<span>Policies the article editor checks drafts against</span>
|
|
4335
|
+
</Link>
|
|
4336
|
+
<Link
|
|
4337
|
+
href={administrationPanelHref("monitoring", isDemo)}
|
|
4338
|
+
data-news-desk-admin-nav="monitoring"
|
|
4339
|
+
data-active={administrationPanel === "monitoring" || undefined}
|
|
4340
|
+
onClick={() => onPanelChange("monitoring")}
|
|
4341
|
+
>
|
|
4342
|
+
<strong>Monitoring</strong>
|
|
4343
|
+
<span>The automated research pipeline, read only</span>
|
|
4344
|
+
</Link>
|
|
4332
4345
|
{signupPluginEnabled() ? (
|
|
4333
4346
|
<Link
|
|
4334
4347
|
href={administrationPanelHref("signups", isDemo)}
|
|
@@ -4340,10 +4353,35 @@ function AdministrationDeskView({
|
|
|
4340
4353
|
<span>Addresses from the site's signup forms</span>
|
|
4341
4354
|
</Link>
|
|
4342
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}
|
|
4343
4378
|
</nav>}
|
|
4344
4379
|
<div className="news-desk-settings-panel">
|
|
4345
4380
|
{administrationPanel === "signups" ? <SignupEntriesPanel /> : null}
|
|
4381
|
+
{administrationPanel === "newsletter" ? <NewsletterPanel isAdmin={canManageUsers} isDemo={isDemo} /> : null}
|
|
4382
|
+
{administrationPanel === "moderation" ? <ModerationPanel /> : null}
|
|
4346
4383
|
{administrationPanel === "editorial-checks" ? <EditorialChecksAdminPanel /> : null}
|
|
4384
|
+
{administrationPanel === "monitoring" ? <MonitoringPanel isAdmin={canManageUsers} isDemo={isDemo} /> : null}
|
|
4347
4385
|
{administrationPanel === "delegations" ? <AdministrationDelegationsPanel disabled={disabled} /> : null}
|
|
4348
4386
|
{administrationPanel === "users" ? (
|
|
4349
4387
|
<AdministrationUsersPanel
|
|
@@ -12807,14 +12845,20 @@ function signupPluginEnabled(): boolean {
|
|
|
12807
12845
|
return pluginSettings(amplifyOutputs as PluginOutputs, "signup") !== null;
|
|
12808
12846
|
}
|
|
12809
12847
|
|
|
12810
|
-
|
|
12811
|
-
|
|
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;
|
|
12812
12855
|
}
|
|
12813
12856
|
|
|
12814
|
-
function normalizeAdministrationPanel(value: string | null | undefined): AdministrationPanel {
|
|
12815
|
-
if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations") return value;
|
|
12857
|
+
function normalizeAdministrationPanel(value: string | null | undefined, demo = false): AdministrationPanel {
|
|
12858
|
+
if (value === "policies" || value === "sections" || value === "procedures" || value === "delegations" || value === "editorial-checks" || value === "monitoring") return value;
|
|
12816
12859
|
if (value === "signups" && signupPluginEnabled()) return value;
|
|
12817
|
-
if (value === "
|
|
12860
|
+
if (value === "newsletter" && newsletterPluginEnabled(demo)) return value;
|
|
12861
|
+
if (value === "moderation" && readerPluginEnabled()) return value;
|
|
12818
12862
|
return "users";
|
|
12819
12863
|
}
|
|
12820
12864
|
|
|
@@ -27,14 +27,16 @@ export function useEditorialChecks({
|
|
|
27
27
|
section,
|
|
28
28
|
itemId,
|
|
29
29
|
}: {
|
|
30
|
-
/** null
|
|
30
|
+
/** null when there is no backend to check against (a preview). Whether a publication runs checks is its stored setting. */
|
|
31
31
|
backend: EditorialChecksBackend | null;
|
|
32
32
|
body: string | null;
|
|
33
33
|
section: string;
|
|
34
34
|
itemId: string | null;
|
|
35
35
|
}) {
|
|
36
|
-
const
|
|
36
|
+
const available = backend !== null;
|
|
37
37
|
const [settings, setSettings] = useState<EditorialSettings>(DEFAULT_EDITORIAL_SETTINGS);
|
|
38
|
+
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
|
39
|
+
const enabled = available && settings.enabled;
|
|
38
40
|
const [result, setResult] = useState<EditorialCheckResult | null>(null);
|
|
39
41
|
const [checkedBody, setCheckedBody] = useState<string | null>(null);
|
|
40
42
|
const [status, setStatus] = useState<CheckStatus>({ state: "idle" });
|
|
@@ -46,9 +48,9 @@ export function useEditorialChecks({
|
|
|
46
48
|
const sequence = useRef(0);
|
|
47
49
|
|
|
48
50
|
useEffect(() => {
|
|
49
|
-
if (!
|
|
50
|
-
backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined);
|
|
51
|
-
}, [
|
|
51
|
+
if (!available) return;
|
|
52
|
+
backend?.loadSettings().then(({ settings: loaded }) => setSettings(loaded)).catch(() => undefined).finally(() => setSettingsLoaded(true));
|
|
53
|
+
}, [available, backend]);
|
|
52
54
|
|
|
53
55
|
useEffect(() => {
|
|
54
56
|
if (!enabled || !itemId) return;
|
|
@@ -57,7 +59,7 @@ export function useEditorialChecks({
|
|
|
57
59
|
|
|
58
60
|
const check = useCallback(async () => {
|
|
59
61
|
const text = latestBody.current;
|
|
60
|
-
if (!backend || text === null || !text.trim()) return;
|
|
62
|
+
if (!backend || !enabled || text === null || !text.trim()) return;
|
|
61
63
|
const run = ++sequence.current;
|
|
62
64
|
setStatus({ state: "checking" });
|
|
63
65
|
try {
|
|
@@ -73,13 +75,13 @@ export function useEditorialChecks({
|
|
|
73
75
|
} catch (error) {
|
|
74
76
|
if (run === sequence.current) setStatus({ state: "error", message: error instanceof Error ? error.message : "The check failed." });
|
|
75
77
|
}
|
|
76
|
-
}, [backend, section]);
|
|
78
|
+
}, [backend, enabled, section]);
|
|
77
79
|
|
|
78
80
|
useEffect(() => {
|
|
79
|
-
if (!enabled || !settings.checkWhileTyping || body === null || body === checkedBody) return;
|
|
81
|
+
if (!enabled || !settingsLoaded || !settings.checkWhileTyping || body === null || body === checkedBody) return;
|
|
80
82
|
const timer = window.setTimeout(() => void check(), CHECK_PAUSE_MS);
|
|
81
83
|
return () => window.clearTimeout(timer);
|
|
82
|
-
}, [body, check, checkedBody, enabled, settings.checkWhileTyping]);
|
|
84
|
+
}, [body, check, checkedBody, enabled, settings.checkWhileTyping, settingsLoaded]);
|
|
83
85
|
|
|
84
86
|
const record = useCallback(
|
|
85
87
|
(finding: EditorialFinding, decision: FeedbackDecision, reason: string | null = null) => {
|
|
@@ -105,7 +107,7 @@ export function useEditorialChecks({
|
|
|
105
107
|
[backend, section],
|
|
106
108
|
);
|
|
107
109
|
|
|
108
|
-
const current = result && checkedBody === body;
|
|
110
|
+
const current = enabled && result && checkedBody === body;
|
|
109
111
|
return {
|
|
110
112
|
enabled,
|
|
111
113
|
settings,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ArnFormat, CfnOutput, Duration, Fn, Stack } from "aws-cdk-lib";
|
|
2
2
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
4
|
-
import
|
|
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, resolveProspectusAccountsEndpoint, 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
|
}
|
|
@@ -68,6 +68,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
68
68
|
if (!config.reader)
|
|
69
69
|
throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
70
70
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
71
|
+
if (config.autoDeploy) {
|
|
72
|
+
this.addGithubDeployRole(config, [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }]);
|
|
73
|
+
}
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
@@ -89,33 +92,49 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
89
92
|
});
|
|
90
93
|
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
91
94
|
if (revalidateSecretParameterName) {
|
|
95
|
+
const revalidateSecret = new SecureStringParameter(this, "RevalidateSecretParameter", {
|
|
96
|
+
parameterName: revalidateSecretParameterName,
|
|
97
|
+
description: `Shared secret that authorizes publishes to call the /api/revalidate route of ${cmsAppName}`,
|
|
98
|
+
});
|
|
92
99
|
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
93
100
|
sid: "ReadRevalidateSecret",
|
|
94
101
|
actions: ["ssm:GetParameter"],
|
|
95
|
-
resources: [
|
|
96
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
97
|
-
],
|
|
102
|
+
resources: [revalidateSecret.parameterArn],
|
|
98
103
|
}));
|
|
99
104
|
}
|
|
100
105
|
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
101
|
-
const
|
|
102
|
-
? new
|
|
106
|
+
const prospectusSessionSealing = prospectusAccountsApiStageArn
|
|
107
|
+
? new SecureStringParameter(this, "ProspectusSessionSealingParameter", {
|
|
108
|
+
parameterName: resolveProspectusSessionSealingParameterName(config),
|
|
103
109
|
description: `Seals the Prospectus sign-in cookies of ${cmsAppName}; replacing it signs everyone out`,
|
|
104
|
-
generateSecretString: { passwordLength: 64, excludePunctuation: true },
|
|
105
110
|
})
|
|
106
111
|
: undefined;
|
|
107
|
-
const prospectusEnvironment = prospectusAccountsApiStageArn &&
|
|
112
|
+
const prospectusEnvironment = prospectusAccountsApiStageArn && prospectusSessionSealing
|
|
108
113
|
? {
|
|
109
114
|
PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn),
|
|
110
|
-
|
|
115
|
+
PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER: prospectusSessionSealing.parameterName,
|
|
111
116
|
PAPYRUS_PROSPECTUS_SITE_PRODUCT: `papyrus-${config.siteId}`,
|
|
112
117
|
}
|
|
113
118
|
: {};
|
|
114
|
-
if (
|
|
119
|
+
if (prospectusSessionSealing) {
|
|
115
120
|
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
116
121
|
sid: "ReadProspectusSessionSealingSecret",
|
|
117
|
-
actions: ["
|
|
118
|
-
resources: [
|
|
122
|
+
actions: ["ssm:GetParameter"],
|
|
123
|
+
resources: [prospectusSessionSealing.parameterArn],
|
|
124
|
+
}));
|
|
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],
|
|
119
138
|
}));
|
|
120
139
|
}
|
|
121
140
|
if (prospectusAccountsApiStageArn) {
|
|
@@ -153,9 +172,15 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
153
172
|
const stagingOrigin = stagingDomainName
|
|
154
173
|
? `https://${stagingDomainName}/`
|
|
155
174
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
175
|
+
// The compute adapter's request URL is https://localhost:3000, so the site names its own public origin.
|
|
176
|
+
// A site served by its own edge (cms.siteOrigin) has no Amplify domain, so the edge host is its origin.
|
|
177
|
+
const siteOrigin = resolveSiteOrigin(config);
|
|
178
|
+
const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : siteOrigin ?? Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
|
|
179
|
+
const stagingSiteOrigin = stagingDomainName ? `https://${stagingDomainName}` : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]);
|
|
156
180
|
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
157
181
|
const defaultOriginsToAllow = [
|
|
158
182
|
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
183
|
+
...(siteOrigin ? [`${siteOrigin}/`] : []),
|
|
159
184
|
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
160
185
|
];
|
|
161
186
|
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
@@ -165,7 +190,8 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
165
190
|
appId: cmsApp.attrAppId,
|
|
166
191
|
branchName: "main",
|
|
167
192
|
stage: "PRODUCTION",
|
|
168
|
-
|
|
193
|
+
// Auto-build off when GitHub Actions starts the job on merge (autoDeploy): nothing may double-build.
|
|
194
|
+
enableAutoBuild: !config.autoDeploy,
|
|
169
195
|
enablePerformanceMode: false,
|
|
170
196
|
environmentVariables: environmentVariables({
|
|
171
197
|
...config.cms.environment,
|
|
@@ -174,6 +200,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
174
200
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
175
201
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
176
202
|
SITE_ENV: "production",
|
|
203
|
+
PAPYRUS_SITE_ORIGIN: productionSiteOrigin,
|
|
177
204
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
178
205
|
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
179
206
|
...prospectusEnvironment,
|
|
@@ -195,6 +222,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
195
222
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
196
223
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
197
224
|
SITE_ENV: "staging",
|
|
225
|
+
PAPYRUS_SITE_ORIGIN: stagingSiteOrigin,
|
|
198
226
|
...stagingMode,
|
|
199
227
|
...prospectusEnvironment,
|
|
200
228
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
@@ -247,6 +275,12 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
247
275
|
}
|
|
248
276
|
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
249
277
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
278
|
+
if (config.autoDeploy) {
|
|
279
|
+
this.addGithubDeployRole(config, [
|
|
280
|
+
{ appId: cmsApp.attrAppId, branch: "main" },
|
|
281
|
+
...(this.readerAppId && config.reader ? [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }] : []),
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
250
284
|
if (config.github) {
|
|
251
285
|
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
252
286
|
}
|
|
@@ -294,6 +328,46 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
294
328
|
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
295
329
|
});
|
|
296
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The merge-to-main deploy role: assumable only by a GitHub Actions run on `main` of the site's own
|
|
333
|
+
* repository (OIDC, no keys), and able to do exactly one thing: start and read Amplify jobs on the
|
|
334
|
+
* production branch of the site's apps.
|
|
335
|
+
*/
|
|
336
|
+
addGithubDeployRole(config, targets) {
|
|
337
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
338
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubDeployOidcProvider", this.formatArn({
|
|
339
|
+
service: "iam",
|
|
340
|
+
region: "",
|
|
341
|
+
resource: "oidc-provider",
|
|
342
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
343
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
344
|
+
}));
|
|
345
|
+
const deployRole = new iam.Role(this, "GithubDeployRole", {
|
|
346
|
+
roleName: `${config.siteId}-github-deploy`,
|
|
347
|
+
description: `Merge-to-main deploy for ${owner}/${repo}: GitHub OIDC on refs/heads/main only; amplify:StartJob and GetJob on the production branch of the site's apps only`,
|
|
348
|
+
maxSessionDuration: Duration.hours(1),
|
|
349
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
350
|
+
StringEquals: {
|
|
351
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com",
|
|
352
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: githubOidcSubjects(config, "main"),
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
deployRole.addToPolicy(new iam.PolicyStatement({
|
|
357
|
+
sid: "StartAndReadProductionJobs",
|
|
358
|
+
actions: ["amplify:StartJob", "amplify:GetJob"],
|
|
359
|
+
resources: targets.map((target) => this.formatArn({
|
|
360
|
+
service: "amplify",
|
|
361
|
+
resource: "apps",
|
|
362
|
+
resourceName: `${target.appId}/branches/${target.branch}/jobs/*`,
|
|
363
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
364
|
+
})),
|
|
365
|
+
}));
|
|
366
|
+
new CfnOutput(this, "GithubDeployRoleArn", {
|
|
367
|
+
value: deployRole.roleArn,
|
|
368
|
+
description: `Role for the merge-to-main deploy workflow in ${owner}/${repo}`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
297
371
|
addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
|
|
298
372
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
|
|
299
373
|
service: "iam",
|
|
@@ -309,7 +383,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
309
383
|
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
310
384
|
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
311
385
|
StringLike: {
|
|
312
|
-
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.
|
|
386
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.flatMap((branch) => githubOidcSubjects(config, branch)),
|
|
313
387
|
},
|
|
314
388
|
}),
|
|
315
389
|
});
|
|
@@ -361,7 +435,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
361
435
|
appId: readerApp.attrAppId,
|
|
362
436
|
branchName: readerBranchName,
|
|
363
437
|
stage: "PRODUCTION",
|
|
364
|
-
enableAutoBuild:
|
|
438
|
+
enableAutoBuild: !config.autoDeploy,
|
|
365
439
|
environmentVariables: environmentVariables({
|
|
366
440
|
...(reader.environment ?? {}),
|
|
367
441
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
@@ -2,10 +2,10 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { ArnFormat, CfnOutput, Duration, Fn, Stack, StackProps } from "aws-cdk-lib";
|
|
3
3
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
5
|
-
import
|
|
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, resolveProspectusAccountsEndpoint, 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 }));
|
|
@@ -72,6 +72,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
72
72
|
if (!hasCmsApp(config)) {
|
|
73
73
|
if (!config.reader) throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
74
74
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
75
|
+
if (config.autoDeploy) {
|
|
76
|
+
this.addGithubDeployRole(config, [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }]);
|
|
77
|
+
}
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -97,37 +100,55 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
97
100
|
|
|
98
101
|
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
99
102
|
if (revalidateSecretParameterName) {
|
|
103
|
+
const revalidateSecret = new SecureStringParameter(this, "RevalidateSecretParameter", {
|
|
104
|
+
parameterName: revalidateSecretParameterName,
|
|
105
|
+
description: `Shared secret that authorizes publishes to call the /api/revalidate route of ${cmsAppName}`,
|
|
106
|
+
});
|
|
100
107
|
computeRole.addToPolicy(
|
|
101
108
|
new iam.PolicyStatement({
|
|
102
109
|
sid: "ReadRevalidateSecret",
|
|
103
110
|
actions: ["ssm:GetParameter"],
|
|
104
|
-
resources: [
|
|
105
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
106
|
-
],
|
|
111
|
+
resources: [revalidateSecret.parameterArn],
|
|
107
112
|
}),
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
112
|
-
const
|
|
113
|
-
? new
|
|
117
|
+
const prospectusSessionSealing = prospectusAccountsApiStageArn
|
|
118
|
+
? new SecureStringParameter(this, "ProspectusSessionSealingParameter", {
|
|
119
|
+
parameterName: resolveProspectusSessionSealingParameterName(config),
|
|
114
120
|
description: `Seals the Prospectus sign-in cookies of ${cmsAppName}; replacing it signs everyone out`,
|
|
115
|
-
generateSecretString: { passwordLength: 64, excludePunctuation: true },
|
|
116
121
|
})
|
|
117
122
|
: undefined;
|
|
118
|
-
const prospectusEnvironment = prospectusAccountsApiStageArn &&
|
|
123
|
+
const prospectusEnvironment: Record<string, string> = prospectusAccountsApiStageArn && prospectusSessionSealing
|
|
119
124
|
? {
|
|
120
125
|
PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn),
|
|
121
|
-
|
|
126
|
+
PAPYRUS_PROSPECTUS_SESSION_SECRET_PARAMETER: prospectusSessionSealing.parameterName,
|
|
122
127
|
PAPYRUS_PROSPECTUS_SITE_PRODUCT: `papyrus-${config.siteId}`,
|
|
123
128
|
}
|
|
124
129
|
: {};
|
|
125
|
-
if (
|
|
130
|
+
if (prospectusSessionSealing) {
|
|
126
131
|
computeRole.addToPolicy(
|
|
127
132
|
new iam.PolicyStatement({
|
|
128
133
|
sid: "ReadProspectusSessionSealingSecret",
|
|
129
|
-
actions: ["
|
|
130
|
-
resources: [
|
|
134
|
+
actions: ["ssm:GetParameter"],
|
|
135
|
+
resources: [prospectusSessionSealing.parameterArn],
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
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],
|
|
131
152
|
}),
|
|
132
153
|
);
|
|
133
154
|
}
|
|
@@ -172,9 +193,15 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
172
193
|
const stagingOrigin = stagingDomainName
|
|
173
194
|
? `https://${stagingDomainName}/`
|
|
174
195
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
196
|
+
// The compute adapter's request URL is https://localhost:3000, so the site names its own public origin.
|
|
197
|
+
// A site served by its own edge (cms.siteOrigin) has no Amplify domain, so the edge host is its origin.
|
|
198
|
+
const siteOrigin = resolveSiteOrigin(config);
|
|
199
|
+
const productionSiteOrigin = cmsHostName ? `https://${cmsHostName}` : siteOrigin ?? Fn.join("", ["https://main.", cmsApp.attrDefaultDomain]);
|
|
200
|
+
const stagingSiteOrigin = stagingDomainName ? `https://${stagingDomainName}` : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]);
|
|
175
201
|
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
176
202
|
const defaultOriginsToAllow = [
|
|
177
203
|
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
204
|
+
...(siteOrigin ? [`${siteOrigin}/`] : []),
|
|
178
205
|
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
179
206
|
];
|
|
180
207
|
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
@@ -185,7 +212,8 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
185
212
|
appId: cmsApp.attrAppId,
|
|
186
213
|
branchName: "main",
|
|
187
214
|
stage: "PRODUCTION",
|
|
188
|
-
|
|
215
|
+
// Auto-build off when GitHub Actions starts the job on merge (autoDeploy): nothing may double-build.
|
|
216
|
+
enableAutoBuild: !config.autoDeploy,
|
|
189
217
|
enablePerformanceMode: false,
|
|
190
218
|
environmentVariables: environmentVariables({
|
|
191
219
|
...config.cms.environment,
|
|
@@ -194,6 +222,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
194
222
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
195
223
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
196
224
|
SITE_ENV: "production",
|
|
225
|
+
PAPYRUS_SITE_ORIGIN: productionSiteOrigin,
|
|
197
226
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
198
227
|
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
199
228
|
...prospectusEnvironment,
|
|
@@ -216,6 +245,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
216
245
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
217
246
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
218
247
|
SITE_ENV: "staging",
|
|
248
|
+
PAPYRUS_SITE_ORIGIN: stagingSiteOrigin,
|
|
219
249
|
...stagingMode,
|
|
220
250
|
...prospectusEnvironment,
|
|
221
251
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
@@ -274,6 +304,13 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
274
304
|
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
275
305
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
276
306
|
|
|
307
|
+
if (config.autoDeploy) {
|
|
308
|
+
this.addGithubDeployRole(config, [
|
|
309
|
+
{ appId: cmsApp.attrAppId, branch: "main" },
|
|
310
|
+
...(this.readerAppId && config.reader ? [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }] : []),
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
|
|
277
314
|
if (config.github) {
|
|
278
315
|
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
279
316
|
}
|
|
@@ -326,6 +363,55 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
326
363
|
});
|
|
327
364
|
}
|
|
328
365
|
|
|
366
|
+
/**
|
|
367
|
+
* The merge-to-main deploy role: assumable only by a GitHub Actions run on `main` of the site's own
|
|
368
|
+
* repository (OIDC, no keys), and able to do exactly one thing: start and read Amplify jobs on the
|
|
369
|
+
* production branch of the site's apps.
|
|
370
|
+
*/
|
|
371
|
+
private addGithubDeployRole(config: AmplifyAppShellSiteConfig, targets: { appId: string; branch: string }[]): void {
|
|
372
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
373
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
374
|
+
this,
|
|
375
|
+
"GithubDeployOidcProvider",
|
|
376
|
+
this.formatArn({
|
|
377
|
+
service: "iam",
|
|
378
|
+
region: "",
|
|
379
|
+
resource: "oidc-provider",
|
|
380
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
381
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
const deployRole = new iam.Role(this, "GithubDeployRole", {
|
|
385
|
+
roleName: `${config.siteId}-github-deploy`,
|
|
386
|
+
description: `Merge-to-main deploy for ${owner}/${repo}: GitHub OIDC on refs/heads/main only; amplify:StartJob and GetJob on the production branch of the site's apps only`,
|
|
387
|
+
maxSessionDuration: Duration.hours(1),
|
|
388
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
389
|
+
StringEquals: {
|
|
390
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com",
|
|
391
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: githubOidcSubjects(config, "main"),
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
deployRole.addToPolicy(
|
|
396
|
+
new iam.PolicyStatement({
|
|
397
|
+
sid: "StartAndReadProductionJobs",
|
|
398
|
+
actions: ["amplify:StartJob", "amplify:GetJob"],
|
|
399
|
+
resources: targets.map((target) =>
|
|
400
|
+
this.formatArn({
|
|
401
|
+
service: "amplify",
|
|
402
|
+
resource: "apps",
|
|
403
|
+
resourceName: `${target.appId}/branches/${target.branch}/jobs/*`,
|
|
404
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
405
|
+
}),
|
|
406
|
+
),
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
new CfnOutput(this, "GithubDeployRoleArn", {
|
|
410
|
+
value: deployRole.roleArn,
|
|
411
|
+
description: `Role for the merge-to-main deploy workflow in ${owner}/${repo}`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
329
415
|
private addGithubCiRole(
|
|
330
416
|
config: AmplifyAppShellSiteConfig,
|
|
331
417
|
github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
|
|
@@ -351,9 +437,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
351
437
|
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
352
438
|
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
353
439
|
StringLike: {
|
|
354
|
-
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.
|
|
355
|
-
(branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`,
|
|
356
|
-
),
|
|
440
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.flatMap((branch) => githubOidcSubjects(config, branch)),
|
|
357
441
|
},
|
|
358
442
|
}),
|
|
359
443
|
});
|
|
@@ -420,7 +504,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
420
504
|
appId: readerApp.attrAppId,
|
|
421
505
|
branchName: readerBranchName,
|
|
422
506
|
stage: "PRODUCTION",
|
|
423
|
-
enableAutoBuild:
|
|
507
|
+
enableAutoBuild: !config.autoDeploy,
|
|
424
508
|
environmentVariables: environmentVariables({
|
|
425
509
|
...(reader.environment ?? {}),
|
|
426
510
|
PAPYRUS_SITE_BRAND: config.brand,
|