@anthusai/papyrus 1.3.0 → 1.4.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/resource.js +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +8 -1
- package/app/newsroom/articles/page.tsx +10 -0
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +154 -240
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +153 -43
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +36 -13
- package/components/newsroom-organizer.css +269 -13
- package/components/newsroom-references-view.tsx +118 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +280 -679
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/chrome-text.ts +504 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +1133 -0
- package/lib/furniture.ts +385 -0
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +40 -11
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +286 -0
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +99 -47
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +9 -16
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/pretext-layout.ts +22 -0
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +28 -1
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/text-columns.ts +251 -0
- package/lib/wiki.ts +56 -0
- package/package.json +46 -3
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -15
- package/app/newsroom/work/page.tsx +0 -10
package/components/ui/sheet.tsx
CHANGED
|
@@ -11,9 +11,9 @@ type SheetSide = "bottom" | "left" | "right";
|
|
|
11
11
|
|
|
12
12
|
const sideClasses: Record<SheetSide, string> = {
|
|
13
13
|
bottom:
|
|
14
|
-
"inset-x-0 bottom-0 top-auto max-h-[85dvh] w-full
|
|
15
|
-
left: "inset-y-0 left-0 h-full w-[min(100vw,20rem)]
|
|
16
|
-
right: "inset-y-0 right-0 h-full w-[min(100vw,20rem)]
|
|
14
|
+
"inset-x-0 bottom-0 top-auto max-h-[85dvh] w-full border-t data-open:slide-in-from-bottom data-closed:slide-out-to-bottom",
|
|
15
|
+
left: "inset-y-0 left-0 h-full w-[min(100vw,20rem)] border-r data-open:slide-in-from-left data-closed:slide-out-to-left",
|
|
16
|
+
right: "inset-y-0 right-0 h-full w-[min(100vw,20rem)] border-l data-open:slide-in-from-right data-closed:slide-out-to-right",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
function Sheet({ ...props }: DialogPrimitive.Root.Props) {
|
|
@@ -31,17 +31,23 @@ function SheetClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
|
31
31
|
function SheetContent({
|
|
32
32
|
children,
|
|
33
33
|
className,
|
|
34
|
+
backdropClassName,
|
|
34
35
|
showCloseButton = true,
|
|
35
36
|
side = "bottom",
|
|
36
37
|
...props
|
|
37
38
|
}: DialogPrimitive.Popup.Props & {
|
|
39
|
+
backdropClassName?: string;
|
|
38
40
|
showCloseButton?: boolean;
|
|
39
41
|
side?: SheetSide;
|
|
40
42
|
}) {
|
|
41
43
|
return (
|
|
42
44
|
<DialogPrimitive.Portal>
|
|
43
45
|
<DialogPrimitive.Backdrop
|
|
44
|
-
className=
|
|
46
|
+
className={cn(
|
|
47
|
+
"fixed inset-0 z-50 bg-black/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
48
|
+
typeof className === "string" && className.includes("md:hidden") && "md:hidden",
|
|
49
|
+
backdropClassName,
|
|
50
|
+
)}
|
|
45
51
|
/>
|
|
46
52
|
<DialogPrimitive.Popup
|
|
47
53
|
className={cn(
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
canonicalContentItemStage,
|
|
6
|
+
legacyStageKeysToLoad,
|
|
7
|
+
type ContentItemVersionRecord,
|
|
8
|
+
} from "../lib/content-item-lifecycle";
|
|
9
|
+
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
10
|
+
import { loadNewsroomContentItemStagePage } from "./news-desk-taxonomy-client";
|
|
11
|
+
|
|
12
|
+
const REFRESH_INTERVAL_MILLISECONDS = 30_000;
|
|
13
|
+
|
|
14
|
+
type StageBucket = {
|
|
15
|
+
versions: ContentItemVersionRecord[];
|
|
16
|
+
nextToken: string | null;
|
|
17
|
+
pagesLoaded: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PagedContentItemVersions = {
|
|
21
|
+
versions: ContentItemVersionRecord[];
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
stageHasMore: (canonicalStage: string) => boolean;
|
|
25
|
+
loadMoreForStage: (canonicalStage: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function mergeFreshFirstPage(existing: StageBucket | undefined, fresh: StageBucket): StageBucket {
|
|
29
|
+
if (!existing || existing.pagesLoaded <= 1) return fresh;
|
|
30
|
+
const freshIds = new Set(fresh.versions.map(version => version.id));
|
|
31
|
+
return {
|
|
32
|
+
versions: [...fresh.versions, ...existing.versions.filter(version => !freshIds.has(version.id))],
|
|
33
|
+
nextToken: existing.nextToken,
|
|
34
|
+
pagesLoaded: existing.pagesLoaded,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Newest-updated content items per stage, one page per stage to start, more pages on demand.
|
|
40
|
+
* The periodic refresh re-fetches only first pages and keeps whatever later pages were already loaded.
|
|
41
|
+
*/
|
|
42
|
+
export function usePagedContentItemVersions(options: {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
openedCollapsedUntilOpenedStages: readonly string[];
|
|
45
|
+
}): PagedContentItemVersions {
|
|
46
|
+
const { enabled, openedCollapsedUntilOpenedStages } = options;
|
|
47
|
+
const [buckets, setBuckets] = useState<Record<string, StageBucket>>({});
|
|
48
|
+
const [loading, setLoading] = useState(false);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
const bucketsRef = useRef(buckets);
|
|
51
|
+
const stageKeysBeingExtendedRef = useRef(new Set<string>());
|
|
52
|
+
const generationRef = useRef(0);
|
|
53
|
+
|
|
54
|
+
const openedStagesSignature = openedCollapsedUntilOpenedStages.join(",");
|
|
55
|
+
const stageKeysToLoad = useMemo(() => {
|
|
56
|
+
const stageDefinitions = getContentItemStageDefinitions();
|
|
57
|
+
return [
|
|
58
|
+
...stageDefinitions
|
|
59
|
+
.filter(definition => !definition.collapsedUntilOpened || openedStagesSignature.split(",").includes(definition.key))
|
|
60
|
+
.map(definition => definition.key),
|
|
61
|
+
...legacyStageKeysToLoad(stageDefinitions),
|
|
62
|
+
];
|
|
63
|
+
}, [openedStagesSignature]);
|
|
64
|
+
const stageKeysToLoadRef = useRef(stageKeysToLoad);
|
|
65
|
+
stageKeysToLoadRef.current = stageKeysToLoad;
|
|
66
|
+
|
|
67
|
+
const storeBuckets = useCallback((update: (previous: Record<string, StageBucket>) => Record<string, StageBucket>) => {
|
|
68
|
+
const next = update(bucketsRef.current);
|
|
69
|
+
bucketsRef.current = next;
|
|
70
|
+
setBuckets(next);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const refreshFirstPages = useCallback(async (request: number, isInitial: boolean, stageKeys: readonly string[]) => {
|
|
74
|
+
if (isInitial) setLoading(true);
|
|
75
|
+
try {
|
|
76
|
+
const freshBuckets = await Promise.all(stageKeys.map(async stageKey => {
|
|
77
|
+
const page = await loadNewsroomContentItemStagePage(stageKey);
|
|
78
|
+
return [stageKey, { versions: page.versions, nextToken: page.nextToken, pagesLoaded: 1 }] as const;
|
|
79
|
+
}));
|
|
80
|
+
if (generationRef.current !== request) return;
|
|
81
|
+
storeBuckets(previous => {
|
|
82
|
+
const next = { ...previous };
|
|
83
|
+
for (const [stageKey, fresh] of freshBuckets) next[stageKey] = mergeFreshFirstPage(previous[stageKey], fresh);
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
setError(null);
|
|
87
|
+
} catch (reason: unknown) {
|
|
88
|
+
if (generationRef.current === request && isInitial) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
89
|
+
} finally {
|
|
90
|
+
if (generationRef.current === request && isInitial) setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
}, [storeBuckets]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const request = ++generationRef.current;
|
|
96
|
+
bucketsRef.current = {};
|
|
97
|
+
setBuckets({});
|
|
98
|
+
setError(null);
|
|
99
|
+
if (!enabled) { setLoading(false); return; }
|
|
100
|
+
void refreshFirstPages(request, true, stageKeysToLoadRef.current);
|
|
101
|
+
const refreshWhenVisible = () => {
|
|
102
|
+
if (document.visibilityState === "visible") void refreshFirstPages(request, false, stageKeysToLoadRef.current);
|
|
103
|
+
};
|
|
104
|
+
const interval = window.setInterval(refreshWhenVisible, REFRESH_INTERVAL_MILLISECONDS);
|
|
105
|
+
document.addEventListener("visibilitychange", refreshWhenVisible);
|
|
106
|
+
return () => {
|
|
107
|
+
generationRef.current = request + 1;
|
|
108
|
+
window.clearInterval(interval);
|
|
109
|
+
document.removeEventListener("visibilitychange", refreshWhenVisible);
|
|
110
|
+
};
|
|
111
|
+
}, [enabled, refreshFirstPages]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!enabled) return;
|
|
115
|
+
const stageKeysNotLoadedYet = stageKeysToLoad.filter(stageKey => !(stageKey in bucketsRef.current));
|
|
116
|
+
if (stageKeysNotLoadedYet.length) void refreshFirstPages(generationRef.current, false, stageKeysNotLoadedYet);
|
|
117
|
+
}, [enabled, refreshFirstPages, stageKeysToLoad]);
|
|
118
|
+
|
|
119
|
+
const stageKeysInCanonicalStage = useCallback((canonicalStage: string) => Object.keys(bucketsRef.current)
|
|
120
|
+
.filter(stageKey => canonicalContentItemStage(stageKey, getContentItemStageDefinitions()) === canonicalStage), []);
|
|
121
|
+
|
|
122
|
+
const loadMoreForStage = useCallback((canonicalStage: string) => {
|
|
123
|
+
const request = generationRef.current;
|
|
124
|
+
for (const stageKey of stageKeysInCanonicalStage(canonicalStage)) {
|
|
125
|
+
const bucket = bucketsRef.current[stageKey];
|
|
126
|
+
if (!bucket?.nextToken || stageKeysBeingExtendedRef.current.has(stageKey)) continue;
|
|
127
|
+
stageKeysBeingExtendedRef.current.add(stageKey);
|
|
128
|
+
loadNewsroomContentItemStagePage(stageKey, bucket.nextToken).then(page => {
|
|
129
|
+
if (generationRef.current !== request) return;
|
|
130
|
+
storeBuckets(previous => {
|
|
131
|
+
const current = previous[stageKey] ?? bucket;
|
|
132
|
+
const knownIds = new Set(current.versions.map(version => version.id));
|
|
133
|
+
return {
|
|
134
|
+
...previous,
|
|
135
|
+
[stageKey]: {
|
|
136
|
+
versions: [...current.versions, ...page.versions.filter(version => !knownIds.has(version.id))],
|
|
137
|
+
nextToken: page.nextToken,
|
|
138
|
+
pagesLoaded: current.pagesLoaded + 1,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}).catch(() => undefined).finally(() => { stageKeysBeingExtendedRef.current.delete(stageKey); });
|
|
143
|
+
}
|
|
144
|
+
}, [stageKeysInCanonicalStage, storeBuckets]);
|
|
145
|
+
|
|
146
|
+
const stageHasMore = useCallback((canonicalStage: string) => stageKeysInCanonicalStage(canonicalStage)
|
|
147
|
+
.some(stageKey => Boolean(bucketsRef.current[stageKey]?.nextToken)), [stageKeysInCanonicalStage]);
|
|
148
|
+
|
|
149
|
+
const versions = useMemo(() => Object.values(buckets).flatMap(bucket => bucket.versions), [buckets]);
|
|
150
|
+
return { versions, loading, error, stageHasMore, loadMoreForStage };
|
|
151
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
5
|
+
import type { SemanticNodeRecord } from "../lib/category-repository";
|
|
6
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
7
|
+
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
8
|
+
import type { createSemanticGraphSnapshot } from "../lib/semantic-graph";
|
|
9
|
+
import {
|
|
10
|
+
conceptEditedAt, conceptId, conceptTitle, conceptWikiPath, conceptsByTitle, recentlyEditedConcepts, resolveConceptLink,
|
|
11
|
+
} from "../lib/wiki";
|
|
12
|
+
import { MarkusIrPreview } from "./markus-ir-preview";
|
|
13
|
+
import { createWikiIndexPage, loadWikiIndexPage, type WikiIndexPage } from "./wiki-index-client";
|
|
14
|
+
|
|
15
|
+
type SemanticGraph = ReturnType<typeof createSemanticGraphSnapshot>;
|
|
16
|
+
|
|
17
|
+
const editedFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" });
|
|
18
|
+
|
|
19
|
+
function formatEdited(value: string): string {
|
|
20
|
+
const parsed = new Date(value);
|
|
21
|
+
return value && !Number.isNaN(parsed.getTime()) ? editedFormat.format(parsed) : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function kindLabel(kind: string): string {
|
|
25
|
+
return kind.replace(/_/g, " ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ConceptLink({ children, demo, node, onOpen }: { children?: ReactNode; demo: boolean; node: SemanticNodeRecord; onOpen: (lineageId: string) => void }) {
|
|
29
|
+
return <a className="wiki-link" data-wiki-concept-link={conceptId(node)} href={getNewsroomNavHref(conceptWikiPath(node), demo)} onClick={event => { if (event.metaKey || event.ctrlKey || event.shiftKey) return; event.preventDefault(); onOpen(conceptId(node)); }}>{children ?? conceptTitle(node)}</a>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ConceptRows({ demo, nodes, onOpen, showEdited }: { demo: boolean; nodes: SemanticNodeRecord[]; onOpen: (lineageId: string) => void; showEdited?: boolean }) {
|
|
33
|
+
if (!nodes.length) return <p className="wiki-empty">No concepts to show.</p>;
|
|
34
|
+
return <ul className="wiki-concept-list">
|
|
35
|
+
{nodes.map(node => <li key={conceptId(node)}>
|
|
36
|
+
<ConceptLink demo={demo} node={node} onOpen={onOpen} />
|
|
37
|
+
<span className="wiki-chip">{kindLabel(node.nodeKind)}</span>
|
|
38
|
+
{showEdited && conceptEditedAt(node) && <span className="wiki-meta">edited {formatEdited(conceptEditedAt(node))}</span>}
|
|
39
|
+
</li>)}
|
|
40
|
+
</ul>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function WikiIndexPageView({ demo, nodes, onOpen }: { demo: boolean; nodes: SemanticNodeRecord[]; onOpen: (lineageId: string) => void }) {
|
|
44
|
+
const [page, setPage] = useState<WikiIndexPage | null>(null);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
const [saving, setSaving] = useState(false);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
loadWikiIndexPage(demo || isLocalPreviewEnabled()).then(loaded => { if (!cancelled) setPage(loaded); }).catch((reason: unknown) => { if (!cancelled) setError(reason instanceof Error ? reason.message : "Could not load the wiki index."); });
|
|
50
|
+
return () => { cancelled = true; };
|
|
51
|
+
}, [demo]);
|
|
52
|
+
const dynamicDirectives = useMemo(() => ({
|
|
53
|
+
"recent-concepts": (attributes: Record<string, unknown>) => <ConceptRows demo={demo} nodes={recentlyEditedConcepts(nodes, { kind: typeof attributes.kind === "string" ? attributes.kind : null, limit: Number(attributes.limit) || 10 })} onOpen={onOpen} showEdited />,
|
|
54
|
+
"concept-list": (attributes: Record<string, unknown>) => <ConceptRows demo={demo} nodes={conceptsByTitle(nodes, { kind: typeof attributes.kind === "string" ? attributes.kind : null, limit: Number(attributes.limit) || null })} onOpen={onOpen} />,
|
|
55
|
+
"concept-count": (attributes: Record<string, unknown>) => <strong>{nodes.filter(node => !attributes.kind || node.nodeKind === attributes.kind).length}</strong>,
|
|
56
|
+
}), [demo, nodes, onOpen]);
|
|
57
|
+
const renderLink = (href: string, children: ReactNode) => {
|
|
58
|
+
const node = resolveConceptLink(href, nodes);
|
|
59
|
+
return node ? <ConceptLink demo={demo} node={node} onOpen={onOpen}>{children}</ConceptLink> : null;
|
|
60
|
+
};
|
|
61
|
+
if (error) return <p className="wiki-empty" role="alert">{error}</p>;
|
|
62
|
+
if (!page) return <p className="wiki-empty">Loading the wiki…</p>;
|
|
63
|
+
return <article className="wiki-page" data-wiki-index>
|
|
64
|
+
<div className="wiki-page-tools">
|
|
65
|
+
{page.itemId
|
|
66
|
+
? <Link className="wiki-tool-link" href={getNewsroomNavHref(`/newsroom/articles/${encodeURIComponent(page.itemId)}`, demo)}>Edit this page</Link>
|
|
67
|
+
: <button className="wiki-tool-link" disabled={saving} onClick={() => { setSaving(true); createWikiIndexPage(demo || isLocalPreviewEnabled()).then(setPage).catch((reason: unknown) => setError(reason instanceof Error ? reason.message : "Could not save the index.")).finally(() => setSaving(false)); }} type="button">{saving ? "Saving…" : "Save this page so it can be edited"}</button>}
|
|
68
|
+
</div>
|
|
69
|
+
<MarkusIrPreview bodyIr={page.bodyIr} dynamicDirectives={dynamicDirectives} renderLink={renderLink} />
|
|
70
|
+
</article>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ConceptPageView({ demo, graph, node, nodes, onOpen }: { demo: boolean; graph: SemanticGraph; node: SemanticNodeRecord; nodes: SemanticNodeRecord[]; onOpen: (lineageId: string) => void }) {
|
|
74
|
+
const lineage = conceptId(node);
|
|
75
|
+
const groups = graph.neighbors("semanticNode", lineage);
|
|
76
|
+
const references = graph.referencesForSemanticNode(lineage);
|
|
77
|
+
const byLineage = new Map(nodes.map(candidate => [conceptId(candidate), candidate]));
|
|
78
|
+
const aliases = (node.aliases ?? []).filter(Boolean) as string[];
|
|
79
|
+
return <article className="wiki-page" data-wiki-concept={lineage}>
|
|
80
|
+
<h1>{conceptTitle(node)}</h1>
|
|
81
|
+
<p className="wiki-meta"><span className="wiki-chip">{kindLabel(node.nodeKind)}</span> {node.status !== "current" && <span className="wiki-chip">{node.status}</span>} {conceptEditedAt(node) && <>Last edited {formatEdited(conceptEditedAt(node))}</>}{node.versionCreatedBy ? ` by ${node.versionCreatedBy}` : ""}</p>
|
|
82
|
+
{aliases.length > 0 && <p className="wiki-meta">Also known as {aliases.join(", ")}</p>}
|
|
83
|
+
{node.description ? node.description.split(/\n{2,}/).map((paragraph, index) => <p key={index}>{paragraph}</p>) : <p className="wiki-empty">This concept has no description yet.</p>}
|
|
84
|
+
<section data-wiki-section="connections">
|
|
85
|
+
<h2>Connections</h2>
|
|
86
|
+
{groups.length ? groups.map(group => <div className="wiki-connection-group" key={`${group.direction}-${group.predicate}`}>
|
|
87
|
+
<h3>{group.label}</h3>
|
|
88
|
+
<ul className="wiki-concept-list">
|
|
89
|
+
{group.objects.map(object => {
|
|
90
|
+
const target = object.kind === "semanticNode" ? byLineage.get(object.lineageId) : undefined;
|
|
91
|
+
return <li key={`${object.kind}-${object.lineageId}`}>{target ? <ConceptLink demo={demo} node={target} onOpen={onOpen} /> : <span>{object.label}</span>}<span className="wiki-chip">{target ? kindLabel(target.nodeKind) : object.kind}</span></li>;
|
|
92
|
+
})}
|
|
93
|
+
</ul>
|
|
94
|
+
</div>) : <p className="wiki-empty">No connections recorded.</p>}
|
|
95
|
+
</section>
|
|
96
|
+
<section data-wiki-section="references">
|
|
97
|
+
<h2>References</h2>
|
|
98
|
+
{references.length ? <ul className="wiki-concept-list">
|
|
99
|
+
{references.map(reference => <li key={reference.lineageId}><Link href={getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(reference.lineageId)}`, demo)}>{reference.label}</Link></li>)}
|
|
100
|
+
</ul> : <p className="wiki-empty">No references recorded.</p>}
|
|
101
|
+
</section>
|
|
102
|
+
</article>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The concept wiki: the front page is a content item whose Markus body can hold live components
|
|
107
|
+
* (recently edited concepts, concept lists), and every concept in the knowledge graph is a page
|
|
108
|
+
* linked to the concepts and references it connects to.
|
|
109
|
+
*/
|
|
110
|
+
export function WikiConceptsView({ demo = false, graph, initialNodeLineageId, nodes }: { demo?: boolean; graph: SemanticGraph; initialNodeLineageId?: string | null; nodes: SemanticNodeRecord[] }) {
|
|
111
|
+
const [selected, setSelected] = useState<string | null>(initialNodeLineageId ?? null);
|
|
112
|
+
const [search, setSearch] = useState("");
|
|
113
|
+
const [kind, setKind] = useState("");
|
|
114
|
+
useEffect(() => { setSelected(initialNodeLineageId ?? null); }, [initialNodeLineageId]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const onPop = () => {
|
|
117
|
+
const match = window.location.pathname.match(/\/concepts\/([^/?#]+)/);
|
|
118
|
+
setSelected(match ? decodeURIComponent(match[1]) : null);
|
|
119
|
+
};
|
|
120
|
+
window.addEventListener("popstate", onPop);
|
|
121
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
122
|
+
}, []);
|
|
123
|
+
const open = (lineageId: string | null) => {
|
|
124
|
+
setSelected(lineageId);
|
|
125
|
+
const target = getNewsroomNavHref(lineageId ? `/newsroom/concepts/${encodeURIComponent(lineageId)}` : "/newsroom/concepts", demo);
|
|
126
|
+
if (`${window.location.pathname}${window.location.search}` !== target) window.history.pushState(null, "", target);
|
|
127
|
+
};
|
|
128
|
+
const node = selected ? nodes.find(candidate => conceptId(candidate) === selected || candidate.id === selected) ?? null : null;
|
|
129
|
+
const kinds = useMemo(() => Array.from(new Set(nodes.map(candidate => candidate.nodeKind))).sort(), [nodes]);
|
|
130
|
+
const matches = useMemo(() => {
|
|
131
|
+
const needle = search.trim().toLowerCase();
|
|
132
|
+
return conceptsByTitle(nodes, { kind: kind || null }).filter(candidate => !needle || conceptTitle(candidate).toLowerCase().includes(needle) || (candidate.aliases ?? []).some(alias => alias?.toLowerCase().includes(needle)));
|
|
133
|
+
}, [kind, nodes, search]);
|
|
134
|
+
return <div className="wiki" data-wiki>
|
|
135
|
+
<nav aria-label="Wiki" className="wiki-sidebar">
|
|
136
|
+
<button className="wiki-home" data-wiki-home aria-current={node ? undefined : "page"} onClick={() => open(null)} type="button">Wiki home</button>
|
|
137
|
+
<input aria-label="Search concepts" className="wiki-search" onChange={event => setSearch(event.target.value)} placeholder="Search concepts" value={search} />
|
|
138
|
+
{kinds.length > 1 && <select aria-label="Concept kind" className="wiki-kind" onChange={event => setKind(event.target.value)} value={kind}>
|
|
139
|
+
<option value="">All kinds</option>
|
|
140
|
+
{kinds.map(option => <option key={option} value={option}>{kindLabel(option)}</option>)}
|
|
141
|
+
</select>}
|
|
142
|
+
<ul className="wiki-sidebar-list">
|
|
143
|
+
{matches.map(candidate => <li key={conceptId(candidate)}><a aria-current={node && conceptId(node) === conceptId(candidate) ? "page" : undefined} href={getNewsroomNavHref(conceptWikiPath(candidate), demo)} onClick={event => { if (event.metaKey || event.ctrlKey || event.shiftKey) return; event.preventDefault(); open(conceptId(candidate)); }}>{conceptTitle(candidate)}</a></li>)}
|
|
144
|
+
</ul>
|
|
145
|
+
</nav>
|
|
146
|
+
<div className="wiki-main">
|
|
147
|
+
{node
|
|
148
|
+
? <ConceptPageView demo={demo} graph={graph} node={node} nodes={nodes} onOpen={open} />
|
|
149
|
+
: selected ? <p className="wiki-empty" role="alert">That concept page was not found.</p>
|
|
150
|
+
: <WikiIndexPageView demo={demo} nodes={nodes} onOpen={open} />}
|
|
151
|
+
</div>
|
|
152
|
+
</div>;
|
|
153
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { Schema } from "../amplify/data/resource";
|
|
5
|
+
import { resolveArticlesBackend } from "../lib/newsroom-articles";
|
|
6
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
7
|
+
import { DEFAULT_WIKI_INDEX_MARKUS, WIKI_INDEX_SLUG, WIKI_PAGE_ITEM_TYPE } from "../lib/wiki";
|
|
8
|
+
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
9
|
+
|
|
10
|
+
export type WikiIndexPage = {
|
|
11
|
+
/** The id of the stored content item, or null while the wiki still shows the built-in starter content. */
|
|
12
|
+
itemId: string | null;
|
|
13
|
+
bodyMarkus: string;
|
|
14
|
+
bodyIr: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type GraphqlResponse = { data?: { itemBySlug?: { items?: Record<string, unknown>[] | null } | null } | null; errors?: { message?: string }[] | null };
|
|
18
|
+
|
|
19
|
+
async function storedIndexRow(preview: boolean): Promise<Record<string, unknown> | null> {
|
|
20
|
+
if (preview || isLocalPreviewEnabled()) return null;
|
|
21
|
+
configureAmplifyClient();
|
|
22
|
+
const client = generateClient<Schema>() as unknown as { graphql: (options: Record<string, unknown>) => Promise<GraphqlResponse> };
|
|
23
|
+
const response = await client.graphql({
|
|
24
|
+
query: "query ($slug: String!) { itemBySlug(slug: $slug) { items { id versionNumber versionState bodyMarkus bodyIr } } }",
|
|
25
|
+
variables: { slug: WIKI_INDEX_SLUG },
|
|
26
|
+
authMode: "userPool",
|
|
27
|
+
});
|
|
28
|
+
if (response.errors?.length) throw new Error(response.errors.map(error => error.message ?? "GraphQL error").join("; "));
|
|
29
|
+
const rows = (response.data?.itemBySlug?.items ?? []).filter(Boolean) as Record<string, unknown>[];
|
|
30
|
+
const ranked = rows.sort((left, right) => Number(right.versionNumber ?? 0) - Number(left.versionNumber ?? 0));
|
|
31
|
+
return ranked.find(row => row.versionState === "current") ?? ranked[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function derive(bodyMarkus: string, preview: boolean): Promise<unknown> {
|
|
35
|
+
const result = await resolveArticlesBackend(preview).actions.deriveMarkus({ frontMatterYaml: null, bodyMarkus, includeIr: true });
|
|
36
|
+
return result.bodyIr ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Loads the wiki's front page content item, falling back to the starter content until one is saved. */
|
|
40
|
+
export async function loadWikiIndexPage(preview: boolean): Promise<WikiIndexPage> {
|
|
41
|
+
const row = await storedIndexRow(preview);
|
|
42
|
+
const bodyMarkus = typeof row?.bodyMarkus === "string" && row.bodyMarkus.trim() ? row.bodyMarkus : DEFAULT_WIKI_INDEX_MARKUS;
|
|
43
|
+
const storedIr = row?.bodyIr;
|
|
44
|
+
return { itemId: row ? String(row.id) : null, bodyMarkus, bodyIr: storedIr ?? await derive(bodyMarkus, preview) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Saves the starter index as the wiki-index content item so editors can change it like any other page. */
|
|
48
|
+
export async function createWikiIndexPage(preview: boolean): Promise<WikiIndexPage> {
|
|
49
|
+
const saved = await resolveArticlesBackend(preview).actions.saveItemDraft({ id: null, type: WIKI_PAGE_ITEM_TYPE, slug: WIKI_INDEX_SLUG, section: null, frontMatterYaml: `title: Concept wiki\n`, bodyMarkus: DEFAULT_WIKI_INDEX_MARKUS, aliases: [], expectedContentHash: null });
|
|
50
|
+
return { itemId: saved.item.id, bodyMarkus: DEFAULT_WIKI_INDEX_MARKUS, bodyIr: await derive(DEFAULT_WIKI_INDEX_MARKUS, preview) };
|
|
51
|
+
}
|
|
@@ -43,11 +43,22 @@ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
|
43
43
|
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
44
44
|
* principal in the account that may assume it); no token is stored anywhere.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
* until Amplify's GitHub App is
|
|
48
|
-
* (docs/site-hosting.md).
|
|
46
|
+
* Unless `repositoryAccessTokenSecretArn` is set, no repository and no access
|
|
47
|
+
* token are configured: the apps are manual-deploy until Amplify's GitHub App is
|
|
48
|
+
* connected once in the console (docs/site-hosting.md). When it is set, both apps
|
|
49
|
+
* are created against `repository` and read the GitHub token from that Secrets
|
|
50
|
+
* Manager secret through a CloudFormation dynamic reference, so the token never
|
|
51
|
+
* appears in site.json or the synthesized template. The Papyrus backend itself is created inside the CMS
|
|
49
52
|
* app by `ampx pipeline-deploy` from the production build.
|
|
50
53
|
*/
|
|
54
|
+
function repositoryConnectionProperties(config) {
|
|
55
|
+
if (config.repositoryAccessTokenSecretArn === undefined)
|
|
56
|
+
return {};
|
|
57
|
+
return {
|
|
58
|
+
repository: config.repository,
|
|
59
|
+
accessToken: `{{resolve:secretsmanager:${config.repositoryAccessTokenSecretArn}:SecretString}}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
51
62
|
export class AmplifyAppShellStack extends Stack {
|
|
52
63
|
cmsAppId;
|
|
53
64
|
readerAppId;
|
|
@@ -114,7 +125,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
114
125
|
resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
|
|
115
126
|
}));
|
|
116
127
|
}
|
|
128
|
+
const repositoryConnection = repositoryConnectionProperties(config);
|
|
117
129
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
130
|
+
...repositoryConnection,
|
|
118
131
|
name: cmsAppName,
|
|
119
132
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
120
133
|
platform: "WEB_COMPUTE",
|
|
@@ -332,6 +345,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
332
345
|
resources: ["arn:aws:s3:::amplify-*/media/*"],
|
|
333
346
|
}));
|
|
334
347
|
const readerApp = new amplify.CfnApp(this, "ReaderApp", {
|
|
348
|
+
...repositoryConnectionProperties(config),
|
|
335
349
|
name: readerAppName,
|
|
336
350
|
description: `Papyrus static reader for ${config.siteId} (WEB)`,
|
|
337
351
|
platform: "WEB",
|
|
@@ -46,11 +46,22 @@ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
|
46
46
|
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
47
47
|
* principal in the account that may assume it); no token is stored anywhere.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
* until Amplify's GitHub App is
|
|
51
|
-
* (docs/site-hosting.md).
|
|
49
|
+
* Unless `repositoryAccessTokenSecretArn` is set, no repository and no access
|
|
50
|
+
* token are configured: the apps are manual-deploy until Amplify's GitHub App is
|
|
51
|
+
* connected once in the console (docs/site-hosting.md). When it is set, both apps
|
|
52
|
+
* are created against `repository` and read the GitHub token from that Secrets
|
|
53
|
+
* Manager secret through a CloudFormation dynamic reference, so the token never
|
|
54
|
+
* appears in site.json or the synthesized template. The Papyrus backend itself is created inside the CMS
|
|
52
55
|
* app by `ampx pipeline-deploy` from the production build.
|
|
53
56
|
*/
|
|
57
|
+
function repositoryConnectionProperties(config: AmplifyAppShellSiteConfig): { repository?: string; accessToken?: string } {
|
|
58
|
+
if (config.repositoryAccessTokenSecretArn === undefined) return {};
|
|
59
|
+
return {
|
|
60
|
+
repository: config.repository,
|
|
61
|
+
accessToken: `{{resolve:secretsmanager:${config.repositoryAccessTokenSecretArn}:SecretString}}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
export class AmplifyAppShellStack extends Stack {
|
|
55
66
|
public readonly cmsAppId?: string;
|
|
56
67
|
public readonly readerAppId?: string;
|
|
@@ -130,7 +141,10 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
130
141
|
);
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
const repositoryConnection = repositoryConnectionProperties(config);
|
|
145
|
+
|
|
133
146
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
147
|
+
...repositoryConnection,
|
|
134
148
|
name: cmsAppName,
|
|
135
149
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
136
150
|
platform: "WEB_COMPUTE",
|
|
@@ -389,6 +403,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
389
403
|
);
|
|
390
404
|
|
|
391
405
|
const readerApp = new amplify.CfnApp(this, "ReaderApp", {
|
|
406
|
+
...repositoryConnectionProperties(config),
|
|
392
407
|
name: readerAppName,
|
|
393
408
|
description: `Papyrus static reader for ${config.siteId} (WEB)`,
|
|
394
409
|
platform: "WEB",
|
package/infra/site-config.js
CHANGED
|
@@ -5,6 +5,7 @@ export function hasCmsApp(config) {
|
|
|
5
5
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
6
6
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
7
7
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
8
|
+
const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
8
9
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
9
10
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
10
11
|
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -297,9 +298,12 @@ function validateOauthRedirectUrls(config) {
|
|
|
297
298
|
}
|
|
298
299
|
export function parseSiteConfig(raw) {
|
|
299
300
|
const record = requireObject(raw, "site");
|
|
300
|
-
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
301
|
+
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
301
302
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
302
303
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
304
|
+
const repositoryAccessTokenSecretArn = record.repositoryAccessTokenSecretArn === undefined
|
|
305
|
+
? undefined
|
|
306
|
+
: requireString(record.repositoryAccessTokenSecretArn, "repositoryAccessTokenSecretArn", SECRETS_MANAGER_SECRET_ARN_PATTERN, "Secrets Manager secret ARN");
|
|
303
307
|
const brand = requireString(record.brand, "brand");
|
|
304
308
|
const frontend = requireString(record.frontend, "frontend");
|
|
305
309
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
@@ -382,6 +386,7 @@ export function parseSiteConfig(raw) {
|
|
|
382
386
|
const config = {
|
|
383
387
|
siteId,
|
|
384
388
|
repository,
|
|
389
|
+
repositoryAccessTokenSecretArn,
|
|
385
390
|
brand,
|
|
386
391
|
frontend,
|
|
387
392
|
hostedZoneId,
|
package/infra/site-config.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type CmsConfig = {
|
|
|
47
47
|
export type AmplifyAppShellSiteConfig = {
|
|
48
48
|
siteId: string;
|
|
49
49
|
repository: string;
|
|
50
|
+
repositoryAccessTokenSecretArn?: string;
|
|
50
51
|
brand: string;
|
|
51
52
|
frontend: "pretext" | "markus-static";
|
|
52
53
|
hostedZoneId?: string;
|
|
@@ -85,6 +86,7 @@ export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
|
85
86
|
|
|
86
87
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
87
88
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
89
|
+
const SECRETS_MANAGER_SECRET_ARN_PATTERN = /^arn:aws:secretsmanager:[a-z0-9-]+:\d{12}:secret:[A-Za-z0-9/_+=.@-]+$/;
|
|
88
90
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
89
91
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
90
92
|
const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -392,12 +394,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
392
394
|
const record = requireObject(raw, "site");
|
|
393
395
|
rejectUnknownKeys(
|
|
394
396
|
record,
|
|
395
|
-
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
397
|
+
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
396
398
|
"site",
|
|
397
399
|
);
|
|
398
400
|
|
|
399
401
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
400
402
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
403
|
+
const repositoryAccessTokenSecretArn = record.repositoryAccessTokenSecretArn === undefined
|
|
404
|
+
? undefined
|
|
405
|
+
: requireString(record.repositoryAccessTokenSecretArn, "repositoryAccessTokenSecretArn", SECRETS_MANAGER_SECRET_ARN_PATTERN, "Secrets Manager secret ARN");
|
|
401
406
|
const brand = requireString(record.brand, "brand");
|
|
402
407
|
const frontend = requireString(record.frontend, "frontend");
|
|
403
408
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
@@ -480,6 +485,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
480
485
|
const config: AmplifyAppShellSiteConfig = {
|
|
481
486
|
siteId,
|
|
482
487
|
repository,
|
|
488
|
+
repositoryAccessTokenSecretArn,
|
|
483
489
|
brand,
|
|
484
490
|
frontend,
|
|
485
491
|
hostedZoneId,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import approvedReport0 from "./approved-priming-proofs/papyrus.json";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Priming proof reports a publication owner approved for visitors, keyed by site brand id.
|
|
3
|
-
* A report reaches this map only through a reviewed commit
|
|
4
|
-
* `publications/priming-proofs/<brand id>.json` file; see docs/cyclotron-priming.md.
|
|
5
|
+
* A report reaches this map only through a reviewed commit made by scripts/approve-priming-proof-report.ts.
|
|
5
6
|
*/
|
|
6
|
-
export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {
|
|
7
|
+
export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {
|
|
8
|
+
"papyrus": approvedReport0,
|
|
9
|
+
};
|