@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
|
@@ -3,12 +3,10 @@ import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-k
|
|
|
3
3
|
import {
|
|
4
4
|
buildNewsroomIndexWebPath,
|
|
5
5
|
effectiveAssignmentsIndexFilters,
|
|
6
|
-
effectiveInsightsIndexFilters,
|
|
7
6
|
effectiveMessagesIndexFilters,
|
|
8
7
|
effectiveReferencesIndexFilters,
|
|
9
8
|
normalizeReferenceIndexOrder,
|
|
10
9
|
type AssignmentsIndexFilters,
|
|
11
|
-
type InsightsIndexFilters,
|
|
12
10
|
type MessagesIndexFilters,
|
|
13
11
|
type NewsroomIndexTab,
|
|
14
12
|
type ReferencesIndexFilters,
|
|
@@ -27,7 +25,6 @@ export type PapyrusWebUiContext = {
|
|
|
27
25
|
const NEWSROOM_TAB_IDS = new Set([
|
|
28
26
|
"overview",
|
|
29
27
|
"messages",
|
|
30
|
-
"insights",
|
|
31
28
|
"assignments",
|
|
32
29
|
"references",
|
|
33
30
|
"topics",
|
|
@@ -123,21 +120,10 @@ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
|
123
120
|
});
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
if ((tab === "references" || tab === "messages" || tab === "assignments"
|
|
123
|
+
if ((tab === "references" || tab === "messages" || tab === "assignments") && segments.length === 2) {
|
|
127
124
|
return newsroomIndexLocation(tab as NewsroomIndexTab, normalized, url.searchParams);
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
if (tab === "insights" && segments[2]) {
|
|
131
|
-
const threadId = decodeURIComponent(segments[2]);
|
|
132
|
-
const objectUri = `papyrus://message/${encodeURIComponent(threadId)}`;
|
|
133
|
-
return location(objectUri, normalized, {
|
|
134
|
-
papyrusObjectUri: objectUri,
|
|
135
|
-
newsroomTab: "insights",
|
|
136
|
-
viewMode: "detail",
|
|
137
|
-
label: `Insight thread ${threadId}`,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
127
|
if ((tab === "references" || tab === "messages" || tab === "assignments") && segments[2]) {
|
|
142
128
|
const objectId = decodeURIComponent(segments[2]);
|
|
143
129
|
const kind = tab === "references" ? "reference" : tab === "messages" ? "message" : "assignment";
|
|
@@ -277,7 +263,7 @@ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
|
|
|
277
263
|
}
|
|
278
264
|
|
|
279
265
|
if (NEWSROOM_TAB_IDS.has(id)) {
|
|
280
|
-
if (id === "references" || id === "messages" || id === "assignments"
|
|
266
|
+
if (id === "references" || id === "messages" || id === "assignments") {
|
|
281
267
|
const mapped = newsroomIndexUriToWebPath(raw, `${id}/index`);
|
|
282
268
|
if (mapped) return mapped;
|
|
283
269
|
}
|
|
@@ -347,9 +333,6 @@ function readIndexFiltersFromSearchParams(tab: NewsroomIndexTab, searchParams: U
|
|
|
347
333
|
kind: searchParams.get("kind")?.trim() ?? undefined,
|
|
348
334
|
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
349
335
|
});
|
|
350
|
-
if (tab === "insights") return effectiveInsightsIndexFilters({
|
|
351
|
-
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
352
|
-
});
|
|
353
336
|
return effectiveAssignmentsIndexFilters({
|
|
354
337
|
status: searchParams.get("status")?.trim() ?? undefined,
|
|
355
338
|
type: searchParams.get("type")?.trim() ?? undefined,
|
|
@@ -368,9 +351,6 @@ function buildNewsroomIndexLocationUri(tab: NewsroomIndexTab, filters: Record<st
|
|
|
368
351
|
const normalized = filters as MessagesIndexFilters;
|
|
369
352
|
if (normalized.kind?.trim()) segments.push("kind", normalized.kind.trim());
|
|
370
353
|
if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
|
|
371
|
-
} else if (tab === "insights") {
|
|
372
|
-
const normalized = filters as InsightsIndexFilters;
|
|
373
|
-
if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
|
|
374
354
|
} else {
|
|
375
355
|
const normalized = filters as AssignmentsIndexFilters;
|
|
376
356
|
if (normalized.status?.trim()) segments.push("status", normalized.status.trim());
|
|
@@ -400,7 +380,7 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
|
|
|
400
380
|
const segments = objectId.split("/").filter(Boolean);
|
|
401
381
|
if (!segments.length) return null;
|
|
402
382
|
const tab = segments[0];
|
|
403
|
-
if (tab !== "references" && tab !== "messages" && tab !== "assignments"
|
|
383
|
+
if (tab !== "references" && tab !== "messages" && tab !== "assignments") return null;
|
|
404
384
|
if (segments.length === 1) {
|
|
405
385
|
return { tab, filters: readIndexFiltersFromSearchParams(tab, new URLSearchParams()) };
|
|
406
386
|
}
|
|
@@ -414,7 +394,6 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
|
|
|
414
394
|
}
|
|
415
395
|
if (tab === "references") return { tab, filters: effectiveReferencesIndexFilters(filters) };
|
|
416
396
|
if (tab === "messages") return { tab, filters: effectiveMessagesIndexFilters(filters) };
|
|
417
|
-
if (tab === "insights") return { tab, filters: effectiveInsightsIndexFilters(filters) };
|
|
418
397
|
return { tab, filters: effectiveAssignmentsIndexFilters(filters) };
|
|
419
398
|
}
|
|
420
399
|
|
package/lib/pretext-layout.ts
CHANGED
|
@@ -9,6 +9,27 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export { layoutNextLine, prepareWithSegments, type LayoutCursor, type PreparedTextWithSegments };
|
|
11
11
|
|
|
12
|
+
const preparedTextCache = new Map<string, PreparedTextWithSegments>();
|
|
13
|
+
let preparedTextCacheMissCount = 0;
|
|
14
|
+
|
|
15
|
+
export function getPreparedTextCacheMissCount(): number {
|
|
16
|
+
return preparedTextCacheMissCount;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function prepareWithSegmentsCached(
|
|
20
|
+
text: string,
|
|
21
|
+
font: string,
|
|
22
|
+
options?: { whiteSpace?: "normal" | "pre-wrap" },
|
|
23
|
+
): PreparedTextWithSegments {
|
|
24
|
+
const key = `${options?.whiteSpace ?? "normal"}\u0000${font}\u0000${text}`;
|
|
25
|
+
const cached = preparedTextCache.get(key);
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
preparedTextCacheMissCount += 1;
|
|
28
|
+
const prepared = prepareWithSegments(text, font, options);
|
|
29
|
+
preparedTextCache.set(key, prepared);
|
|
30
|
+
return prepared;
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export type TextLine = {
|
|
13
34
|
text: string;
|
|
14
35
|
width: number;
|
|
@@ -184,5 +205,6 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
export function clearPretextMeasurementCache(): void {
|
|
208
|
+
preparedTextCache.clear();
|
|
187
209
|
clearCache();
|
|
188
210
|
}
|
|
@@ -36,6 +36,7 @@ export const primingProofReportSchema = z.object({
|
|
|
36
36
|
fingerprint: z.string().min(1),
|
|
37
37
|
frozenAt: z.string().min(1),
|
|
38
38
|
source: z.string().min(1),
|
|
39
|
+
design: z.string().min(1).optional(),
|
|
39
40
|
}),
|
|
40
41
|
priming: z.object({
|
|
41
42
|
acceptedExamples: z.number().int().min(0),
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface ProspectusPublishingAccountChoice {
|
|
2
|
+
accountId: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
role: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ProspectusAccountState =
|
|
8
|
+
| { status: "unavailable" }
|
|
9
|
+
| { status: "signedOut"; signInUrl: string }
|
|
10
|
+
| {
|
|
11
|
+
status: "signedIn";
|
|
12
|
+
publishingAccounts: ProspectusPublishingAccountChoice[];
|
|
13
|
+
selectedAccountId: string | null;
|
|
14
|
+
cyclotronLabAccess: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type FetchImplementation = (input: string, init?: RequestInit) => Promise<Response>;
|
|
18
|
+
|
|
19
|
+
const SESSION_PATH = "/api/auth/prospectus/session";
|
|
20
|
+
const SELECTION_PATH = "/api/auth/prospectus/selection";
|
|
21
|
+
const ACCOUNTS_PATH = "/api/auth/prospectus/accounts";
|
|
22
|
+
const SIGN_IN_PATH = "/api/auth/prospectus/sign-in";
|
|
23
|
+
|
|
24
|
+
export function prospectusSignInUrl(returnPath: string): string {
|
|
25
|
+
return `${SIGN_IN_PATH}?returnTo=${encodeURIComponent(returnPath)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadProspectusAccountState(
|
|
29
|
+
returnPath: string,
|
|
30
|
+
fetchImplementation: FetchImplementation = fetch,
|
|
31
|
+
): Promise<ProspectusAccountState> {
|
|
32
|
+
let response: Response;
|
|
33
|
+
try {
|
|
34
|
+
response = await fetchImplementation(SESSION_PATH, { credentials: "same-origin", cache: "no-store" });
|
|
35
|
+
} catch {
|
|
36
|
+
return { status: "unavailable" };
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) return { status: "unavailable" };
|
|
39
|
+
const body = await response.json().catch(() => null) as
|
|
40
|
+
| { signedIn?: boolean; publishingAccounts?: ProspectusPublishingAccountChoice[]; selectedAccountId?: string | null; cyclotronLabAccess?: boolean }
|
|
41
|
+
| null;
|
|
42
|
+
if (!body) return { status: "unavailable" };
|
|
43
|
+
if (!body.signedIn) return { status: "signedOut", signInUrl: prospectusSignInUrl(returnPath) };
|
|
44
|
+
return {
|
|
45
|
+
status: "signedIn",
|
|
46
|
+
publishingAccounts: Array.isArray(body.publishingAccounts) ? body.publishingAccounts : [],
|
|
47
|
+
selectedAccountId: body.selectedAccountId ?? null,
|
|
48
|
+
cyclotronLabAccess: body.cyclotronLabAccess === true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function selectProspectusAccount(
|
|
53
|
+
accountId: string,
|
|
54
|
+
fetchImplementation: FetchImplementation = fetch,
|
|
55
|
+
): Promise<{ selectedAccountId: string; role: string } | { error: string }> {
|
|
56
|
+
const response = await fetchImplementation(SELECTION_PATH, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
credentials: "same-origin",
|
|
59
|
+
headers: { "content-type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ accountId }),
|
|
61
|
+
});
|
|
62
|
+
const body = await response.json().catch(() => null) as { selectedAccountId?: string; role?: string; message?: string } | null;
|
|
63
|
+
if (!response.ok || !body?.selectedAccountId) return { error: body?.message ?? "That account could not be selected." };
|
|
64
|
+
return { selectedAccountId: body.selectedAccountId, role: body.role ?? "" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a publishing account for the signed-in person and select it. Each call carries a fresh idempotency key,
|
|
69
|
+
* so a retried request after a dropped response cannot create a second account.
|
|
70
|
+
*/
|
|
71
|
+
export async function createProspectusAccount(
|
|
72
|
+
name: string,
|
|
73
|
+
fetchImplementation: FetchImplementation = fetch,
|
|
74
|
+
idempotencyKey: string = crypto.randomUUID(),
|
|
75
|
+
): Promise<{ selectedAccountId: string; role: string } | { error: string }> {
|
|
76
|
+
let response: Response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fetchImplementation(ACCOUNTS_PATH, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
credentials: "same-origin",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify({ name: name.trim() || undefined, idempotencyKey }),
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
return { error: "The publishing account could not be created. Check your connection and try again." };
|
|
86
|
+
}
|
|
87
|
+
const body = await response.json().catch(() => null) as { selectedAccountId?: string; role?: string; message?: string } | null;
|
|
88
|
+
if (!response.ok || !body?.selectedAccountId) return { error: body?.message ?? "The publishing account could not be created." };
|
|
89
|
+
return { selectedAccountId: body.selectedAccountId, role: body.role ?? "" };
|
|
90
|
+
}
|
|
@@ -282,3 +282,48 @@ export async function handleProspectusAccountSelection(request: NextRequest, opt
|
|
|
282
282
|
return signInFailedResponse(503, "prospectus_unavailable", "Prospectus could not be reached.");
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
|
+
|
|
286
|
+
const MAXIMUM_PUBLISHING_ACCOUNT_NAME_LENGTH = 150;
|
|
287
|
+
const DEFAULT_PUBLISHING_ACCOUNT_NAME = "Publishing account";
|
|
288
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
289
|
+
|
|
290
|
+
export async function handleProspectusPublishingAccountCreation(request: NextRequest, options: ProspectusSignInRouteOptions = {}): Promise<NextResponse> {
|
|
291
|
+
const runtime = await signInRuntimeFor(request, options);
|
|
292
|
+
if (!runtime) return signInUnavailableResponse();
|
|
293
|
+
if (request.headers.get("origin") !== request.nextUrl.origin) {
|
|
294
|
+
return signInFailedResponse(403, "cross_origin_creation", "Create an account from this site.");
|
|
295
|
+
}
|
|
296
|
+
const nowInSeconds = currentTimeInSeconds(options);
|
|
297
|
+
const session = signedInProspectusSession(request, runtime.sealingSecret, nowInSeconds);
|
|
298
|
+
if (!session) return signInFailedResponse(401, "signed_out", "Sign in first.");
|
|
299
|
+
const body = await request.json().catch(() => null) as { name?: unknown; idempotencyKey?: unknown } | null;
|
|
300
|
+
const requestedName = typeof body?.name === "string" ? body.name.trim() : "";
|
|
301
|
+
const accountName = requestedName || DEFAULT_PUBLISHING_ACCOUNT_NAME;
|
|
302
|
+
if (accountName.length > MAXIMUM_PUBLISHING_ACCOUNT_NAME_LENGTH) {
|
|
303
|
+
return signInFailedResponse(400, "account_name_too_long", "Name the account in 150 characters or fewer.");
|
|
304
|
+
}
|
|
305
|
+
const idempotencyKey = typeof body?.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
306
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(idempotencyKey)) {
|
|
307
|
+
return signInFailedResponse(400, "idempotency_key_required", "Send a UUID idempotency key.");
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const created = await runtime.accounts.createAccount(session.accessToken, accountName, idempotencyKey);
|
|
311
|
+
const selection = await freshlyAuthorizedPublishingSelection(runtime, session, created.account.id, siteProductFor(options));
|
|
312
|
+
if (!selection) return signInFailedResponse(403, "not_a_publishing_account", "The new account does not grant publishing access on this site.");
|
|
313
|
+
const response = NextResponse.json(
|
|
314
|
+
{ selectedAccountId: selection.accountId, role: selection.role, publicationCapabilities: selection.publicationCapabilities },
|
|
315
|
+
{ status: 201, headers: NO_STORE_HEADERS },
|
|
316
|
+
);
|
|
317
|
+
const selectedSession: ProspectusSignedInSession = { ...session, selectedAccountId: selection.accountId };
|
|
318
|
+
response.cookies.set(
|
|
319
|
+
PROSPECTUS_SESSION_COOKIE,
|
|
320
|
+
sealProspectusCookie(runtime.sealingSecret, PROSPECTUS_SESSION_COOKIE, selectedSession, session.accessTokenExpiresAt),
|
|
321
|
+
sealedCookieAttributes(session.accessTokenExpiresAt, nowInSeconds),
|
|
322
|
+
);
|
|
323
|
+
return response;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error instanceof ProspectusAccountsError) return signInFailedResponse(error.status, error.code, error.message);
|
|
326
|
+
console.error(`Prospectus account creation failed (${error instanceof Error ? error.name : "error"}).`);
|
|
327
|
+
return signInFailedResponse(503, "prospectus_unavailable", "Prospectus could not be reached.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -49,6 +49,7 @@ export interface ProspectusRequest {
|
|
|
49
49
|
body?: unknown;
|
|
50
50
|
humanAccessToken?: string;
|
|
51
51
|
identityToken?: string;
|
|
52
|
+
idempotencyKey?: string;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export type ProspectusTransport = (request: ProspectusRequest) => Promise<unknown>;
|
|
@@ -84,7 +85,7 @@ export function signedProspectusTransport(options: SignedProspectusTransportOpti
|
|
|
84
85
|
}
|
|
85
86
|
const runtimeCredentials = options.runtimeCredentials ?? defaultProvider();
|
|
86
87
|
const fetcher = options.fetcher ?? fetch;
|
|
87
|
-
return async ({ path, method, body, humanAccessToken, identityToken }) => {
|
|
88
|
+
return async ({ path, method, body, humanAccessToken, identityToken, idempotencyKey }) => {
|
|
88
89
|
const credentials = await runtimeCredentials();
|
|
89
90
|
if (!credentials.sessionToken || (credentials.expiration && credentials.expiration.getTime() <= Date.now())) {
|
|
90
91
|
throw new Error("Prospectus calls require the site's temporary runtime credentials; static access keys are refused.");
|
|
@@ -95,6 +96,7 @@ export function signedProspectusTransport(options: SignedProspectusTransportOpti
|
|
|
95
96
|
host: url.host,
|
|
96
97
|
...(humanAccessToken ? { "x-prospectus-human-token": humanAccessToken } : {}),
|
|
97
98
|
...(identityToken ? { "x-prospectus-identity-token": identityToken } : {}),
|
|
99
|
+
...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
|
|
98
100
|
...(payload ? { "content-type": "application/json" } : {}),
|
|
99
101
|
};
|
|
100
102
|
const signer = new SignatureV4({ service: "execute-api", region: options.region, credentials, sha256: Sha256 });
|
|
@@ -144,6 +146,16 @@ export class ProspectusSiteAccounts {
|
|
|
144
146
|
}) as Promise<ProspectusAccountAuthorization>;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
createAccount(humanAccessToken: string, accountName: string, idempotencyKey: string): Promise<{ account: { id: string; name: string; role: ProspectusRole } }> {
|
|
150
|
+
return this.transport({
|
|
151
|
+
path: "/v1/accounts",
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: { name: accountName },
|
|
154
|
+
humanAccessToken,
|
|
155
|
+
idempotencyKey,
|
|
156
|
+
}) as Promise<{ account: { id: string; name: string; role: ProspectusRole } }>;
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
accountDirectory(humanAccessToken: string): Promise<{ person: ProspectusPerson | null; accounts: ProspectusDirectoryAccount[] }> {
|
|
148
160
|
return this.transport({ path: "/v1/account-directory", method: "GET", humanAccessToken }) as Promise<{
|
|
149
161
|
person: ProspectusPerson | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROSPECTUS_SESSION_COOKIE,
|
|
3
|
+
loadProspectusSessionSealingSecret,
|
|
4
|
+
openProspectusCookie,
|
|
5
|
+
type ProspectusSignedInSession,
|
|
6
|
+
} from "./prospectus-session";
|
|
7
|
+
import { tenantIdForProspectusAccount } from "./tenant-keys";
|
|
8
|
+
|
|
9
|
+
type CookieReader = { get(name: string): { value: string } | undefined };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The tenant of the publishing account the signed-in person selected, read from the sealed Prospectus session
|
|
13
|
+
* cookie. Returns undefined on a site without Prospectus, with no session, or with no account selected, so the
|
|
14
|
+
* caller falls back to the site's own tenant (PAPYRUS_TENANT_ID).
|
|
15
|
+
*/
|
|
16
|
+
export async function selectedTenantIdFromCookies(cookies: CookieReader, loadSecret: () => Promise<string | null> = loadProspectusSessionSealingSecret): Promise<string | undefined> {
|
|
17
|
+
const sealingSecret = await loadSecret();
|
|
18
|
+
if (!sealingSecret) return undefined;
|
|
19
|
+
const session = openProspectusCookie<ProspectusSignedInSession>(sealingSecret, PROSPECTUS_SESSION_COOKIE, cookies.get(PROSPECTUS_SESSION_COOKIE)?.value);
|
|
20
|
+
if (!session || session.accessTokenExpiresAt * 1000 <= Date.now()) return undefined;
|
|
21
|
+
return tenantIdForProspectusAccount(session.selectedAccountId);
|
|
22
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ContentRepository, EditionContent } from "./content-types";
|
|
2
2
|
import { graphqlContentRepository } from "./graphql-content-repository";
|
|
3
|
-
import { SITE_BRAND } from "./site-brand";
|
|
3
|
+
import { SITE_BRAND, editionsEnabled } from "./site-brand";
|
|
4
4
|
import { getContentSource } from "./site-env";
|
|
5
5
|
import type { RendererConfig } from "./renderer-config";
|
|
6
6
|
|
|
@@ -40,6 +40,7 @@ const RESERVED_DATE_CHILD_SEGMENTS = new Set(["page", "section"]);
|
|
|
40
40
|
type EnumerationRepository = Pick<ContentRepository, "listPublishedEditions" | "loadEditionContent">;
|
|
41
41
|
|
|
42
42
|
function shouldEnumerateAtBuild(): boolean {
|
|
43
|
+
if (!editionsEnabled()) return false;
|
|
43
44
|
if (process.env.NODE_ENV === "development") return false;
|
|
44
45
|
return siteEnumeratesReaderRoutesAtBuild();
|
|
45
46
|
}
|
|
@@ -2,6 +2,15 @@ import type { SemanticRelationRecord } from "./category-repository";
|
|
|
2
2
|
|
|
3
3
|
export const RELEVANCE_RELATION = "relevance_decision_is";
|
|
4
4
|
|
|
5
|
+
/** What produced a suggestion beyond the model version, as the sweep recorded it. */
|
|
6
|
+
export type RelevanceProvenance = {
|
|
7
|
+
labelSet: string[];
|
|
8
|
+
doctrineFingerprint: string | null;
|
|
9
|
+
teachingFingerprint: string | null;
|
|
10
|
+
teachingLabels: number;
|
|
11
|
+
primingExamples: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
5
14
|
/** The relevance cyclotron's current decision about one reference, as the References tab shows it. */
|
|
6
15
|
export type RelevanceDecisionView = {
|
|
7
16
|
decisionRelationId: string;
|
|
@@ -19,6 +28,10 @@ export type RelevanceDecisionView = {
|
|
|
19
28
|
/** Thumbs for a two-class decision with a positive label; per-class buttons otherwise. */
|
|
20
29
|
reviewControl: "thumbs" | "labels";
|
|
21
30
|
cyclotronId: string | null;
|
|
31
|
+
/** When the cyclotron made the suggestion, if the decision recorded it. */
|
|
32
|
+
decidedAt: string | null;
|
|
33
|
+
/** Null for decisions made before provenance was recorded. */
|
|
34
|
+
provenance: RelevanceProvenance | null;
|
|
22
35
|
};
|
|
23
36
|
|
|
24
37
|
/** What an editor submits from the review control. */
|
|
@@ -49,6 +62,20 @@ function parseMetadata(value: unknown): Record<string, unknown> {
|
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
function parseProvenance(value: unknown): RelevanceProvenance | null {
|
|
66
|
+
const record = parseMetadata(value);
|
|
67
|
+
if (!Object.keys(record).length) return null;
|
|
68
|
+
const count = (entry: unknown) => (Number.isFinite(Number(entry)) ? Number(entry) : 0);
|
|
69
|
+
const text = (entry: unknown) => (typeof entry === "string" && entry ? entry : null);
|
|
70
|
+
return {
|
|
71
|
+
labelSet: Array.isArray(record.labelSet) ? record.labelSet.map(String) : [],
|
|
72
|
+
doctrineFingerprint: text(record.doctrineFingerprint),
|
|
73
|
+
teachingFingerprint: text(record.teachingFingerprint),
|
|
74
|
+
teachingLabels: count(record.teachingLabels),
|
|
75
|
+
primingExamples: count(record.primingExamples),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
/**
|
|
53
80
|
* Current decisions keyed by reference lineage, from relations the Newsroom
|
|
54
81
|
* already loads: no extra request per row.
|
|
@@ -77,6 +104,8 @@ export function currentRelevanceDecisions(relations: SemanticRelationRecord[]):
|
|
|
77
104
|
question: typeof metadata.question === "string" ? metadata.question : null,
|
|
78
105
|
reviewControl: metadata.reviewControl === "thumbs" ? "thumbs" : "labels",
|
|
79
106
|
cyclotronId: typeof metadata.cyclotronId === "string" ? metadata.cyclotronId : null,
|
|
107
|
+
decidedAt: typeof metadata.decidedAt === "string" ? metadata.decidedAt : null,
|
|
108
|
+
provenance: parseProvenance(metadata.provenance),
|
|
80
109
|
});
|
|
81
110
|
}
|
|
82
111
|
return decisions;
|
package/lib/relevance-status.ts
CHANGED
|
@@ -61,3 +61,76 @@ export const DEMO_CYCLOTRON_STATUS: CyclotronStatus = {
|
|
|
61
61
|
lastChange: null,
|
|
62
62
|
pending: { decisionsAwaitingReview: 1, staleSince: null },
|
|
63
63
|
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What the Newsroom assumes until a decision names the cyclotron: the defaults
|
|
67
|
+
* of the steering config's relevanceCyclotron block (src/papyrus_content/relevance_cyclotron.py).
|
|
68
|
+
*/
|
|
69
|
+
export const DEFAULT_RELEVANCE_CYCLOTRON = { id: "papyrus-relevance", labels: ["include", "exclude"] } as const;
|
|
70
|
+
|
|
71
|
+
/** The payload kind of a priming request; the sweep lists the cyclotron's payloads and filters on it. */
|
|
72
|
+
export const PRIMING_REQUEST_KIND = "cyclotron-priming-request";
|
|
73
|
+
|
|
74
|
+
/** The attachment role the sweep writes a request's outcome under. */
|
|
75
|
+
export const PRIMING_OUTCOME_ROLE = "priming_outcome";
|
|
76
|
+
|
|
77
|
+
/** The KnowledgeRawPayload an editor writes to ask the next sweep to prime the cyclotron with one URL. */
|
|
78
|
+
export function primingRequestPayloadId(cyclotronId: string, requestId: string): string {
|
|
79
|
+
return `knowledge-raw-payload-cyclotron-priming-request-${safeId(cyclotronId)}-${safeId(requestId)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PrimingRequest = {
|
|
83
|
+
requestId: string;
|
|
84
|
+
url: string;
|
|
85
|
+
label: string;
|
|
86
|
+
explanation: string;
|
|
87
|
+
requestedBy: string;
|
|
88
|
+
requestedAt: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type PrimingOutcome =
|
|
92
|
+
| { state: "applied"; version: number; at: string; itemId?: string }
|
|
93
|
+
| { state: "failed"; message: string; at: string };
|
|
94
|
+
|
|
95
|
+
/** A saved request with the sweep's outcome, once there is one. */
|
|
96
|
+
export type PrimingRequestView = { request: PrimingRequest; outcome: PrimingOutcome | null };
|
|
97
|
+
|
|
98
|
+
export function buildPrimingRequest(
|
|
99
|
+
input: { url: string; label: string; explanation?: string },
|
|
100
|
+
requestedBy: string,
|
|
101
|
+
labels: readonly string[],
|
|
102
|
+
now: Date = new Date(),
|
|
103
|
+
requestId: string = `${now.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
104
|
+
): PrimingRequest {
|
|
105
|
+
let parsed: URL | null = null;
|
|
106
|
+
try {
|
|
107
|
+
parsed = new URL(input.url.trim());
|
|
108
|
+
} catch {
|
|
109
|
+
parsed = null;
|
|
110
|
+
}
|
|
111
|
+
if (!parsed || parsed.protocol !== "https:") throw new Error("Paste an https URL.");
|
|
112
|
+
if (!labels.includes(input.label)) throw new Error(`Choose a label: ${labels.join(" or ")}.`);
|
|
113
|
+
return {
|
|
114
|
+
requestId,
|
|
115
|
+
url: parsed.toString(),
|
|
116
|
+
label: input.label,
|
|
117
|
+
explanation: (input.explanation ?? "").trim(),
|
|
118
|
+
requestedBy,
|
|
119
|
+
requestedAt: now.toISOString(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isPrimingOutcome(value: unknown): value is PrimingOutcome {
|
|
124
|
+
if (!value || typeof value !== "object") return false;
|
|
125
|
+
const outcome = value as Record<string, unknown>;
|
|
126
|
+
return (outcome.state === "applied" && typeof outcome.version === "number")
|
|
127
|
+
|| (outcome.state === "failed" && typeof outcome.message === "string");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** In words: waiting for the next sweep, applied as version N, or failed with the reason. */
|
|
131
|
+
export function primingStateText(view: PrimingRequestView): string {
|
|
132
|
+
if (!view.outcome) return "Waiting for the next sweep";
|
|
133
|
+
return view.outcome.state === "applied"
|
|
134
|
+
? `Applied as version ${view.outcome.version}`
|
|
135
|
+
: `Failed: ${view.outcome.message}`;
|
|
136
|
+
}
|
|
@@ -106,7 +106,7 @@ export type ReportingStoryBudgetInput = {
|
|
|
106
106
|
messagePayloads?: Record<string, unknown>;
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
const REPORTING_ASSIGNMENT_TYPE = "reporting.
|
|
109
|
+
const REPORTING_ASSIGNMENT_TYPE = "reporting.candidate";
|
|
110
110
|
const REPORTING_PACKET_KIND = "reporting_context_packet";
|
|
111
111
|
const RESEARCH_PACKET_KIND = "research_packet";
|
|
112
112
|
|
|
@@ -331,7 +331,7 @@ function buildResearchPacketIdsByReportingAssignment(assignments: AssignmentReco
|
|
|
331
331
|
if (relation.subjectKind === "assignment" && relation.objectKind === "assignment") {
|
|
332
332
|
const reporting = assignmentById.get(relation.subjectId);
|
|
333
333
|
const research = assignmentById.get(relation.objectId);
|
|
334
|
-
if (reporting?.assignmentTypeKey !== REPORTING_ASSIGNMENT_TYPE || research?.assignmentTypeKey !== "research.
|
|
334
|
+
if (reporting?.assignmentTypeKey !== REPORTING_ASSIGNMENT_TYPE || research?.assignmentTypeKey !== "research.candidate") continue;
|
|
335
335
|
for (const packetId of producedResearchPacketsByAssignment.get(research.id) ?? []) {
|
|
336
336
|
result.set(reporting.id, addToSet(result.get(reporting.id), packetId));
|
|
337
337
|
}
|
package/lib/semantic-graph.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type SemanticPredicateId =
|
|
|
41
41
|
| "has_editorial_form"
|
|
42
42
|
| "about"
|
|
43
43
|
| "comment"
|
|
44
|
-
| "
|
|
44
|
+
| "note_about"
|
|
45
45
|
| "ingestion_rationale"
|
|
46
46
|
| "uses_evidence"
|
|
47
47
|
| "uses_signal"
|
|
@@ -77,7 +77,7 @@ export const SEMANTIC_PREDICATES: SemanticPredicateDefinition[] = [
|
|
|
77
77
|
{ id: "reference_summary_1000_tokens", label: "1000-token reference summary", group: "summarization", inverseLabel: "1000-token summary for", contextPackTags: ["reference_curation", "research", "context_ranking"] },
|
|
78
78
|
{ id: "mentions", label: "mentions", group: "ontology", inverseLabel: "mentioned by" },
|
|
79
79
|
{ id: "has_editorial_form", label: "has editorial form", group: "editorial", inverseLabel: "items by editorial form", contextPackTags: ["editing", "publication", "assignment_context"] },
|
|
80
|
-
{ id: "
|
|
80
|
+
{ id: "note_about", label: "note about", group: "knowledge", inverseLabel: "notes", contextPackTags: ["research", "reference_graph", "editing"] },
|
|
81
81
|
{ id: "about", label: "about", group: "commentary", inverseLabel: "commentary" },
|
|
82
82
|
{ id: "comment", label: "comments on", group: "commentary", inverseLabel: "commented on by", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
|
|
83
83
|
{ id: "ingestion_rationale", label: "ingestion rationale for", group: "commentary", inverseLabel: "ingestion rationale", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
|
|
@@ -317,13 +317,13 @@ export class SemanticGraphSnapshot {
|
|
|
317
317
|
return ranked.map((entry) => entry.message);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
notesFor(kind: string, lineageId: string): MessageRecord[] {
|
|
321
321
|
return this.incoming(kind, lineageId)
|
|
322
322
|
.filter((relation) => relation.subjectKind === "message")
|
|
323
|
-
.filter((relation) => relationTypeKey(relation) === "
|
|
323
|
+
.filter((relation) => relationTypeKey(relation) === "note_about" || relation.predicate === "note_about")
|
|
324
324
|
.map((relation) => this.messagesById.get(relation.subjectId))
|
|
325
325
|
.filter((message): message is MessageRecord => Boolean(message))
|
|
326
|
-
.filter((message) => message.messageKind === "
|
|
326
|
+
.filter((message) => message.messageKind === "note" && message.messageDomain === "knowledge")
|
|
327
327
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
328
328
|
}
|
|
329
329
|
|
package/lib/site-brand.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isLocalPreviewEnabled } from "./local-preview";
|
|
2
|
+
import { areEditionsEnabled } from "./site-env";
|
|
1
3
|
import type { ComponentType } from "react";
|
|
2
4
|
import type { EditionPresentationFormat } from "./content-types";
|
|
3
5
|
import type { Article, ArticleVideoAsset } from "./articles";
|
|
@@ -22,6 +24,8 @@ export type SiteBrandId = string;
|
|
|
22
24
|
|
|
23
25
|
/** Brand-specific replacements for generic Pretext components. */
|
|
24
26
|
export type SiteBrandComponents = {
|
|
27
|
+
/** The mark shown at the top of the newsroom CMS sidebar in place of the app title. */
|
|
28
|
+
NewsroomLogo?: ComponentType<{ className?: string }>;
|
|
25
29
|
PictogramFigure?: ComponentType<PictogramFigureProps>;
|
|
26
30
|
BlogPageBackground?: ComponentType<BlogPageBackgroundProps>;
|
|
27
31
|
};
|
|
@@ -78,8 +82,15 @@ export type ContentItemName = {
|
|
|
78
82
|
|
|
79
83
|
export const DEFAULT_CONTENT_ITEM_NAME: ContentItemName = { singular: "Story", plural: "Stories" };
|
|
80
84
|
|
|
85
|
+
/** Browser-tab icon files under `public/`, as site-root paths. The dark variant defaults to the light one. */
|
|
86
|
+
export type SiteBrandIcons = { light: string; dark?: string; appleTouch?: string };
|
|
87
|
+
|
|
88
|
+
export const DEFAULT_SITE_BRAND_ICONS: Required<Pick<SiteBrandIcons, "light" | "dark">> & { fallback: string } = { light: "/icon-light.png", dark: "/icon-dark.png", fallback: "/icon.png" };
|
|
89
|
+
|
|
81
90
|
export type SiteBrand = {
|
|
82
91
|
id: SiteBrandId;
|
|
92
|
+
/** The browser-tab icon. Omit to use the Papyrus plant. */
|
|
93
|
+
icons?: SiteBrandIcons;
|
|
83
94
|
appTitle: string;
|
|
84
95
|
appDescription: string;
|
|
85
96
|
mastheadTitle: string;
|
|
@@ -147,6 +158,11 @@ export type SiteBrand = {
|
|
|
147
158
|
fonts?: SiteBrandFont[];
|
|
148
159
|
demoEdition?: SiteBrandDemoEdition;
|
|
149
160
|
video?: SiteBrandVideo;
|
|
161
|
+
/**
|
|
162
|
+
* Editions are an optional setting, off unless `{ enabled: true }`. When off the
|
|
163
|
+
* publication is a plain stream of items: no edition date routes, archive or masthead date.
|
|
164
|
+
*/
|
|
165
|
+
editions?: { enabled: boolean };
|
|
150
166
|
};
|
|
151
167
|
|
|
152
168
|
const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
|
|
@@ -184,11 +200,17 @@ const REFERENCE_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
|
184
200
|
* The Papyrus reference brand plus whatever the publication registered in its
|
|
185
201
|
* own `papyrus.config.ts` (resolved through the `papyrus-site` alias).
|
|
186
202
|
*/
|
|
187
|
-
const
|
|
203
|
+
const REGISTERED_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
188
204
|
...REFERENCE_SITE_BRANDS,
|
|
189
205
|
...Object.fromEntries((papyrusSite.brands ?? []).map((brand) => [brand.id, brand])),
|
|
190
206
|
};
|
|
191
207
|
|
|
208
|
+
const SITE_BRANDS: Record<SiteBrandId, SiteBrand> = isLocalPreviewEnabled()
|
|
209
|
+
? Object.fromEntries(
|
|
210
|
+
Object.entries(REGISTERED_SITE_BRANDS).map(([id, brand]) => [id, { ...brand, newsroomLayout: "organizer" as const }]),
|
|
211
|
+
)
|
|
212
|
+
: REGISTERED_SITE_BRANDS;
|
|
213
|
+
|
|
192
214
|
export function normalizeSiteBrandId(value: string | undefined | null): SiteBrandId | null {
|
|
193
215
|
if (!value) return null;
|
|
194
216
|
const trimmed = value.trim();
|
|
@@ -237,6 +259,11 @@ export function getRootRoute(brand: SiteBrand = SITE_BRAND): RootRouteConfig {
|
|
|
237
259
|
|
|
238
260
|
export const rootRoute = getRootRoute();
|
|
239
261
|
|
|
262
|
+
/** The brand setting wins when present; otherwise the PAPYRUS_EDITIONS_ENABLED switch decides. */
|
|
263
|
+
export function editionsEnabled(brand: Pick<SiteBrand, "editions"> = SITE_BRAND): boolean {
|
|
264
|
+
return brand.editions?.enabled ?? areEditionsEnabled();
|
|
265
|
+
}
|
|
266
|
+
|
|
240
267
|
export function enforcePresentation(presentation: EditionPresentationFormat): EditionPresentationFormat {
|
|
241
268
|
return SITE_BRAND.forcedPresentation ?? presentation;
|
|
242
269
|
}
|
package/lib/site-env.ts
CHANGED
|
@@ -20,6 +20,11 @@ export function getSiteEnv(environment: EnvironmentVariables = process.env): Sit
|
|
|
20
20
|
return configured as SiteEnv;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function areEditionsEnabled(environment: EnvironmentVariables = process.env): boolean {
|
|
24
|
+
const configured = (environment.PAPYRUS_EDITIONS_ENABLED ?? "").trim().toLowerCase();
|
|
25
|
+
return configured === "true" || configured === "1" || configured === "yes" || configured === "on";
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export function getContentSource(environment: EnvironmentVariables = process.env): ContentSource {
|
|
24
29
|
const configured = (environment.PAPYRUS_CONTENT_SOURCE ?? "").trim();
|
|
25
30
|
if (!configured) return "published";
|