@anthusai/papyrus 0.0.0 → 1.0.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.
Files changed (282) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2069 -0
  5. package/amplify/data/resource.ts +2185 -0
  6. package/amplify/data/schema.js +1882 -0
  7. package/amplify/data/schema.ts +1990 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/content-actions/handler.py +149 -0
  22. package/amplify/functions/content-actions/requirements.txt +4 -0
  23. package/amplify/functions/content-actions/resource.js +40 -0
  24. package/amplify/functions/content-actions/resource.ts +54 -0
  25. package/amplify/functions/email-submission-processor/handler.py +46 -0
  26. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  27. package/amplify/functions/email-submission-processor/resource.js +56 -0
  28. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  29. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  30. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  31. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  32. package/amplify/functions/knowledge-query/handler.py +1 -0
  33. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  34. package/amplify/functions/knowledge-query/resource.js +50 -0
  35. package/amplify/functions/knowledge-query/resource.ts +61 -0
  36. package/amplify/functions/manage-user-role/handler.ts +799 -0
  37. package/amplify/functions/manage-user-role/resource.js +8 -0
  38. package/amplify/functions/manage-user-role/resource.ts +9 -0
  39. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  40. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  41. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  42. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  43. package/amplify/functions/newsroom-summary/resource.js +8 -0
  44. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  45. package/amplify/functions/procedure-action/handler.ts +621 -0
  46. package/amplify/functions/procedure-action/resource.js +8 -0
  47. package/amplify/functions/procedure-action/resource.ts +9 -0
  48. package/amplify/functions/reader-settings/handler.ts +322 -0
  49. package/amplify/functions/reader-settings/resource.js +8 -0
  50. package/amplify/functions/reader-settings/resource.ts +9 -0
  51. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  52. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  53. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  54. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  55. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  56. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  57. package/amplify/functions/shared/email-submission.ts +415 -0
  58. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  59. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  60. package/amplify/functions/shared/model-payloads.ts +186 -0
  61. package/amplify/functions/shared/python-bundle.js +130 -0
  62. package/amplify/functions/shared/python-bundle.ts +162 -0
  63. package/amplify/functions/shared/slack-events.ts +111 -0
  64. package/amplify/functions/slack-delivery/handler.py +112 -0
  65. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  66. package/amplify/functions/slack-delivery/resource.js +38 -0
  67. package/amplify/functions/slack-delivery/resource.ts +51 -0
  68. package/amplify/functions/slack-events/handler.ts +155 -0
  69. package/amplify/functions/slack-events/resource.js +11 -0
  70. package/amplify/functions/slack-events/resource.ts +12 -0
  71. package/amplify/inbound-email/stack.js +46 -0
  72. package/amplify/inbound-email/stack.ts +61 -0
  73. package/amplify/package.json +3 -0
  74. package/amplify/rebuild-trigger-settings.js +22 -0
  75. package/amplify/rebuild-trigger-settings.ts +42 -0
  76. package/amplify/site-backend-config.js +1 -0
  77. package/amplify/site-backend-config.ts +36 -0
  78. package/amplify/site-backend.js +615 -0
  79. package/amplify/site-backend.ts +739 -0
  80. package/amplify/storage/resource.js +21 -0
  81. package/amplify/storage/resource.ts +22 -0
  82. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  83. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  84. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  85. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  86. package/app/[year]/[month]/[day]/page.tsx +22 -0
  87. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  88. package/app/api/archive/editions/route.ts +18 -0
  89. package/app/api/media/[...path]/route.ts +35 -0
  90. package/app/api/revalidate/route.ts +94 -0
  91. package/app/archive/page.tsx +24 -0
  92. package/app/articles/[slug]/page.tsx +35 -0
  93. package/app/globals.css +10345 -0
  94. package/app/layout.tsx +142 -0
  95. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  96. package/app/newsroom/[section]/page.tsx +47 -0
  97. package/app/newsroom/articles/[id]/page.tsx +15 -0
  98. package/app/newsroom/articles/page.tsx +13 -0
  99. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  100. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  101. package/app/newsroom/insights/page.tsx +14 -0
  102. package/app/newsroom/layout.tsx +12 -0
  103. package/app/newsroom/page.tsx +31 -0
  104. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  105. package/app/page.tsx +272 -0
  106. package/app/papyrus-theme.css +11 -0
  107. package/app/robots.ts +11 -0
  108. package/app/settings/page.tsx +14 -0
  109. package/app/tailwind.css +65 -0
  110. package/bin/papyrus-app.mjs +20 -0
  111. package/bin/papyrus-infra.mjs +95 -0
  112. package/components/ai-elements/conversation.tsx +168 -0
  113. package/components/ai-elements/message.tsx +360 -0
  114. package/components/ai-elements/model-selector.tsx +30 -0
  115. package/components/ai-elements/prompt-input.tsx +1461 -0
  116. package/components/ai-elements/shimmer.tsx +76 -0
  117. package/components/ai-elements/suggestion.tsx +54 -0
  118. package/components/ai-elements/tool.tsx +173 -0
  119. package/components/amplify-client-provider.tsx +140 -0
  120. package/components/archive-grid.tsx +89 -0
  121. package/components/archive-shell.tsx +41 -0
  122. package/components/article-video.tsx +156 -0
  123. package/components/blog-page-background.tsx +20 -0
  124. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  125. package/components/markus-ir-preview.tsx +300 -0
  126. package/components/news-desk-client-provider.tsx +211 -0
  127. package/components/news-desk-page.tsx +116 -0
  128. package/components/news-desk-taxonomy-client.ts +3684 -0
  129. package/components/newspaper.tsx +1929 -0
  130. package/components/newsroom-access-gate.tsx +66 -0
  131. package/components/newsroom-article-editor.tsx +741 -0
  132. package/components/newsroom-articles-view.tsx +129 -0
  133. package/components/newsroom-assignments-view.tsx +664 -0
  134. package/components/newsroom-ops-overview.tsx +226 -0
  135. package/components/newsroom-ops-shell.tsx +321 -0
  136. package/components/newsroom-references-view.tsx +598 -0
  137. package/components/newsroom-topics-view.tsx +425 -0
  138. package/components/papyrus-console-shell.tsx +1912 -0
  139. package/components/pictogram-figure.tsx +65 -0
  140. package/components/presentation-footer.tsx +146 -0
  141. package/components/reader-auth-control.tsx +162 -0
  142. package/components/reader-auth-state.ts +122 -0
  143. package/components/reader-settings.ts +332 -0
  144. package/components/reader-theme-control.tsx +85 -0
  145. package/components/reference-source-preview.tsx +163 -0
  146. package/components/settings-page.tsx +239 -0
  147. package/components/staging-banner.tsx +11 -0
  148. package/components/topic-steering-workspace.tsx +18005 -0
  149. package/components/ui/badge.tsx +30 -0
  150. package/components/ui/button-group.tsx +87 -0
  151. package/components/ui/button.tsx +58 -0
  152. package/components/ui/card.tsx +32 -0
  153. package/components/ui/command.tsx +194 -0
  154. package/components/ui/dialog.tsx +158 -0
  155. package/components/ui/dropdown-menu.tsx +266 -0
  156. package/components/ui/hover-card.tsx +51 -0
  157. package/components/ui/input-group.tsx +158 -0
  158. package/components/ui/input.tsx +20 -0
  159. package/components/ui/scroll-area.tsx +16 -0
  160. package/components/ui/select.tsx +199 -0
  161. package/components/ui/separator.tsx +23 -0
  162. package/components/ui/sheet.tsx +92 -0
  163. package/components/ui/spinner.tsx +10 -0
  164. package/components/ui/tabs.tsx +113 -0
  165. package/components/ui/textarea.tsx +18 -0
  166. package/components/ui/tooltip.tsx +66 -0
  167. package/components/use-resolved-papyrus-theme.ts +47 -0
  168. package/infra/amplify-app-shell.js +234 -0
  169. package/infra/amplify-app-shell.ts +284 -0
  170. package/infra/build-specs.js +85 -0
  171. package/infra/build-specs.ts +131 -0
  172. package/infra/github-oidc-provider.js +24 -0
  173. package/infra/github-oidc-provider.ts +30 -0
  174. package/infra/index.js +15 -0
  175. package/infra/site-config.js +186 -0
  176. package/infra/site-config.ts +235 -0
  177. package/lib/amplify-outputs-guard.ts +24 -0
  178. package/lib/amplify-outputs-path.ts +16 -0
  179. package/lib/amplify-server-runtime.ts +63 -0
  180. package/lib/archive-data.ts +30 -0
  181. package/lib/archive-types.ts +15 -0
  182. package/lib/articles.ts +320 -0
  183. package/lib/cached-content-repository.ts +90 -0
  184. package/lib/category-dashboard.ts +37 -0
  185. package/lib/category-repository.ts +1907 -0
  186. package/lib/console-chat-client.ts +1316 -0
  187. package/lib/console-web-context.ts +118 -0
  188. package/lib/content-actions-client.ts +98 -0
  189. package/lib/content-actions-demo.ts +190 -0
  190. package/lib/content-load-timing.ts +23 -0
  191. package/lib/content-repository.ts +43 -0
  192. package/lib/content-source-context.ts +26 -0
  193. package/lib/content-types.ts +104 -0
  194. package/lib/define-site.js +3 -0
  195. package/lib/define-site.ts +21 -0
  196. package/lib/demo-amplify-outputs.ts +15 -0
  197. package/lib/dev-sandbox-editor-auth.ts +18 -0
  198. package/lib/doctrine.ts +128 -0
  199. package/lib/edition-routes.ts +155 -0
  200. package/lib/edition-sections.ts +111 -0
  201. package/lib/empty-edition-layout-plan.ts +109 -0
  202. package/lib/empty-theme.css +1 -0
  203. package/lib/excerpts.ts +8 -0
  204. package/lib/graphql-content-repository.ts +1140 -0
  205. package/lib/image-url.ts +10 -0
  206. package/lib/layout-plan.ts +981 -0
  207. package/lib/layout-scenarios.ts +564 -0
  208. package/lib/markus-body.ts +168 -0
  209. package/lib/markus-ir.ts +235 -0
  210. package/lib/markus-projection.ts +243 -0
  211. package/lib/markus-to-article.ts +93 -0
  212. package/lib/news-desk-session.ts +160 -0
  213. package/lib/newspaper-layout.ts +3459 -0
  214. package/lib/newsroom-articles.ts +325 -0
  215. package/lib/newsroom-assignments.ts +173 -0
  216. package/lib/newsroom-base-path.ts +48 -0
  217. package/lib/newsroom-card-layout.ts +102 -0
  218. package/lib/newsroom-category-drilldown.ts +133 -0
  219. package/lib/newsroom-demo-articles.ts +90 -0
  220. package/lib/newsroom-demo-dashboard.ts +267 -0
  221. package/lib/newsroom-demo-profile.ts +46 -0
  222. package/lib/newsroom-forum-routes.ts +153 -0
  223. package/lib/newsroom-index-filters.ts +202 -0
  224. package/lib/newsroom-knowledge-query-request.ts +51 -0
  225. package/lib/newsroom-list-selection.ts +42 -0
  226. package/lib/newsroom-media-backend.ts +142 -0
  227. package/lib/newsroom-media.ts +182 -0
  228. package/lib/newsroom-nav.ts +44 -0
  229. package/lib/newsroom-references.ts +116 -0
  230. package/lib/newsroom-sections.ts +52 -0
  231. package/lib/newsroom-topics.ts +118 -0
  232. package/lib/papyrus-object-kinds.ts +24 -0
  233. package/lib/papyrus-web-locations.ts +480 -0
  234. package/lib/pilobol-sample.ts +167 -0
  235. package/lib/presentation-footer.ts +92 -0
  236. package/lib/pretext-layout.ts +179 -0
  237. package/lib/public-placeholder-config.ts +102 -0
  238. package/lib/publication-items.ts +181 -0
  239. package/lib/reader-route-config.ts +1 -0
  240. package/lib/reader-static-params.ts +56 -0
  241. package/lib/reader-storage-url.ts +63 -0
  242. package/lib/reference-display.ts +74 -0
  243. package/lib/reference-policy.ts +97 -0
  244. package/lib/reference-source-preview.ts +175 -0
  245. package/lib/renderer-config.ts +50 -0
  246. package/lib/renderer.ts +87 -0
  247. package/lib/reporting-story-budget.ts +638 -0
  248. package/lib/semantic-graph.ts +554 -0
  249. package/lib/site-brand-tenant.ts +22 -0
  250. package/lib/site-brand.ts +194 -0
  251. package/lib/site-env.ts +52 -0
  252. package/lib/site-renderer.ts +17 -0
  253. package/lib/site-stack.ts +195 -0
  254. package/lib/staging-gate.ts +37 -0
  255. package/lib/staging-preview-object.ts +98 -0
  256. package/lib/themed-image.ts +30 -0
  257. package/lib/utils.ts +6 -0
  258. package/lib/video-mode.ts +42 -0
  259. package/lib/video-script.ts +63 -0
  260. package/middleware.ts +103 -0
  261. package/package.json +318 -1
  262. package/renderers/markus/index.ts +25 -0
  263. package/renderers/markus/stubs.tsx +33 -0
  264. package/renderers/pretext/article-page.tsx +169 -0
  265. package/renderers/pretext/index.ts +30 -0
  266. package/renderers/pretext/layouts.ts +16 -0
  267. package/renderers/pretext/presentation-shell.tsx +470 -0
  268. package/routes.manifest.json +208 -0
  269. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  270. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  271. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  272. package/scripts/videoml/bundle-common.mjs +129 -0
  273. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  274. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  275. package/scripts/videoml/preview/preview-timing.ts +42 -0
  276. package/scripts/videoml/preview.html +77 -0
  277. package/scripts/videoml/standard-entry.tsx +46 -0
  278. package/src/backend.js +1 -0
  279. package/src/backend.ts +3 -0
  280. package/src/sync.mjs +87 -0
  281. package/src/with-papyrus.d.mts +2 -0
  282. package/src/with-papyrus.mjs +60 -0
@@ -0,0 +1,194 @@
1
+ import type { ComponentType } from "react";
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";
6
+ import papyrusSite from "papyrus-site";
7
+ import {
8
+ DEFAULT_THEME_PACK_TOKENS,
9
+ type HostingConfig,
10
+ type OpsChrome,
11
+ type ReaderDeployment,
12
+ type RendererConfig,
13
+ type RootRouteConfig,
14
+ type ThemePackId,
15
+ type ThemePackTokens,
16
+ } from "./site-stack";
17
+
18
+ /** Open string: a publication registers its own brand through `papyrus.config.ts` (`defineSite`). */
19
+ export type SiteBrandId = string;
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
+
37
+ /**
38
+ * Video slot: how a publication supplies its own VideoML scene components.
39
+ * Paths are relative to the publication root.
40
+ */
41
+ export type SiteBrandVideo = {
42
+ /** TSX module that registers the brand's `@videoml` scene components via `window.Babulus.registerComponent`. */
43
+ bundleEntry: string;
44
+ /** DSL element aliases for the browser preview, e.g. `{ "quote-card": "acme-quote-card" }`. */
45
+ sceneComponents?: Record<string, string>;
46
+ rhythm?: Record<string, unknown>;
47
+ };
48
+
49
+ export type SiteBrand = {
50
+ id: SiteBrandId;
51
+ appTitle: string;
52
+ appDescription: string;
53
+ mastheadTitle: string;
54
+ mastheadSubtitle: string;
55
+ mastheadTagline?: string;
56
+ backToHomeLabel: string;
57
+ articleTitleSuffix: string;
58
+ placeholderByline: string;
59
+ defaultPresentation: EditionPresentationFormat;
60
+ forcedPresentation?: EditionPresentationFormat;
61
+ textFont: string;
62
+ footerTitle?: string;
63
+ footerSubtitleOverride?: string;
64
+ mastheadWordSplit: boolean;
65
+ mastheadDateFormat: "raw" | "formatted";
66
+ mastheadSource: "edition" | "brand";
67
+ sectionLinkStrategy: "route" | "anchor";
68
+ defaultVideoCredit?: string;
69
+ themePack: ThemePackId;
70
+ themeTokens: ThemePackTokens;
71
+ renderer: RendererConfig;
72
+ hosting: HostingConfig;
73
+ /** Static reader app when it is not co-hosted with this Papyrus checkout. */
74
+ readerDeployment?: ReaderDeployment;
75
+ /**
76
+ * What the root route (`/`) does. Defaults to `{ kind: "reader" }` (render
77
+ * the publication home page). Set to `{ kind: "redirect", destination }`
78
+ * for a CMS-only deployment whose reader lives elsewhere.
79
+ */
80
+ rootRoute?: RootRouteConfig;
81
+ /** Public URL prefix for newsroom routes. Default `/newsroom`; use `""` on a CMS-only subdomain. */
82
+ newsroomBasePath?: string;
83
+ opsChrome: OpsChrome;
84
+ corpusKey: string;
85
+ steeringConfigPath: string;
86
+ newsroomSectionsConfigPath: string;
87
+ analysisProfilesPath: string;
88
+ publicationName: string;
89
+ components?: SiteBrandComponents;
90
+ demoEdition?: SiteBrandDemoEdition;
91
+ video?: SiteBrandVideo;
92
+ };
93
+
94
+ const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
95
+
96
+ const REFERENCE_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
97
+ papyrus: {
98
+ id: "papyrus",
99
+ appTitle: "Papyrus",
100
+ appDescription: "A Pretext-powered responsive newspaper layout lab.",
101
+ mastheadTitle: "PAPYRUS",
102
+ mastheadSubtitle: "Inside Papyrus",
103
+ backToHomeLabel: "Back to Papyrus",
104
+ articleTitleSuffix: "Papyrus",
105
+ placeholderByline: "Papyrus",
106
+ defaultPresentation: "newsprint",
107
+ textFont: SERIF_TEXT_FONT,
108
+ mastheadWordSplit: false,
109
+ mastheadDateFormat: "raw",
110
+ mastheadSource: "edition",
111
+ sectionLinkStrategy: "route",
112
+ themePack: "papyrus",
113
+ themeTokens: DEFAULT_THEME_PACK_TOKENS,
114
+ renderer: { kind: "pretext" },
115
+ hosting: { kind: "amplify-ssr" },
116
+ opsChrome: "app",
117
+ corpusKey: "threat-intelligence",
118
+ steeringConfigPath: "corpora/papyrus-steering.yml",
119
+ newsroomSectionsConfigPath: "corpora/papyrus-newsroom-sections.yml",
120
+ analysisProfilesPath: "corpora/papyrus-analysis-profiles.yml",
121
+ publicationName: "Anthus Threat Intelligence",
122
+ },
123
+ };
124
+
125
+ /**
126
+ * The Papyrus reference brand plus whatever the publication registered in its
127
+ * own `papyrus.config.ts` (resolved through the `papyrus-site` alias).
128
+ */
129
+ const SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
130
+ ...REFERENCE_SITE_BRANDS,
131
+ ...Object.fromEntries((papyrusSite.brands ?? []).map((brand) => [brand.id, brand])),
132
+ };
133
+
134
+ export function normalizeSiteBrandId(value: string | undefined | null): SiteBrandId | null {
135
+ if (!value) return null;
136
+ const trimmed = value.trim();
137
+ return Object.prototype.hasOwnProperty.call(SITE_BRANDS, trimmed) ? trimmed : null;
138
+ }
139
+
140
+ function unknownBrandError(value: string): Error {
141
+ return new Error(`Unknown brand '${value}'. Registered: ${Object.keys(SITE_BRANDS).join(", ")}`);
142
+ }
143
+
144
+ export function resolveSiteBrandId(
145
+ raw: string | undefined | null = process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND ?? process.env.PAPYRUS_SITE_BRAND,
146
+ ): SiteBrandId {
147
+ const requested = raw?.trim();
148
+ if (requested) {
149
+ const normalized = normalizeSiteBrandId(requested);
150
+ if (normalized === null) throw unknownBrandError(requested);
151
+ return normalized;
152
+ }
153
+ const fallback = papyrusSite.defaultBrand ?? "papyrus";
154
+ const normalizedFallback = normalizeSiteBrandId(fallback);
155
+ if (normalizedFallback === null) throw unknownBrandError(fallback);
156
+ return normalizedFallback;
157
+ }
158
+
159
+ export function getSiteBrand(id: SiteBrandId = resolveSiteBrandId()): SiteBrand {
160
+ const brand = Object.prototype.hasOwnProperty.call(SITE_BRANDS, id) ? SITE_BRANDS[id] : undefined;
161
+ if (!brand) throw unknownBrandError(id);
162
+ return brand;
163
+ }
164
+
165
+ export const SITE_BRAND = getSiteBrand();
166
+
167
+ /** Root-route config for a brand, defaulting to `{ kind: "reader" }`. */
168
+ export function getRootRoute(brand: SiteBrand = SITE_BRAND): RootRouteConfig {
169
+ return brand.rootRoute ?? { kind: "reader" };
170
+ }
171
+
172
+ export const rootRoute = getRootRoute();
173
+
174
+ export function enforcePresentation(presentation: EditionPresentationFormat): EditionPresentationFormat {
175
+ return SITE_BRAND.forcedPresentation ?? presentation;
176
+ }
177
+
178
+ export function getPresentationChoices(brand: SiteBrand = SITE_BRAND): EditionPresentationFormat[] {
179
+ return brand.forcedPresentation
180
+ ? [brand.forcedPresentation]
181
+ : ["newsprint", "blog", "magazine"];
182
+ }
183
+
184
+ export function getForcedPresentation(): EditionPresentationFormat | undefined {
185
+ return SITE_BRAND.forcedPresentation;
186
+ }
187
+
188
+ export function getDefaultPretextLayout(): EditionPresentationFormat {
189
+ return SITE_BRAND.defaultPresentation;
190
+ }
191
+
192
+ export function getRendererKind(): RendererConfig["kind"] {
193
+ return SITE_BRAND.renderer.kind;
194
+ }
@@ -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,17 @@
1
+ import { MarkusSiteRendererError } from "./renderer-config";
2
+ import { SITE_BRAND } from "./site-brand";
3
+ import type { Renderer } from "./renderer";
4
+ import { pretextRenderer } from "../renderers/pretext";
5
+
6
+ export { MarkusSiteRendererError } from "./renderer-config";
7
+
8
+ export function assertPretextSite(): void {
9
+ if (SITE_BRAND.renderer.kind === "markus") {
10
+ throw new MarkusSiteRendererError("hackerman", SITE_BRAND.id);
11
+ }
12
+ }
13
+
14
+ export function getSiteRenderer(): Renderer {
15
+ assertPretextSite();
16
+ return pretextRenderer;
17
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Independent site-stack axes. Do not collapse these into one "brand mode".
3
+ * - themePack / themeTokens: ops + shared Shadcn colors
4
+ * - opsChrome: newsroom shell (app vs newsprint)
5
+ * - renderer: publication / reader output (pretext vs markus)
6
+ * - hosting: where the built site is served
7
+ *
8
+ * Shared ops components consume CSS variables only. Pilobolus is the first
9
+ * extra pack, not the platform default (that remains `papyrus`).
10
+ */
11
+ /** Open string: publications register their own pack id (see lib/define-site.ts). */
12
+ export type ThemePackId = string;
13
+
14
+ export type RendererConfig = {
15
+ kind: "pretext" | "markus";
16
+ };
17
+
18
+ export type HostingConfig =
19
+ | { kind: "amplify-ssr" }
20
+ | { kind: "amplify-static" };
21
+
22
+ /**
23
+ * What the root route (`/`) of a Papyrus app does. Independent of hosting and
24
+ * of where the public reader lives — a site configures this explicitly.
25
+ *
26
+ * - `reader`: render the publication home page here (the single-app reader+Cms
27
+ * model used by p.apyr.us and Threat Intelligence). Default when omitted.
28
+ * - `redirect`: redirect `/` to `destination` (a path or absolute URL). Use
29
+ * this for CMS-only deployments whose public reader lives on another app.
30
+ * - `newsroom`: render the newsroom desk at `/` (for a CMS-only subdomain).
31
+ */
32
+ export type RootRouteConfig =
33
+ | { kind: "reader" }
34
+ | { kind: "redirect"; destination: string; permanent?: boolean }
35
+ | { kind: "newsroom" };
36
+
37
+ /**
38
+ * Optional reader deployment when CMS and public reader ship separately
39
+ * (Pilobolus, Anth.us).
40
+ *
41
+ * `domain` and `amplifyAppId` are optional because a reader can legitimately
42
+ * exist before either is known: a staging port runs on the Amplify-provided
43
+ * hostname with no custom domain attached, and the app id does not exist until
44
+ * someone creates the app. Leave them unset rather than writing down a
45
+ * hostname that is not actually serving this build — a wrong canonical host is
46
+ * worse than an absent one.
47
+ */
48
+ export type ReaderDeployment = {
49
+ kind: "amplify-static";
50
+ repository: string;
51
+ domain?: string;
52
+ amplifyAppId?: string;
53
+ };
54
+
55
+ export type OpsChrome = "newsprint" | "app";
56
+
57
+ export type SiteOpsStack = {
58
+ chrome: OpsChrome;
59
+ themePack: ThemePackId;
60
+ themeTokens: ThemePackTokens;
61
+ };
62
+
63
+ export type SitePublicationStack = {
64
+ renderer: RendererConfig;
65
+ };
66
+
67
+ /** Ops UI and publication/reader are independently pluggable. */
68
+ export type SiteStack = {
69
+ ops: SiteOpsStack;
70
+ publication: SitePublicationStack;
71
+ hosting: HostingConfig;
72
+ };
73
+
74
+ export type SiteStackSource = {
75
+ themePack: ThemePackId;
76
+ themeTokens: ThemePackTokens;
77
+ opsChrome: OpsChrome;
78
+ renderer: RendererConfig;
79
+ hosting: HostingConfig;
80
+ };
81
+
82
+ export function getSiteStack(source: SiteStackSource): SiteStack {
83
+ return {
84
+ ops: {
85
+ chrome: source.opsChrome,
86
+ themePack: source.themePack,
87
+ themeTokens: source.themeTokens,
88
+ },
89
+ publication: { renderer: source.renderer },
90
+ hosting: source.hosting,
91
+ };
92
+ }
93
+
94
+ export type ThemePackPalette = {
95
+ paper: string;
96
+ moss: string;
97
+ ochre: string;
98
+ ink: string;
99
+ muted?: string;
100
+ card?: string;
101
+ line?: string;
102
+ quote?: string;
103
+ tip?: string;
104
+ caution?: string;
105
+ stage?: string;
106
+ };
107
+
108
+ export type ThemePackTokens = ThemePackPalette & {
109
+ dark?: ThemePackPalette;
110
+ };
111
+
112
+ export const DEFAULT_THEME_PACK_TOKENS: ThemePackTokens = {
113
+ paper: "#f4f4f5",
114
+ moss: "#18181b",
115
+ ochre: "#a1a1aa",
116
+ ink: "#3f3f46",
117
+ };
118
+
119
+ /**
120
+ * Pilobolus / fungus-among-us palette copied from AnthusAI/Pilobol.us
121
+ * `web/css/pilobil-theme-v10.css` (`:root` / `@media (prefers-color-scheme: dark)`).
122
+ * Colors only — Shadcn owns typography. Do not load that file at runtime.
123
+ */
124
+ export const PILOBOL_US_THEME_PACK_TOKENS: ThemePackTokens = {
125
+ ink: "#211d17",
126
+ muted: "#6b6153",
127
+ paper: "#f1ead9",
128
+ card: "#fbf6ea",
129
+ line: "#d7cbb2",
130
+ moss: "#3f5d43",
131
+ ochre: "#a35a2a",
132
+ quote: "#4a3548",
133
+ tip: "#4f6b3a",
134
+ caution: "#8a3324",
135
+ stage: "#ddd4bf",
136
+ dark: {
137
+ ink: "#dfded0",
138
+ muted: "#8b9184",
139
+ paper: "#14170f",
140
+ card: "#1c2117",
141
+ line: "#333b2a",
142
+ moss: "#b7d18a",
143
+ ochre: "#d0895a",
144
+ quote: "#b7a3c4",
145
+ tip: "#9fce7a",
146
+ caution: "#d17e6e",
147
+ stage: "#0d0f0a",
148
+ },
149
+ };
150
+
151
+ /**
152
+ * Anthus palette, read off the live Gatsby site at `AnthusAI/Anth.us`
153
+ * (`src/styles/variables.scss` `:root`). Colors only — Shadcn owns typography.
154
+ * Mapping from that file's names to the pack's slots:
155
+ *
156
+ * | pack slot | Anth.us variable | value |
157
+ * | --------- | ------------------------- | --------- |
158
+ * | `paper` | `--color-layout-bg` | `#f1f9fe` |
159
+ * | `card` | `--color-wrapper-bg` | `#ffffff` |
160
+ * | `ink` | `--color-text` | `#333333` |
161
+ * | `moss` | `--color-primary` | `#dc5497` |
162
+ * | `ochre` | `--color-active` | `#ff57b4` |
163
+ * | `quote` | `--color-gradient-mid` | `#9b165d` |
164
+ * | `tip` | `--color-gradient-end` | `#0389d7` |
165
+ * | `stage` | `--color-hamburger` | `#fedded` |
166
+ *
167
+ * Two slots have no counterpart on the Gatsby site and are **derived**, which
168
+ * is called out here so nobody mistakes them for copied brand values:
169
+ *
170
+ * - `line`: the site's only rule color is `hr { background: hsla(0,0%,0%,0.2) }`
171
+ * over white, flattened to `#cccccc`.
172
+ * - `muted`: the site has no muted-text token at all — it dims with
173
+ * `opacity: 0.5` instead (and references an undefined `--color-text-light`).
174
+ * `#5f6b73` is `--color-text` desaturated toward `paper`'s blue.
175
+ * - `caution`: `#e95800`, which the site uses for one of its `::marker`
176
+ * colors; it has no dedicated danger color.
177
+ *
178
+ * There is deliberately **no `dark` palette**: the Gatsby site has no dark
179
+ * mode. Its only `prefers-color-scheme: dark` block overrides two
180
+ * code-highlight tints. Inventing a dark Anthus palette here would be making
181
+ * up a look rather than porting one, so `theme.css` pins `color-scheme: light`.
182
+ */
183
+ export const ANTH_US_THEME_PACK_TOKENS: ThemePackTokens = {
184
+ ink: "#333333",
185
+ muted: "#5f6b73",
186
+ paper: "#f1f9fe",
187
+ card: "#ffffff",
188
+ line: "#cccccc",
189
+ moss: "#dc5497",
190
+ ochre: "#ff57b4",
191
+ quote: "#9b165d",
192
+ tip: "#0389d7",
193
+ caution: "#e95800",
194
+ stage: "#fedded",
195
+ };
@@ -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
+ }
@@ -0,0 +1,98 @@
1
+ import { cookies } from "next/headers";
2
+ import { getUrl } from "aws-amplify/storage/server";
3
+ import { getAmplifyServerRuntime } from "./amplify-server-runtime";
4
+ import { getSiteEnv } from "./site-env";
5
+
6
+ export const PREVIEW_ROUTE_PREFIX = "/__preview";
7
+ export const PREVIEW_KEY_PREFIX = "preview/";
8
+
9
+ const PRESIGN_SECONDS = 60;
10
+ const NOT_FOUND_ERROR_NAMES = ["NotFound", "NoSuchKey"];
11
+ const PREVIEW_EXCLUDED_PREFIXES = ["/newsroom", "/api", "/_next", PREVIEW_ROUTE_PREFIX];
12
+
13
+ export function isStaticPreviewEnabled(environment: Record<string, string | undefined> = process.env): boolean {
14
+ return getSiteEnv(environment) === "staging" && (environment.PAPYRUS_STAGING_PREVIEW ?? "").trim() === "static";
15
+ }
16
+
17
+ export function previewCandidateKeys(requestPath: string): string[] | null {
18
+ if (requestPath.includes("//")) return null;
19
+ let decoded: string;
20
+ try {
21
+ decoded = decodeURIComponent(requestPath);
22
+ } catch {
23
+ return null;
24
+ }
25
+ if (decoded.includes("//") || decoded.includes("\\") || decoded.includes("\0")) return null;
26
+ const segments = decoded.split("/").filter((segment) => segment !== "");
27
+ if (segments.some((segment) => segment === ".." || segment === ".")) return null;
28
+ if (segments.length === 0) return [`${PREVIEW_KEY_PREFIX}index.html`];
29
+ const joined = segments.join("/");
30
+ if (decoded.endsWith("/")) return [`${PREVIEW_KEY_PREFIX}${joined}/index.html`];
31
+ const base = `${PREVIEW_KEY_PREFIX}${joined}`;
32
+ return [base, `${base}.html`, `${base}/index.html`];
33
+ }
34
+
35
+ export function shouldRewriteToPreview(pathname: string): boolean {
36
+ if (pathname === "/favicon.ico" || pathname === "/robots.txt" || pathname.startsWith("/icon")) return false;
37
+ return !PREVIEW_EXCLUDED_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
38
+ }
39
+
40
+ export function isPreviewGatedPath(pathname: string): boolean {
41
+ return shouldRewriteToPreview(pathname) || pathname === PREVIEW_ROUTE_PREFIX || pathname.startsWith(`${PREVIEW_ROUTE_PREFIX}/`);
42
+ }
43
+
44
+ export function previewRewritePathname(pathname: string): string {
45
+ return `${PREVIEW_ROUTE_PREFIX}${pathname}`;
46
+ }
47
+
48
+ export function previewRequestPath(pathname: string): string {
49
+ return pathname.slice(PREVIEW_ROUTE_PREFIX.length) || "/";
50
+ }
51
+
52
+ function isNotFoundError(error: unknown): boolean {
53
+ const name = (error as { name?: string } | null)?.name ?? "";
54
+ return NOT_FOUND_ERROR_NAMES.includes(name);
55
+ }
56
+
57
+ async function presignFirstExistingKey(candidateKeys: string[]): Promise<URL | null> {
58
+ const { runWithAmplifyServerContext } = getAmplifyServerRuntime();
59
+ return runWithAmplifyServerContext({
60
+ nextServerContext: { cookies },
61
+ operation: async (contextSpec) => {
62
+ for (const key of candidateKeys) {
63
+ try {
64
+ const { url } = await getUrl(contextSpec, {
65
+ path: key,
66
+ options: { validateObjectExistence: true, expiresIn: PRESIGN_SECONDS },
67
+ });
68
+ return url;
69
+ } catch (error) {
70
+ if (!isNotFoundError(error)) throw error;
71
+ }
72
+ }
73
+ return null;
74
+ },
75
+ });
76
+ }
77
+
78
+ function notFound(): Response {
79
+ return new Response("Not found", { status: 404, headers: { "Cache-Control": "private, no-store" } });
80
+ }
81
+
82
+ export async function streamPreviewObject(pathname: string): Promise<Response> {
83
+ if (!isStaticPreviewEnabled()) return notFound();
84
+ const candidateKeys = previewCandidateKeys(previewRequestPath(pathname));
85
+ if (!candidateKeys) return notFound();
86
+ const presigned = await presignFirstExistingKey(candidateKeys);
87
+ if (!presigned) return notFound();
88
+ const upstream = await fetch(presigned);
89
+ if (!upstream.ok || !upstream.body) return notFound();
90
+ return new Response(upstream.body, {
91
+ status: 200,
92
+ headers: {
93
+ "Content-Type": upstream.headers.get("content-type") ?? "application/octet-stream",
94
+ "Cache-Control": "private, no-store",
95
+ "X-Robots-Tag": "noindex, nofollow",
96
+ },
97
+ });
98
+ }
@@ -0,0 +1,30 @@
1
+ import type { ArticleImageThemeVariants, ArticleVideoThemeVariants } from "./articles";
2
+
3
+ export type ResolvedTheme = "light" | "dark";
4
+
5
+ export function resolveThemedImageSrc(
6
+ src: string,
7
+ themeVariants: ArticleImageThemeVariants | undefined,
8
+ theme: ResolvedTheme,
9
+ ): string {
10
+ if (theme === "dark" && themeVariants?.dark?.src) return themeVariants.dark.src;
11
+ if (theme === "dark") return resolveStaticDarkSvgFallback(src) ?? src;
12
+ return src;
13
+ }
14
+
15
+ export function resolveThemedVideoSrc(
16
+ src: string,
17
+ themeVariants: ArticleVideoThemeVariants | undefined,
18
+ theme: ResolvedTheme,
19
+ ): string {
20
+ if (theme === "light" && themeVariants?.light?.src) return themeVariants.light.src;
21
+ if (theme === "dark" && themeVariants?.dark?.src) return themeVariants.dark.src;
22
+ return src;
23
+ }
24
+
25
+ function resolveStaticDarkSvgFallback(src: string): string | null {
26
+ if (src.startsWith("/seed-art/threat-intelligence/") && !src.endsWith("-dark.svg") && /\.svg(?:\?.*)?$/i.test(src)) {
27
+ return src.replace(/\.svg(?:\?.*)?$/i, (suffix) => `-dark${suffix}`);
28
+ }
29
+ return null;
30
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,42 @@
1
+ export type VideoMode = "preview" | "mp4";
2
+
3
+ function parseVideoMode(value: string | null | undefined): VideoMode | null {
4
+ if (value === "preview" || value === "mp4") return value;
5
+ return null;
6
+ }
7
+
8
+ /** Server-safe mode from env only. Production always returns `"mp4"`. */
9
+ export function resolveVideoModeFromEnv(): VideoMode {
10
+ if (process.env.NODE_ENV === "production") return "mp4";
11
+ return parseVideoMode(process.env.NEXT_PUBLIC_PAPYRUS_VIDEO_MODE) ?? "preview";
12
+ }
13
+
14
+ /** Client-only `?video=` override. Returns null when absent or in production. */
15
+ export function resolveVideoModeFromSearch(search: string): VideoMode | null {
16
+ if (process.env.NODE_ENV === "production") return null;
17
+ const query = search.startsWith("?") ? search.slice(1) : search;
18
+ return parseVideoMode(new URLSearchParams(query).get("video"));
19
+ }
20
+
21
+ /**
22
+ * Effective video mode for the reader.
23
+ * Production always uses `"mp4"`. In development, `?video=` overrides
24
+ * `NEXT_PUBLIC_PAPYRUS_VIDEO_MODE` (default `"preview"`).
25
+ */
26
+ export function resolveVideoMode(): VideoMode {
27
+ if (process.env.NODE_ENV === "production") return "mp4";
28
+ if (typeof window !== "undefined") {
29
+ const fromSearch = resolveVideoModeFromSearch(window.location.search);
30
+ if (fromSearch) return fromSearch;
31
+ }
32
+ return resolveVideoModeFromEnv();
33
+ }
34
+
35
+ /**
36
+ * Server-safe gate for fetching VideoML DSL scripts.
37
+ * Always true in development so `?video=preview` and `?video=mp4` can both
38
+ * work without restarting the server. Production never fetches scripts.
39
+ */
40
+ export function shouldFetchVideoScripts(): boolean {
41
+ return process.env.NODE_ENV === "development";
42
+ }