@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.4
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 +26 -0
- package/amplify/data/resource.ts +30 -0
- package/amplify/data/schema.js +26 -0
- package/amplify/data/schema.ts +30 -0
- package/amplify/functions/content-actions/handler.py +107 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/site-backend.js +10 -0
- package/amplify/site-backend.ts +15 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
- package/app/globals.css +1 -4
- package/app/layout.tsx +8 -12
- package/app/robots.ts +11 -0
- package/bin/papyrus-infra.mjs +46 -16
- package/components/news-desk-page.tsx +1 -4
- package/components/staging-banner.tsx +11 -0
- package/infra/amplify-app-shell.js +123 -80
- package/infra/amplify-app-shell.ts +139 -113
- package/infra/build-specs.js +86 -0
- package/infra/build-specs.ts +132 -0
- package/infra/index.js +2 -0
- package/infra/site-config.js +125 -0
- package/infra/site-config.ts +163 -0
- package/lib/cached-content-repository.ts +2 -1
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +16 -0
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +182 -37
- package/lib/layout-scenarios.ts +8 -8
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +44 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/middleware.ts +46 -34
- package/package.json +6 -1
- package/renderers/pretext/article-page.tsx +1 -6
- package/renderers/pretext/presentation-shell.tsx +2 -9
- package/routes.manifest.json +1 -1
- package/src/with-papyrus.mjs +15 -2
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
package/lib/layout-scenarios.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { createDefaultEditionLayoutPlan, type EditionLayoutPlan } from "./layout
|
|
|
5
5
|
import { articleToPublicationItem, cloneArticle } from "./publication-items";
|
|
6
6
|
import { PILOBOL_SAMPLE_SCENARIO_ID, createPilobolSampleEditionContent } from "./pilobol-sample";
|
|
7
7
|
import { SITE_BRAND } from "./site-brand";
|
|
8
|
-
import threatIntelligenceSeedContent from "../publications/threat_intelligence/seed/seed-edition-content.json";
|
|
9
8
|
|
|
10
9
|
export type LayoutScenario = EditionContent & {
|
|
11
10
|
source: "scenario";
|
|
@@ -190,7 +189,8 @@ function getDefaultScenarioSeed(): {
|
|
|
190
189
|
editionVideo?: ArticleVideoAsset;
|
|
191
190
|
newsDeskAppendix?: NewsDeskAppendix | null;
|
|
192
191
|
} {
|
|
193
|
-
|
|
192
|
+
const demoEdition = SITE_BRAND.demoEdition;
|
|
193
|
+
if (!demoEdition) {
|
|
194
194
|
return {
|
|
195
195
|
title: "Current Edition",
|
|
196
196
|
editionDate,
|
|
@@ -201,12 +201,12 @@ function getDefaultScenarioSeed(): {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
return {
|
|
204
|
-
title:
|
|
205
|
-
editionDate:
|
|
206
|
-
description:
|
|
207
|
-
articles:
|
|
208
|
-
suppressNewsDeskAppendix:
|
|
209
|
-
editionVideo:
|
|
204
|
+
title: demoEdition.title,
|
|
205
|
+
editionDate: demoEdition.editionDate,
|
|
206
|
+
description: demoEdition.description,
|
|
207
|
+
articles: demoEdition.articles,
|
|
208
|
+
suppressNewsDeskAppendix: demoEdition.suppressNewsDeskAppendix === true,
|
|
209
|
+
editionVideo: demoEdition.editionVideo,
|
|
210
210
|
newsDeskAppendix: null,
|
|
211
211
|
};
|
|
212
212
|
}
|
package/lib/site-brand-tenant.ts
CHANGED
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
getSiteBrand,
|
|
4
|
-
resolveRuntimeSiteBrandId,
|
|
5
|
-
resolveSiteBrandId,
|
|
6
|
-
type SiteBrand,
|
|
7
|
-
type SiteBrandId,
|
|
8
|
-
} from "./site-brand";
|
|
2
|
+
import { getSiteBrand, resolveSiteBrandId, type SiteBrand, type SiteBrandId } from "./site-brand";
|
|
9
3
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
export function resolveActiveSiteBrandId(
|
|
13
|
-
cookieOverride: string | undefined | null = null,
|
|
14
|
-
envBrandId: SiteBrandId = resolveSiteBrandId(),
|
|
15
|
-
): SiteBrandId {
|
|
16
|
-
return resolveRuntimeSiteBrandId(cookieOverride) ?? envBrandId;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function resolveActiveSiteBrand(
|
|
20
|
-
cookieOverride: string | undefined | null = null,
|
|
21
|
-
brandId?: SiteBrandId,
|
|
22
|
-
): SiteBrand {
|
|
23
|
-
const resolvedId = brandId ?? resolveActiveSiteBrandId(cookieOverride);
|
|
24
|
-
return getSiteBrand(resolvedId);
|
|
4
|
+
export function resolveActiveSiteBrand(brandId?: SiteBrandId): SiteBrand {
|
|
5
|
+
return getSiteBrand(brandId ?? resolveSiteBrandId());
|
|
25
6
|
}
|
|
26
7
|
|
|
27
8
|
export function resolveBrandConfigPath(relativePath: string): string {
|
package/lib/site-brand.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
1
2
|
import type { EditionPresentationFormat } from "./content-types";
|
|
3
|
+
import type { Article, ArticleVideoAsset } from "./articles";
|
|
4
|
+
import type { BlogPageBackgroundProps } from "../components/blog-page-background";
|
|
5
|
+
import type { PictogramFigureProps } from "../components/pictogram-figure";
|
|
2
6
|
import papyrusSite from "papyrus-site";
|
|
3
|
-
import { threatIntelligenceBrand } from "../publications/threat_intelligence/brand";
|
|
4
|
-
import { pilobolUsBrand } from "../publications/pilobol_us/brand";
|
|
5
|
-
import { anthUsBrand } from "../publications/anth_us/brand";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_THEME_PACK_TOKENS,
|
|
8
9
|
type HostingConfig,
|
|
@@ -17,6 +18,22 @@ import {
|
|
|
17
18
|
/** Open string: a publication registers its own brand through `papyrus.config.ts` (`defineSite`). */
|
|
18
19
|
export type SiteBrandId = string;
|
|
19
20
|
|
|
21
|
+
/** Brand-specific replacements for generic Pretext components. */
|
|
22
|
+
export type SiteBrandComponents = {
|
|
23
|
+
PictogramFigure?: ComponentType<PictogramFigureProps>;
|
|
24
|
+
BlogPageBackground?: ComponentType<BlogPageBackgroundProps>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default scenario content a brand supplies for the layout lab. */
|
|
28
|
+
export type SiteBrandDemoEdition = {
|
|
29
|
+
title: string;
|
|
30
|
+
editionDate: string;
|
|
31
|
+
description: string;
|
|
32
|
+
articles: Article[];
|
|
33
|
+
suppressNewsDeskAppendix?: boolean;
|
|
34
|
+
editionVideo?: ArticleVideoAsset;
|
|
35
|
+
};
|
|
36
|
+
|
|
20
37
|
export type SiteBrand = {
|
|
21
38
|
id: SiteBrandId;
|
|
22
39
|
appTitle: string;
|
|
@@ -57,11 +74,13 @@ export type SiteBrand = {
|
|
|
57
74
|
newsroomSectionsConfigPath: string;
|
|
58
75
|
analysisProfilesPath: string;
|
|
59
76
|
publicationName: string;
|
|
77
|
+
components?: SiteBrandComponents;
|
|
78
|
+
demoEdition?: SiteBrandDemoEdition;
|
|
60
79
|
};
|
|
61
80
|
|
|
62
81
|
const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
|
|
63
82
|
|
|
64
|
-
const
|
|
83
|
+
const REFERENCE_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
65
84
|
papyrus: {
|
|
66
85
|
id: "papyrus",
|
|
67
86
|
appTitle: "Papyrus",
|
|
@@ -88,63 +107,46 @@ const BUILT_IN_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
|
88
107
|
analysisProfilesPath: "corpora/papyrus-analysis-profiles.yml",
|
|
89
108
|
publicationName: "Anthus Threat Intelligence",
|
|
90
109
|
},
|
|
91
|
-
"threat-intelligence": threatIntelligenceBrand,
|
|
92
|
-
"pilobol-us": pilobolUsBrand,
|
|
93
|
-
"anth-us": anthUsBrand,
|
|
94
110
|
};
|
|
95
111
|
|
|
96
112
|
/**
|
|
97
|
-
*
|
|
113
|
+
* The Papyrus reference brand plus whatever the publication registered in its
|
|
98
114
|
* own `papyrus.config.ts` (resolved through the `papyrus-site` alias).
|
|
99
115
|
*/
|
|
100
116
|
const SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
101
|
-
...
|
|
117
|
+
...REFERENCE_SITE_BRANDS,
|
|
102
118
|
...Object.fromEntries((papyrusSite.brands ?? []).map((brand) => [brand.id, brand])),
|
|
103
119
|
};
|
|
104
120
|
|
|
105
121
|
export function normalizeSiteBrandId(value: string | undefined | null): SiteBrandId | null {
|
|
106
122
|
if (!value) return null;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (normalized === "papyrus") return "papyrus";
|
|
110
|
-
// NOTE: the bare alias `anthus` has resolved to `threat-intelligence` since
|
|
111
|
-
// that brand was added and is kept pointing there so existing
|
|
112
|
-
// PAPYRUS_SITE_BRAND values do not silently change meaning. The Anth.us
|
|
113
|
-
// publication is therefore addressed as `anth-us` / `anth.us` / `anth_us`
|
|
114
|
-
// only. Do not "tidy" this by moving `anthus` across.
|
|
115
|
-
if (normalized === "threat-intelligence" || normalized === "threat-intel" || normalized === "anthus") {
|
|
116
|
-
return "threat-intelligence";
|
|
117
|
-
}
|
|
118
|
-
if (normalized === "anth-us") {
|
|
119
|
-
// `normalized` has already mapped `.` and `_` to `-`, so this covers
|
|
120
|
-
// `anth.us` and `anth_us` too.
|
|
121
|
-
return "anth-us";
|
|
122
|
-
}
|
|
123
|
-
// Bare `pilobol` is deliberately NOT an alias (f268e72): the brand name is
|
|
124
|
-
// Pilobolus; Pilobol.us is the domain and `pilobol-us` the technical id.
|
|
125
|
-
if (normalized === "pilobol-us" || normalized === "pilobolus") {
|
|
126
|
-
return "pilobol-us";
|
|
127
|
-
}
|
|
128
|
-
// Registered brands (publication-owned `papyrus.config.ts`): exact id only.
|
|
129
|
-
if (Object.prototype.hasOwnProperty.call(SITE_BRANDS, normalized)) return normalized;
|
|
130
|
-
return null;
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
return Object.prototype.hasOwnProperty.call(SITE_BRANDS, trimmed) ? trimmed : null;
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
): SiteBrandId {
|
|
136
|
-
return normalizeSiteBrandId(raw) ?? papyrusSite.defaultBrand ?? "papyrus";
|
|
127
|
+
function unknownBrandError(value: string): Error {
|
|
128
|
+
return new Error(`Unknown brand '${value}'. Registered: ${Object.keys(SITE_BRANDS).join(", ")}`);
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
cookieOverride: string | undefined | null = null,
|
|
131
|
+
export function resolveSiteBrandId(
|
|
132
|
+
raw: string | undefined | null = process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND ?? process.env.PAPYRUS_SITE_BRAND,
|
|
142
133
|
): SiteBrandId {
|
|
143
|
-
|
|
134
|
+
const requested = raw?.trim();
|
|
135
|
+
if (requested) {
|
|
136
|
+
const normalized = normalizeSiteBrandId(requested);
|
|
137
|
+
if (normalized === null) throw unknownBrandError(requested);
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
const fallback = papyrusSite.defaultBrand ?? "papyrus";
|
|
141
|
+
const normalizedFallback = normalizeSiteBrandId(fallback);
|
|
142
|
+
if (normalizedFallback === null) throw unknownBrandError(fallback);
|
|
143
|
+
return normalizedFallback;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
export function getSiteBrand(id: SiteBrandId = resolveSiteBrandId()): SiteBrand {
|
|
147
|
-
|
|
147
|
+
const brand = Object.prototype.hasOwnProperty.call(SITE_BRANDS, id) ? SITE_BRANDS[id] : undefined;
|
|
148
|
+
if (!brand) throw unknownBrandError(id);
|
|
149
|
+
return brand;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export const SITE_BRAND = getSiteBrand();
|
package/lib/site-env.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type SiteEnv = "production" | "staging" | "development";
|
|
2
|
+
export type ContentSource = "published" | "drafts";
|
|
3
|
+
|
|
4
|
+
type EnvironmentVariables = Record<string, string | undefined>;
|
|
5
|
+
|
|
6
|
+
const SITE_ENVIRONMENTS: readonly SiteEnv[] = ["production", "staging", "development"];
|
|
7
|
+
const CONTENT_SOURCES: readonly ContentSource[] = ["published", "drafts"];
|
|
8
|
+
const PRODUCTION_BRANCH = "main";
|
|
9
|
+
|
|
10
|
+
export function getSiteEnv(environment: EnvironmentVariables = process.env): SiteEnv {
|
|
11
|
+
const configured = (environment.SITE_ENV ?? "").trim();
|
|
12
|
+
if (!configured) return "development";
|
|
13
|
+
if (!SITE_ENVIRONMENTS.includes(configured as SiteEnv)) {
|
|
14
|
+
throw new Error(`SITE_ENV must be one of ${SITE_ENVIRONMENTS.join(", ")}; received "${configured}".`);
|
|
15
|
+
}
|
|
16
|
+
if (configured === "production") {
|
|
17
|
+
const branch = (environment.AWS_BRANCH ?? "").trim();
|
|
18
|
+
if (branch && branch !== PRODUCTION_BRANCH) return "staging";
|
|
19
|
+
}
|
|
20
|
+
return configured as SiteEnv;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getContentSource(environment: EnvironmentVariables = process.env): ContentSource {
|
|
24
|
+
const configured = (environment.PAPYRUS_CONTENT_SOURCE ?? "").trim();
|
|
25
|
+
if (!configured) return "published";
|
|
26
|
+
if (!CONTENT_SOURCES.includes(configured as ContentSource)) {
|
|
27
|
+
throw new Error(`PAPYRUS_CONTENT_SOURCE must be one of ${CONTENT_SOURCES.join(", ")}; received "${configured}".`);
|
|
28
|
+
}
|
|
29
|
+
return configured as ContentSource;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isIndexable(environment: EnvironmentVariables = process.env): boolean {
|
|
33
|
+
return getSiteEnv(environment) === "production";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function showStagingBanner(environment: EnvironmentVariables = process.env): boolean {
|
|
37
|
+
return getSiteEnv(environment) === "staging";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function analyticsAllowed(environment: EnvironmentVariables = process.env): boolean {
|
|
41
|
+
return getSiteEnv(environment) === "production";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertContentSourceMatchesEnv(environment: EnvironmentVariables = process.env): void {
|
|
45
|
+
if (getContentSource(environment) !== "drafts") return;
|
|
46
|
+
const siteEnv = getSiteEnv(environment);
|
|
47
|
+
if (siteEnv !== "staging") {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`PAPYRUS_CONTENT_SOURCE=drafts requires SITE_ENV=staging; the resolved site environment is "${siteEnv}". Drafts must never be served from production or development.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fetchAuthSession } from "aws-amplify/auth/server";
|
|
3
|
+
import { getAmplifyServerRuntime } from "./amplify-server-runtime";
|
|
4
|
+
|
|
5
|
+
export type StagingAccess = "anonymous" | "forbidden" | "ok";
|
|
6
|
+
|
|
7
|
+
const STAGING_ALLOWED_GROUPS = ["editor", "admin"];
|
|
8
|
+
|
|
9
|
+
export function groupsFromIdTokenPayload(payload: Record<string, unknown> | undefined): string[] {
|
|
10
|
+
const raw = payload?.["cognito:groups"];
|
|
11
|
+
if (Array.isArray(raw)) return raw.filter((group): group is string => typeof group === "string");
|
|
12
|
+
if (typeof raw === "string" && raw) return [raw];
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function accessForGroups(groups: string[] | null): StagingAccess {
|
|
17
|
+
if (groups === null) return "anonymous";
|
|
18
|
+
return groups.some((group) => STAGING_ALLOWED_GROUPS.includes(group)) ? "ok" : "forbidden";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getStagingAccess(request: NextRequest, response: NextResponse): Promise<StagingAccess> {
|
|
22
|
+
const { runWithAmplifyServerContext } = getAmplifyServerRuntime();
|
|
23
|
+
const groups = await runWithAmplifyServerContext({
|
|
24
|
+
nextServerContext: { request, response },
|
|
25
|
+
operation: async (contextSpec) => {
|
|
26
|
+
try {
|
|
27
|
+
const session = await fetchAuthSession(contextSpec);
|
|
28
|
+
const idTokenPayload = session.tokens?.idToken?.payload as Record<string, unknown> | undefined;
|
|
29
|
+
if (!session.tokens?.idToken) return null;
|
|
30
|
+
return groupsFromIdTokenPayload(idTokenPayload);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return accessForGroups(groups);
|
|
37
|
+
}
|
package/middleware.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
import { getSiteBrand
|
|
3
|
+
import { getSiteBrand } from "./lib/site-brand";
|
|
4
|
+
import { getSiteEnv, isIndexable } from "./lib/site-env";
|
|
5
|
+
import { getStagingAccess } from "./lib/staging-gate";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const cookieBrand = request.cookies.get(BRAND_OVERRIDE_COOKIE)?.value;
|
|
9
|
-
const brandId = normalizeSiteBrandId(cookieBrand)
|
|
10
|
-
?? normalizeSiteBrandId(request.nextUrl.searchParams.get("brand"))
|
|
11
|
-
?? resolveSiteBrandId();
|
|
12
|
-
// Brand property (not a hard-coded id) so publication-registered brands work.
|
|
13
|
-
return getSiteBrand(brandId).rootRoute?.kind === "newsroom" && getSiteBrand(brandId).newsroomBasePath === "";
|
|
7
|
+
function usesNewsroomRootPaths(): boolean {
|
|
8
|
+
const brand = getSiteBrand();
|
|
9
|
+
return brand.rootRoute?.kind === "newsroom" && brand.newsroomBasePath === "";
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
function isStaticOrApiPath(pathname: string): boolean {
|
|
@@ -23,27 +19,21 @@ function isStaticOrApiPath(pathname: string): boolean {
|
|
|
23
19
|
);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
response = NextResponse.next();
|
|
33
|
-
response.cookies.set(BRAND_OVERRIDE_COOKIE, brandId, {
|
|
34
|
-
path: "/",
|
|
35
|
-
maxAge: 60 * 60 * 24 * 30,
|
|
36
|
-
sameSite: "lax",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
22
|
+
function isStagingGatedPath(pathname: string): boolean {
|
|
23
|
+
if (isStaticOrApiPath(pathname)) return false;
|
|
24
|
+
if (pathname === "/robots.txt") return false;
|
|
25
|
+
if (pathname === "/newsroom" || pathname.startsWith("/newsroom/")) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
function routeRequest(request: NextRequest): NextResponse {
|
|
30
|
+
if (!usesNewsroomRootPaths()) {
|
|
31
|
+
return NextResponse.next();
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
const { pathname } = request.nextUrl;
|
|
45
35
|
if (isStaticOrApiPath(pathname)) {
|
|
46
|
-
return
|
|
36
|
+
return NextResponse.next();
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
if (pathname === "/newsroom" || pathname === "/newsroom/") {
|
|
@@ -55,24 +45,46 @@ export function middleware(request: NextRequest) {
|
|
|
55
45
|
}
|
|
56
46
|
|
|
57
47
|
if (pathname === "/" || pathname === "") {
|
|
58
|
-
return
|
|
48
|
+
return NextResponse.next();
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
if (!pathname.startsWith("/newsroom")) {
|
|
62
52
|
const rewriteUrl = request.nextUrl.clone();
|
|
63
53
|
rewriteUrl.pathname = `/newsroom${pathname}`;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
return NextResponse.rewrite(rewriteUrl);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return NextResponse.next();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
61
|
+
let sessionResponse: NextResponse | null = null;
|
|
62
|
+
if (getSiteEnv() === "staging" && isStagingGatedPath(request.nextUrl.pathname)) {
|
|
63
|
+
sessionResponse = NextResponse.next();
|
|
64
|
+
const access = await getStagingAccess(request, sessionResponse);
|
|
65
|
+
if (access === "anonymous") {
|
|
66
|
+
return withRobotsHeader(NextResponse.redirect(new URL("/newsroom", request.url)));
|
|
67
|
+
}
|
|
68
|
+
if (access === "forbidden") {
|
|
69
|
+
return withRobotsHeader(new NextResponse("Staging is limited to editors and admins.", { status: 403 }));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const response = routeRequest(request);
|
|
74
|
+
if (sessionResponse) {
|
|
75
|
+
for (const cookie of sessionResponse.headers.getSetCookie()) {
|
|
76
|
+
response.headers.append("set-cookie", cookie);
|
|
69
77
|
}
|
|
70
|
-
return rewrite;
|
|
71
78
|
}
|
|
79
|
+
return withRobotsHeader(response);
|
|
80
|
+
}
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
function withRobotsHeader(response: NextResponse): NextResponse {
|
|
83
|
+
if (!isIndexable()) response.headers.set("X-Robots-Tag", "noindex, nofollow");
|
|
84
|
+
return response;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
export const config = {
|
|
88
|
+
runtime: "nodejs",
|
|
77
89
|
matcher: ["/((?!_next/static|_next/image|favicon.ico|icon).*)"],
|
|
78
90
|
};
|
package/package.json
CHANGED
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"./components/reader-theme-control": "./components/reader-theme-control.tsx",
|
|
96
96
|
"./components/reference-source-preview": "./components/reference-source-preview.tsx",
|
|
97
97
|
"./components/settings-page": "./components/settings-page.tsx",
|
|
98
|
+
"./components/staging-banner": "./components/staging-banner.tsx",
|
|
98
99
|
"./components/topic-steering-workspace": "./components/topic-steering-workspace.tsx",
|
|
99
100
|
"./components/ui/badge": "./components/ui/badge.tsx",
|
|
100
101
|
"./components/ui/button-group": "./components/ui/button-group.tsx",
|
|
@@ -126,8 +127,10 @@
|
|
|
126
127
|
"./lib/category-repository": "./lib/category-repository.ts",
|
|
127
128
|
"./lib/console-chat-client": "./lib/console-chat-client.ts",
|
|
128
129
|
"./lib/console-web-context": "./lib/console-web-context.ts",
|
|
130
|
+
"./lib/content-actions-client": "./lib/content-actions-client.ts",
|
|
129
131
|
"./lib/content-load-timing": "./lib/content-load-timing.ts",
|
|
130
132
|
"./lib/content-repository": "./lib/content-repository.ts",
|
|
133
|
+
"./lib/content-source-context": "./lib/content-source-context.ts",
|
|
131
134
|
"./lib/content-types": "./lib/content-types.ts",
|
|
132
135
|
"./lib/define-site": "./lib/define-site.ts",
|
|
133
136
|
"./lib/demo-amplify-outputs": "./lib/demo-amplify-outputs.ts",
|
|
@@ -180,8 +183,10 @@
|
|
|
180
183
|
"./lib/semantic-graph": "./lib/semantic-graph.ts",
|
|
181
184
|
"./lib/site-brand-tenant": "./lib/site-brand-tenant.ts",
|
|
182
185
|
"./lib/site-brand": "./lib/site-brand.ts",
|
|
186
|
+
"./lib/site-env": "./lib/site-env.ts",
|
|
183
187
|
"./lib/site-renderer": "./lib/site-renderer.ts",
|
|
184
188
|
"./lib/site-stack": "./lib/site-stack.ts",
|
|
189
|
+
"./lib/staging-gate": "./lib/staging-gate.ts",
|
|
185
190
|
"./lib/themed-image": "./lib/themed-image.ts",
|
|
186
191
|
"./lib/utils": "./lib/utils.ts",
|
|
187
192
|
"./renderers/markus/index": "./renderers/markus/index.ts",
|
|
@@ -256,7 +261,7 @@
|
|
|
256
261
|
"optional": true
|
|
257
262
|
}
|
|
258
263
|
},
|
|
259
|
-
"version": "1.0.0-next.
|
|
264
|
+
"version": "1.0.0-next.4",
|
|
260
265
|
"dependencies": {
|
|
261
266
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
262
267
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
|
@@ -7,15 +7,10 @@ import { getPublicationItemVideoAsset } from "../../lib/publication-items";
|
|
|
7
7
|
import type { ArticlePageEditionFooter, RenderArticleProps, RenderItemProps } from "../../lib/renderer";
|
|
8
8
|
import { SITE_BRAND } from "../../lib/site-brand";
|
|
9
9
|
import { ArticleVideoFigure } from "../../components/article-video";
|
|
10
|
-
import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
|
|
11
10
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
12
11
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
// publication gets the generic fallback (components/pictogram-figure.tsx).
|
|
16
|
-
// Same decoupling as renderers/pretext/presentation-shell.tsx -- see
|
|
17
|
-
// docs/pluggable-publishers.md section 2.6 and PPY-d79ff2.
|
|
18
|
-
const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
|
|
13
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
19
14
|
|
|
20
15
|
export type { ArticlePageEditionFooter };
|
|
21
16
|
|
|
@@ -18,21 +18,14 @@ import type { ArticleVideoAsset } from "../../lib/articles";
|
|
|
18
18
|
import type { PresentationTarget, RenderEditionProps } from "../../lib/renderer";
|
|
19
19
|
import { SITE_BRAND, enforcePresentation, getDefaultPretextLayout } from "../../lib/site-brand";
|
|
20
20
|
import { ArticleVideoFigure } from "../../components/article-video";
|
|
21
|
-
import { BlogPageBackground as ThreatIntelligenceBlogPageBackground } from "../../publications/threat_intelligence/blog-defense/page-background";
|
|
22
21
|
import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
|
|
23
22
|
import { Newspaper } from "../../components/newspaper";
|
|
24
|
-
import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
|
|
25
23
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
26
24
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
27
25
|
import { readLocalReaderSettings, resolveReaderSettings, subscribeReaderSettingsChanges } from "../../components/reader-settings";
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// coupling docs/pluggable-publishers.md section 2.6 flags, decoupled here
|
|
32
|
-
// per PPY-d79ff2 by making the choice publication-conditional instead of
|
|
33
|
-
// hardcoded.
|
|
34
|
-
const BlogPageBackground = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligenceBlogPageBackground : GenericBlogPageBackground;
|
|
35
|
-
const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
|
|
27
|
+
const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
|
|
28
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
36
29
|
|
|
37
30
|
export type { PresentationTarget };
|
|
38
31
|
|
package/routes.manifest.json
CHANGED
package/src/with-papyrus.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Wrap a publication repo's Next config for Papyrus (SPIKE, PPY-82be6c).
|
|
5
7
|
* - transpile the package (it ships TypeScript source),
|
|
6
|
-
* - alias `papyrus-site` -> ./papyrus.config.ts
|
|
7
|
-
* `papyrus-amplify-outputs` -> ./amplify_outputs.json
|
|
8
|
+
* - alias `papyrus-site` -> ./papyrus.config.ts,
|
|
9
|
+
* `papyrus-amplify-outputs` -> ./amplify_outputs.json and
|
|
10
|
+
* `papyrus-site-theme` -> ./publication/theme.css when it exists, else the shipped
|
|
11
|
+
* empty lib/empty-theme.css (both webpack and turbopack),
|
|
8
12
|
* - carry Papyrus's own server-external packages and image settings.
|
|
9
13
|
*/
|
|
10
14
|
const SERVER_EXTERNAL = [
|
|
@@ -18,11 +22,19 @@ const SERVER_EXTERNAL = [
|
|
|
18
22
|
"@aws-amplify/data-schema",
|
|
19
23
|
];
|
|
20
24
|
|
|
25
|
+
const EMPTY_THEME = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../lib/empty-theme.css");
|
|
26
|
+
|
|
21
27
|
export function withPapyrus(nextConfig = {}, options = {}) {
|
|
22
28
|
const root = options.root ?? process.cwd();
|
|
29
|
+
const hasPublicationTheme = fs.existsSync(path.join(root, "publication/theme.css"));
|
|
30
|
+
const toProjectRelative = (absolute) => {
|
|
31
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
32
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
33
|
+
};
|
|
23
34
|
const aliases = {
|
|
24
35
|
"papyrus-site": path.join(root, "papyrus.config.ts"),
|
|
25
36
|
"papyrus-amplify-outputs": path.join(root, "amplify_outputs.json"),
|
|
37
|
+
"papyrus-site-theme": hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME,
|
|
26
38
|
};
|
|
27
39
|
return {
|
|
28
40
|
distDir: process.env.NEXT_DIST_DIR || ".next",
|
|
@@ -37,6 +49,7 @@ export function withPapyrus(nextConfig = {}, options = {}) {
|
|
|
37
49
|
...nextConfig.turbopack?.resolveAlias,
|
|
38
50
|
"papyrus-site": "./papyrus.config.ts",
|
|
39
51
|
"papyrus-amplify-outputs": "./amplify_outputs.json",
|
|
52
|
+
"papyrus-site-theme": toProjectRelative(hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME),
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
55
|
webpack(config, context) {
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { SiteBrand } from "../../lib/site-brand";
|
|
2
|
-
import { ANTH_US_THEME_PACK_TOKENS } from "../../lib/site-stack";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Site definition for the Anth.us publication on Papyrus.
|
|
6
|
-
*
|
|
7
|
-
* This is the **site**: masthead, theme pack, corpus wiring, deployment
|
|
8
|
-
* metadata. It is a sibling of `publications/anthus/`, which is the
|
|
9
|
-
* **editorial identity** (`style-profile.yml`, `editorial-rewrite-skill.yml`,
|
|
10
|
-
* `reference-samples/`) and is not touched by this file.
|
|
11
|
-
*
|
|
12
|
-
* Every string below is read off the live Gatsby site at `AnthusAI/Anth.us`:
|
|
13
|
-
* `gatsby-config.mjs` `siteMetadata` for the title/description/author,
|
|
14
|
-
* `src/components/header.js` for the wordmark, `gatsby-plugin-manifest` for
|
|
15
|
-
* "Anth.us AI Solutions", and `src/components/seo.js` for the
|
|
16
|
-
* `"<page> | Anthus"` title pattern.
|
|
17
|
-
*/
|
|
18
|
-
export const anthUsBrand: SiteBrand = {
|
|
19
|
-
id: "anth-us",
|
|
20
|
-
appTitle: "Anthus",
|
|
21
|
-
appDescription:
|
|
22
|
-
"Anthus builds and operates self-aligning AI systems — custom models, agent harnesses, and evaluation loops with a human in the loop, grounded in 14 years of production operations.",
|
|
23
|
-
// The live header renders the bare word "Anthus" (Jersey 25, weight 900),
|
|
24
|
-
// not a dotted domain wordmark, so this is not "ANTH.US".
|
|
25
|
-
mastheadTitle: "Anthus",
|
|
26
|
-
mastheadSubtitle: "AI Solutions",
|
|
27
|
-
backToHomeLabel: "Back to Anthus",
|
|
28
|
-
articleTitleSuffix: "Anthus",
|
|
29
|
-
placeholderByline: "Ryan Porter",
|
|
30
|
-
// The source site is a blog with a long single-column measure, not a
|
|
31
|
-
// newsprint grid.
|
|
32
|
-
defaultPresentation: "blog",
|
|
33
|
-
textFont:
|
|
34
|
-
'"Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
35
|
-
mastheadWordSplit: false,
|
|
36
|
-
mastheadDateFormat: "formatted",
|
|
37
|
-
mastheadSource: "brand",
|
|
38
|
-
sectionLinkStrategy: "route",
|
|
39
|
-
themePack: "anth-us",
|
|
40
|
-
themeTokens: ANTH_US_THEME_PACK_TOKENS,
|
|
41
|
-
renderer: { kind: "markus" },
|
|
42
|
-
hosting: { kind: "amplify-ssr" },
|
|
43
|
-
readerDeployment: {
|
|
44
|
-
kind: "amplify-static",
|
|
45
|
-
repository: "https://github.com/AnthusAI/Anth.us-Papyrus",
|
|
46
|
-
// `domain` and `amplifyAppId` are deliberately unset.
|
|
47
|
-
//
|
|
48
|
-
// This reader is a **staging** port of a live marketing site. Production
|
|
49
|
-
// `anth.us` stays pointed at the existing Gatsby build until the port has
|
|
50
|
-
// been reviewed, so writing `domain: "anth.us"` here would assert a
|
|
51
|
-
// canonical host this build does not serve. Staging runs on whatever
|
|
52
|
-
// hostname Amplify hands out. No DNS or domain association is configured
|
|
53
|
-
// anywhere in this repo for it.
|
|
54
|
-
//
|
|
55
|
-
// The repository is private while the port is half-finished; the URL above
|
|
56
|
-
// will 404 for anyone without access. That is expected, not a typo.
|
|
57
|
-
},
|
|
58
|
-
rootRoute: { kind: "newsroom" },
|
|
59
|
-
newsroomBasePath: "",
|
|
60
|
-
opsChrome: "app",
|
|
61
|
-
corpusKey: "anth-us",
|
|
62
|
-
// Declared paths. The YAML files themselves are not in the repo yet — see
|
|
63
|
-
// README.md "Not wired yet".
|
|
64
|
-
steeringConfigPath: "corpora/anth-us-steering.yml",
|
|
65
|
-
newsroomSectionsConfigPath: "corpora/anth-us-newsroom-sections.yml",
|
|
66
|
-
analysisProfilesPath: "corpora/anth-us-analysis-profiles.yml",
|
|
67
|
-
publicationName: "Anthus",
|
|
68
|
-
};
|